dsh-zen-remote 1.1.2 → 1.1.3
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 +26 -9
- package/docs/interface.md +5 -5
- package/lib/client.js +2094 -1875
- package/lib/index.js +21 -1
- package/lib/index.js.map +1 -1
- package/lib/types/client/client-config.d.ts +51 -0
- package/lib/types/client/client-config.d.ts.map +1 -0
- package/lib/types/client/effects/keyboard-avoid.d.ts +14 -7
- package/lib/types/client/effects/keyboard-avoid.d.ts.map +1 -1
- package/lib/types/client/effects/turn-fold.d.ts +14 -0
- package/lib/types/client/effects/turn-fold.d.ts.map +1 -1
- package/lib/types/client/styles/compat.css.d.ts +1 -1
- package/lib/types/client/styles/compat.css.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/lib/types/client/styles/turn-fold.css.d.ts.map +1 -1
- package/lib/types/index.d.ts +25 -0
- package/lib/types/index.d.ts.map +1 -1
- package/package.json +11 -11
- package/scripts/check-keyboard-avoid.mjs +39 -0
- package/src/client/client-config.ts +86 -0
- package/src/client/effects/keyboard-avoid.ts +34 -14
- package/src/client/effects/turn-fold.ts +22 -17
- package/src/client/styles/compat.css.ts +33 -0
- package/src/client/styles/composer.css.ts +61 -1
- package/src/client/styles/turn-fold.css.ts +51 -8
- package/src/index.ts +35 -1
package/lib/client.js
CHANGED
|
@@ -2463,6 +2463,7 @@ __modules["styles/compat.css.js"] = function (require, module, exports) {
|
|
|
2463
2463
|
// Do not reorder: styles/index.ts concatenates in this exact order.
|
|
2464
2464
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
2465
2465
|
exports.COMPAT_CSS = void 0;
|
|
2466
|
+
const attach_upload_ts_1 = require("./attach-upload.js");
|
|
2466
2467
|
exports.COMPAT_CSS = ` /* ---------- dsh-web-ui family compatibility ----------
|
|
2467
2468
|
The linxin666 plugin suite extends the shell frame directly:
|
|
2468
2469
|
- aionui-panel appends two trailing grid columns (explorer / preview)
|
|
@@ -3198,6 +3199,37 @@ exports.COMPAT_CSS = ` /* ---------- dsh-web-ui family compatibility ----------
|
|
|
3198
3199
|
display: none !important;
|
|
3199
3200
|
}
|
|
3200
3201
|
}
|
|
3202
|
+
|
|
3203
|
+
/* ---------- dsh-at-file compatibility ----------
|
|
3204
|
+
dsh-at-file registers its own \`conversation.input.dock\` entry
|
|
3205
|
+
([data-at-file-dock]) and renders one row per \`@path\` token in the
|
|
3206
|
+
draft. That is exactly the same source this plugin's attachment chips
|
|
3207
|
+
read, so on a phone a single uploaded image produced TWO entries on the
|
|
3208
|
+
composer rail: our 48px thumbnail tile AND at-file's file-name row for
|
|
3209
|
+
the same token (reported 2026-08-22).
|
|
3210
|
+
|
|
3211
|
+
Drop at-file's row for the upload directory only. Everything else the
|
|
3212
|
+
user @-mentions by hand is at-file's own job and stays — this is not a
|
|
3213
|
+
blanket hide. The path is readable from the row's own title attribute
|
|
3214
|
+
(FilesDock passes \`title={mention.relative}\`), which is a rendered
|
|
3215
|
+
contract rather than a hashed class name.
|
|
3216
|
+
|
|
3217
|
+
Phone only, and deliberately so: at \u003e= 768px our chips are hidden by
|
|
3218
|
+
composer.css.ts, which leaves at-file's row as the ONLY thing on screen
|
|
3219
|
+
representing the attachment. Hiding it there would make an uploaded file
|
|
3220
|
+
invisible.
|
|
3221
|
+
|
|
3222
|
+
When every row was an upload the container itself is left empty, and an
|
|
3223
|
+
empty flex item still claims the rail's 6px gap — so the dock goes too
|
|
3224
|
+
unless at least one non-upload row survives. */
|
|
3225
|
+
@media (max-width: 767px) {
|
|
3226
|
+
[data-at-file-dock] [data-at-file-row]:has([title^="${attach_upload_ts_1.UPLOAD_DIR}/"]) {
|
|
3227
|
+
display: none !important;
|
|
3228
|
+
}
|
|
3229
|
+
[data-at-file-dock]:not(:has([data-at-file-row] [title]:not([title^="${attach_upload_ts_1.UPLOAD_DIR}/"]))) {
|
|
3230
|
+
display: none !important;
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3201
3233
|
`;
|
|
3202
3234
|
};
|
|
3203
3235
|
__modules["styles/misc.css.js"] = function (require, module, exports) {
|
|
@@ -4670,9 +4702,18 @@ exports.COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- */
|
|
|
4670
4702
|
/* --- 7b. attachment chips (S7.1) ---
|
|
4671
4703
|
Our own dock entry opts OUT of the 26px pill cage above: an image chip is
|
|
4672
4704
|
a 48px tile. The row sits tight under the card's top edge, so the chips
|
|
4673
|
-
read as part of the composer rather than as a floating strip.
|
|
4705
|
+
read as part of the composer rather than as a floating strip.
|
|
4706
|
+
|
|
4707
|
+
It also takes the LAST line of its own (line policy in 7c): a 48px tile
|
|
4708
|
+
beside the to-do chip on one nowrap line squeezed that chip down to an
|
|
4709
|
+
unreadable stub, with a long file name pushing the rest off-screen
|
|
4710
|
+
(reported 2026-08-22). Its own chips wrap rather than overflow, so
|
|
4711
|
+
several attachments stack instead of scrolling out of reach. */
|
|
4674
4712
|
[data-slot="conversation.input.dock"] > [data-mobile-nav="attach-chips"] {
|
|
4675
4713
|
display: flex !important;
|
|
4714
|
+
flex: 1 0 100% !important;
|
|
4715
|
+
flex-wrap: wrap !important;
|
|
4716
|
+
order: 2 !important;
|
|
4676
4717
|
align-items: center !important;
|
|
4677
4718
|
gap: 6px !important;
|
|
4678
4719
|
max-width: none !important;
|
|
@@ -4682,6 +4723,57 @@ exports.COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- */
|
|
|
4682
4723
|
overflow: visible !important;
|
|
4683
4724
|
margin-bottom: -2px;
|
|
4684
4725
|
}
|
|
4726
|
+
|
|
4727
|
+
/* --- 7c. the queue strip opts OUT of the cage, on a line of its own ---
|
|
4728
|
+
Third entry that is not a pill. conversation.input.dock is a \`list\` slot
|
|
4729
|
+
and DSH's own QueueDock registers into it at order 20 (id \`queue\`,
|
|
4730
|
+
dsh-client-ui-conversation) — a 36px row per queued message, or a
|
|
4731
|
+
collapsible "N 条排队消息" header once there is more than one. The 26px
|
|
4732
|
+
cage above clipped it to a sliver: on a phone a queued message showed as
|
|
4733
|
+
a grey stub poking out from behind the input card, unreadable and with
|
|
4734
|
+
its edit / delete / send buttons cut off (reported 2026-08-22, same
|
|
4735
|
+
failure the to-do card hit in 7a).
|
|
4736
|
+
|
|
4737
|
+
Two things are restored. Geometry: the official row height back, and the
|
|
4738
|
+
dock's own composer-column sizing dropped — its width/max-width are
|
|
4739
|
+
calc()s over the composer card variables and its bottom margin is
|
|
4740
|
+
negative by design (upstream tucks the strip UNDER the card), none of
|
|
4741
|
+
which survives being a flex item of our chip rail.
|
|
4742
|
+
|
|
4743
|
+
Layout — the line policy for the whole rail, now that three things want
|
|
4744
|
+
room on it. The pills (the to-do card, a branch chip, whatever else
|
|
4745
|
+
registers) keep order 0 and share the top line, still scrolling
|
|
4746
|
+
horizontally. The queue takes the next line (order 1). The attachment
|
|
4747
|
+
chips take the last one (order 2, 7b), so what the user is about to send
|
|
4748
|
+
sits closest to the input it will be sent from. The rail only wraps
|
|
4749
|
+
while one of the two full-width entries exists, so a session with
|
|
4750
|
+
neither keeps the original single scrolling chip line. */
|
|
4751
|
+
[data-slot="conversation.input.dock"]:has(> [data-queue-dock], > [data-mobile-nav="attach-chips"]) {
|
|
4752
|
+
flex-wrap: wrap !important;
|
|
4753
|
+
}
|
|
4754
|
+
[data-slot="conversation.input.dock"] > [data-queue-dock] {
|
|
4755
|
+
flex: 1 0 100% !important;
|
|
4756
|
+
order: 1 !important;
|
|
4757
|
+
max-width: none !important;
|
|
4758
|
+
min-height: 0 !important;
|
|
4759
|
+
max-height: none !important;
|
|
4760
|
+
margin: 0 !important;
|
|
4761
|
+
padding: 0 !important;
|
|
4762
|
+
border-radius: 0 !important;
|
|
4763
|
+
overflow: visible !important;
|
|
4764
|
+
font-size: 13px !important;
|
|
4765
|
+
line-height: normal !important;
|
|
4766
|
+
}
|
|
4767
|
+
/* The panel is the dock's only child; upstream rounds its top corners only
|
|
4768
|
+
and drops the bottom border, because there it is half-hidden behind the
|
|
4769
|
+
card. On its own line it is a free-standing strip, so close it up. */
|
|
4770
|
+
[data-slot="conversation.input.dock"] > [data-queue-dock] > * {
|
|
4771
|
+
border-radius: 12px !important;
|
|
4772
|
+
}
|
|
4773
|
+
[data-slot="conversation.input.dock"] > [data-queue-dock] > *::after {
|
|
4774
|
+
border-bottom: 1px solid var(--dsw-alias-border-l1) !important;
|
|
4775
|
+
}
|
|
4776
|
+
|
|
4685
4777
|
[data-mobile-nav="attach-chip"] {
|
|
4686
4778
|
position: relative;
|
|
4687
4779
|
display: inline-flex;
|
|
@@ -5140,1996 +5232,2103 @@ exports.INFO_CSS = `/* ---------- session-info sheet (< 768px) ---------- */
|
|
|
5140
5232
|
}
|
|
5141
5233
|
`;
|
|
5142
5234
|
};
|
|
5143
|
-
__modules["
|
|
5235
|
+
__modules["client-config.js"] = function (require, module, exports) {
|
|
5144
5236
|
"use strict";
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5237
|
+
/**
|
|
5238
|
+
* The plugin row's client-facing knobs, fetched once per page.
|
|
5239
|
+
*
|
|
5240
|
+
* The client bundle ships statically and never sees the row config, so the
|
|
5241
|
+
* host republishes the client-relevant subset at {@link CLIENT_CONFIG_ROUTE}
|
|
5242
|
+
* (src/index.ts). Two effects read it — turn-fold's desktop switch and
|
|
5243
|
+
* keyboard-avoid's fallback tuning — so the request is memoized here rather
|
|
5244
|
+
* than fired once per reader.
|
|
5245
|
+
*
|
|
5246
|
+
* Defaults live in THIS file, not in the host: the route omits a knob the
|
|
5247
|
+
* row never set, so there is exactly one place each default is written and
|
|
5248
|
+
* no way for the two halves to drift apart on the value. The host's job is
|
|
5249
|
+
* only to validate and clamp what a user actually typed.
|
|
5250
|
+
*
|
|
5251
|
+
* Every read is total — a failed request, a non-JSON body, or a garbage
|
|
5252
|
+
* value all fall back to the defaults rather than throwing into an effect.
|
|
5253
|
+
*/
|
|
5157
5254
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5158
|
-
exports.
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
align-self: flex-start;
|
|
5177
|
-
align-items: center;
|
|
5178
|
-
gap: 6px;
|
|
5179
|
-
margin: -4px 0;
|
|
5180
|
-
padding: 0 10px;
|
|
5181
|
-
height: 26px;
|
|
5182
|
-
border: none;
|
|
5183
|
-
border-radius: 13px;
|
|
5184
|
-
background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06));
|
|
5185
|
-
color: var(--dsw-alias-label-secondary, rgba(0, 0, 0, .55));
|
|
5186
|
-
font: inherit;
|
|
5187
|
-
font-size: 12px;
|
|
5188
|
-
line-height: 26px;
|
|
5189
|
-
white-space: nowrap;
|
|
5190
|
-
cursor: pointer;
|
|
5191
|
-
-webkit-tap-highlight-color: transparent;
|
|
5192
|
-
}
|
|
5193
|
-
${scope} [data-chat-flow] > [data-mobile-nav="turn-fold"]:active {
|
|
5194
|
-
background: var(--dsw-alias-interactive-bg-pressed, rgba(0, 0, 0, .1));
|
|
5195
|
-
}
|
|
5196
|
-
|
|
5197
|
-
/* Leading dot: idle turns get a quiet mark, a running turn gets the same
|
|
5198
|
-
business-primary colour the session header's own status dot uses
|
|
5199
|
-
(styles/header.css.ts) plus a breathing pulse. */
|
|
5200
|
-
${scope} [data-chat-flow] > [data-mobile-nav="turn-fold"]::before {
|
|
5201
|
-
content: '';
|
|
5202
|
-
flex: none;
|
|
5203
|
-
width: 6px;
|
|
5204
|
-
height: 6px;
|
|
5205
|
-
border-radius: 50%;
|
|
5206
|
-
background: currentColor;
|
|
5207
|
-
opacity: .45;
|
|
5208
|
-
}
|
|
5209
|
-
${scope} [data-chat-flow] > [data-mobile-nav="turn-fold"][data-running]::before {
|
|
5210
|
-
background: var(--dsw-alias-state-business-primary, #4f6ef7);
|
|
5211
|
-
opacity: 1;
|
|
5212
|
-
animation: dsh-mobile-nav-breathe 1.4s var(--ds-ease-in-out, ease-in-out) infinite;
|
|
5213
|
-
}
|
|
5214
|
-
|
|
5215
|
-
/* Chevron drawn from two borders — no icon injection, no mask image, and
|
|
5216
|
-
it rotates to point up once the turn is open. */
|
|
5217
|
-
${scope} [data-chat-flow] > [data-mobile-nav="turn-fold"]::after {
|
|
5218
|
-
content: '';
|
|
5219
|
-
flex: none;
|
|
5220
|
-
width: 5px;
|
|
5221
|
-
height: 5px;
|
|
5222
|
-
margin-top: -3px;
|
|
5223
|
-
border-right: 1.5px solid currentColor;
|
|
5224
|
-
border-bottom: 1.5px solid currentColor;
|
|
5225
|
-
transform: rotate(45deg);
|
|
5226
|
-
transition: transform .16s var(--ds-ease-out, ease-in-out);
|
|
5227
|
-
}
|
|
5228
|
-
${scope} [data-chat-flow] > [data-mobile-nav="turn-fold"][data-open]::after {
|
|
5229
|
-
margin-top: 3px;
|
|
5230
|
-
transform: rotate(-135deg);
|
|
5231
|
-
}
|
|
5232
|
-
|
|
5233
|
-
@media (prefers-reduced-motion: reduce) {
|
|
5234
|
-
${scope} [data-chat-flow] > [data-mobile-nav="turn-fold"][data-running]::before {
|
|
5235
|
-
animation: none;
|
|
5236
|
-
}
|
|
5237
|
-
${scope} [data-chat-flow] > [data-mobile-nav="turn-fold"]::after {
|
|
5238
|
-
transition: none;
|
|
5239
|
-
}
|
|
5240
|
-
}
|
|
5241
|
-
`;
|
|
5242
|
-
exports.TURN_FOLD_CSS = `/* ---------- turn process fold (< 768px, or desktop opt-in) ---------- */
|
|
5243
|
-
|
|
5244
|
-
/* The summary row never paints outside its active scope, even if a
|
|
5245
|
-
media-query change raced the effect's own cleanup. */
|
|
5246
|
-
[data-mobile-nav="turn-fold"] {
|
|
5247
|
-
display: none;
|
|
5255
|
+
exports.KEYBOARD_DEFAULTS = exports.CLIENT_CONFIG_ROUTE = void 0;
|
|
5256
|
+
exports.parseClientConfig = parseClientConfig;
|
|
5257
|
+
exports.clientConfig = clientConfig;
|
|
5258
|
+
/** Mirror of src/index.ts CLIENT_CONFIG_ROUTE (client tsconfig cannot reach it). */
|
|
5259
|
+
exports.CLIENT_CONFIG_ROUTE = '/_dsh/mobile-nav/client-config';
|
|
5260
|
+
/** Shipped tuning: the values measured on the one reported device (issue #1,
|
|
5261
|
+
* WeType ~315 CSS px on an 858px viewport) plus the Android IME under-report
|
|
5262
|
+
* pad. Changing these changes the default for every install — a per-device
|
|
5263
|
+
* fix belongs in that install's plugin row instead. */
|
|
5264
|
+
exports.KEYBOARD_DEFAULTS = {
|
|
5265
|
+
liftRatio: 0.42,
|
|
5266
|
+
liftMaxPx: 400,
|
|
5267
|
+
safetyPadPx: 15,
|
|
5268
|
+
};
|
|
5269
|
+
/** One published number, or the shipped default when the row left it unset
|
|
5270
|
+
* (the route omits the key) or the body is not what it claims to be. */
|
|
5271
|
+
function num(value, fallback) {
|
|
5272
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
5248
5273
|
}
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5274
|
+
/** Parse one route body. Exported for scripts/check-client-config.mjs. */
|
|
5275
|
+
function parseClientConfig(body) {
|
|
5276
|
+
const row = typeof body === 'object' && body !== null
|
|
5277
|
+
? body
|
|
5278
|
+
: {};
|
|
5279
|
+
return {
|
|
5280
|
+
turnFoldDesktop: row['turnFoldDesktop'] === true,
|
|
5281
|
+
keyboard: {
|
|
5282
|
+
liftRatio: num(row['keyboardLiftRatio'], exports.KEYBOARD_DEFAULTS.liftRatio),
|
|
5283
|
+
liftMaxPx: num(row['keyboardLiftMaxPx'], exports.KEYBOARD_DEFAULTS.liftMaxPx),
|
|
5284
|
+
safetyPadPx: num(row['keyboardSafetyPadPx'], exports.KEYBOARD_DEFAULTS.safetyPadPx),
|
|
5285
|
+
},
|
|
5286
|
+
};
|
|
5287
|
+
}
|
|
5288
|
+
let cached;
|
|
5289
|
+
/** The row config, fetched at most once per page load. Never rejects. */
|
|
5290
|
+
function clientConfig() {
|
|
5291
|
+
cached ??= fetch(exports.CLIENT_CONFIG_ROUTE, { cache: 'no-store' })
|
|
5292
|
+
.then((res) => (res.ok ? res.json() : undefined))
|
|
5293
|
+
.catch(() => undefined)
|
|
5294
|
+
.then(parseClientConfig);
|
|
5295
|
+
return cached;
|
|
5260
5296
|
}
|
|
5261
|
-
`;
|
|
5262
5297
|
};
|
|
5263
|
-
__modules["
|
|
5298
|
+
__modules["locales.js"] = function (require, module, exports) {
|
|
5264
5299
|
"use strict";
|
|
5265
|
-
// chips — S5 (2026-08-17): the home-screen plugin-entry chips row, its
|
|
5266
|
-
// customize sheet, the settings gear button, and the settings/usage-stats
|
|
5267
|
-
// dialog portal fix. Appended LAST in index.ts (after home.css.ts) so its
|
|
5268
|
-
// `display` overrides win any tie against the phone shell's blanket
|
|
5269
|
-
// `[data-mobile-nav="frame"] > :first-child { display: none }` rule — though
|
|
5270
|
-
// the extra `:has()` clause already outranks it on specificity alone.
|
|
5271
5300
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5272
|
-
exports.
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
[data-mobile-nav="frame"] > :first-child:has([aria-modal="true"], [data-usage-stats-panel], .dshst-overlay) [class$="_newSession"],
|
|
5328
|
-
[data-mobile-nav="frame"] > :first-child:has([aria-modal="true"], [data-usage-stats-panel], .dshst-overlay) [class$="_regionArea"] {
|
|
5329
|
-
display: none !important;
|
|
5330
|
-
}
|
|
5331
|
-
|
|
5332
|
-
/* Home-top settings gear: 44pt touch target, pushed to the row's end by
|
|
5333
|
-
its own auto margin (home-top itself stays a plain flex row). */
|
|
5334
|
-
[data-mobile-nav="home-settings"] {
|
|
5335
|
-
flex: none;
|
|
5336
|
-
display: inline-flex;
|
|
5337
|
-
align-items: center;
|
|
5338
|
-
justify-content: center;
|
|
5339
|
-
width: 44px;
|
|
5340
|
-
height: 44px;
|
|
5341
|
-
margin-left: auto;
|
|
5342
|
-
padding: 0;
|
|
5343
|
-
border: none;
|
|
5344
|
-
background: transparent;
|
|
5345
|
-
color: var(--dsw-alias-label-secondary, inherit);
|
|
5346
|
-
cursor: pointer;
|
|
5347
|
-
touch-action: manipulation;
|
|
5348
|
-
-webkit-tap-highlight-color: transparent;
|
|
5349
|
-
}
|
|
5350
|
-
[data-mobile-nav="home-settings"]:active {
|
|
5351
|
-
background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06));
|
|
5352
|
-
border-radius: 50%;
|
|
5353
|
-
}
|
|
5354
|
-
|
|
5355
|
-
/* Plugin-entry chips row: one horizontally-scrolling line of 34px pills
|
|
5356
|
-
between the workspace title and the session list (prototype spec).
|
|
5357
|
-
Scrollbar hidden on BOTH engines (2026-08-18 user decision): iOS
|
|
5358
|
-
overlay scrollbars never showed anyway, but desktop browsers painted a
|
|
5359
|
-
permanent 2px strip under the pills. This revisits the AGENTS.md
|
|
5360
|
-
"silently cut off the last tab" lesson deliberately — with 4+ chips the
|
|
5361
|
-
row now always overflows mid-chip (a partially visible pill is the
|
|
5362
|
-
scroll affordance), unlike the early incident where the chips fit
|
|
5363
|
-
flush and the hidden overflow was undiscoverable. */
|
|
5364
|
-
[data-mobile-nav="chip-row"] {
|
|
5365
|
-
flex: 0 0 auto;
|
|
5366
|
-
display: flex;
|
|
5367
|
-
align-items: center;
|
|
5368
|
-
gap: 8px;
|
|
5369
|
-
padding: 0 16px 10px;
|
|
5370
|
-
overflow-x: auto;
|
|
5371
|
-
overscroll-behavior: contain;
|
|
5372
|
-
-webkit-overflow-scrolling: touch;
|
|
5373
|
-
scrollbar-width: none;
|
|
5374
|
-
}
|
|
5375
|
-
[data-mobile-nav="chip-row"]::-webkit-scrollbar {
|
|
5376
|
-
display: none;
|
|
5377
|
-
}
|
|
5378
|
-
[data-mobile-nav="chip"],
|
|
5379
|
-
[data-mobile-nav="chip-more"] {
|
|
5380
|
-
flex: none;
|
|
5381
|
-
display: inline-flex;
|
|
5382
|
-
align-items: center;
|
|
5383
|
-
gap: 6px;
|
|
5384
|
-
height: 34px;
|
|
5385
|
-
padding: 0 14px;
|
|
5386
|
-
border: none;
|
|
5387
|
-
border-radius: 17px;
|
|
5388
|
-
/* Same surface as the list rows: the home page is bg-base since the
|
|
5389
|
-
2026-08-20 swap (see home.css.ts), so a bg-base chip would disappear
|
|
5390
|
-
into it. */
|
|
5391
|
-
background: var(--dsw-specific-sidebar-fill, #f5f5f5);
|
|
5392
|
-
color: var(--dsw-alias-label-primary, inherit);
|
|
5393
|
-
font-family: inherit;
|
|
5394
|
-
font-size: 13px;
|
|
5395
|
-
font-weight: 500;
|
|
5396
|
-
white-space: nowrap;
|
|
5397
|
-
cursor: pointer;
|
|
5398
|
-
touch-action: manipulation;
|
|
5399
|
-
-webkit-tap-highlight-color: transparent;
|
|
5400
|
-
box-shadow: 0 1px 3px rgba(0, 0, 0, .06);
|
|
5401
|
-
}
|
|
5402
|
-
[data-mobile-nav="chip-more"] {
|
|
5403
|
-
padding: 0 9px;
|
|
5404
|
-
}
|
|
5405
|
-
[data-mobile-nav="chip"]:active,
|
|
5406
|
-
[data-mobile-nav="chip-more"]:active {
|
|
5407
|
-
background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06));
|
|
5408
|
-
}
|
|
5409
|
-
|
|
5410
|
-
/* Harvested chip icon (S5.1): the cloned <svg> keeps whatever width/height
|
|
5411
|
-
the source plugin gave it (18px for the one verified live example) —
|
|
5412
|
-
force it down to the same 16px box every hand-registered chip icon
|
|
5413
|
-
uses, CSS wins over the presentational SVG attributes. */
|
|
5414
|
-
[data-mobile-nav="chip-harvest-icon"] {
|
|
5415
|
-
display: inline-flex;
|
|
5416
|
-
flex: none;
|
|
5417
|
-
}
|
|
5418
|
-
[data-mobile-nav="chip-harvest-icon"] svg {
|
|
5419
|
-
display: block;
|
|
5420
|
-
width: 16px;
|
|
5421
|
-
height: 16px;
|
|
5422
|
-
}
|
|
5423
|
-
|
|
5424
|
-
/* Customize sheet: toggle rows inside the SAME home-sheet-layer/mask/sheet
|
|
5425
|
-
chrome the workspace switcher declares in home.css.ts — S6's
|
|
5426
|
-
drag-to-close and mask-click-close bind to
|
|
5427
|
-
\`[data-mobile-nav="home-sheet"]\` generically (effects/gestures.ts), so
|
|
5428
|
-
this second use of the marker needs no new gesture wiring. */
|
|
5429
|
-
[data-mobile-nav="chip-toggle-row"] {
|
|
5430
|
-
display: flex;
|
|
5431
|
-
align-items: center;
|
|
5432
|
-
gap: 10px;
|
|
5433
|
-
width: 100%;
|
|
5434
|
-
/* Real-device fix: without border-box this row's used width is
|
|
5435
|
-
100% (of the sheet's own already-padded content box) PLUS the
|
|
5436
|
-
12px+12px padding below, overflowing the sheet by 24px and
|
|
5437
|
-
shoving the flex:none toggle off its right edge (2026-08-17
|
|
5438
|
-
report: "开关跑到屏幕外"). home-sheet-item next door in
|
|
5439
|
-
home.css.ts has the identical width:100%+padding shape and the
|
|
5440
|
-
same latent bug, just with no trailing element to visibly clip. */
|
|
5441
|
-
box-sizing: border-box;
|
|
5442
|
-
min-height: 48px;
|
|
5443
|
-
padding: 0 12px;
|
|
5444
|
-
color: inherit;
|
|
5445
|
-
}
|
|
5446
|
-
[data-mobile-nav="chip-toggle-label"] {
|
|
5447
|
-
flex: 1 1 auto;
|
|
5448
|
-
min-width: 0;
|
|
5449
|
-
font-size: 15px;
|
|
5450
|
-
overflow: hidden;
|
|
5451
|
-
text-overflow: ellipsis;
|
|
5452
|
-
white-space: nowrap;
|
|
5453
|
-
}
|
|
5454
|
-
[data-mobile-nav="chip-toggle"] {
|
|
5455
|
-
flex: none;
|
|
5456
|
-
position: relative;
|
|
5457
|
-
width: 42px;
|
|
5458
|
-
height: 26px;
|
|
5459
|
-
border: none;
|
|
5460
|
-
border-radius: 13px;
|
|
5461
|
-
background: var(--dsw-alias-border-l2, rgba(0, 0, 0, .16));
|
|
5462
|
-
cursor: pointer;
|
|
5463
|
-
touch-action: manipulation;
|
|
5464
|
-
-webkit-tap-highlight-color: transparent;
|
|
5465
|
-
transition: background .15s var(--ds-ease-in-out, ease-in-out);
|
|
5466
|
-
}
|
|
5467
|
-
[data-mobile-nav="chip-toggle"]::after {
|
|
5468
|
-
content: '';
|
|
5469
|
-
position: absolute;
|
|
5470
|
-
top: 2px;
|
|
5471
|
-
left: 2px;
|
|
5472
|
-
width: 22px;
|
|
5473
|
-
height: 22px;
|
|
5474
|
-
border-radius: 50%;
|
|
5475
|
-
background: #ffffff;
|
|
5476
|
-
box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
|
|
5477
|
-
transition: transform .15s var(--ds-ease-in-out, ease-in-out);
|
|
5478
|
-
}
|
|
5479
|
-
[data-mobile-nav="chip-toggle"][aria-checked="true"] {
|
|
5480
|
-
background: var(--dsw-alias-button-primary-fill, #1a1a1a);
|
|
5481
|
-
}
|
|
5482
|
-
[data-mobile-nav="chip-toggle"][aria-checked="true"]::after {
|
|
5483
|
-
transform: translateX(16px);
|
|
5484
|
-
}
|
|
5485
|
-
@media (prefers-reduced-motion: reduce) {
|
|
5486
|
-
[data-mobile-nav="chip-toggle"],
|
|
5487
|
-
[data-mobile-nav="chip-toggle"]::after {
|
|
5488
|
-
transition: none !important;
|
|
5489
|
-
}
|
|
5490
|
-
}
|
|
5491
|
-
}
|
|
5492
|
-
`;
|
|
5301
|
+
exports.en = exports.zh = exports.NS = void 0;
|
|
5302
|
+
/** `mobileNav` namespace dictionaries: drawer controls. */
|
|
5303
|
+
exports.NS = 'mobileNav';
|
|
5304
|
+
/** Simplified Chinese dictionary (the key-set source of truth). */
|
|
5305
|
+
exports.zh = {
|
|
5306
|
+
'open': '打开目录',
|
|
5307
|
+
'close': '收起目录',
|
|
5308
|
+
'backdrop': '点击关闭目录',
|
|
5309
|
+
'sessionLog': '导出会话日志',
|
|
5310
|
+
'files': '文件浏览',
|
|
5311
|
+
'previewFullscreen': '全屏预览',
|
|
5312
|
+
'previewExitFullscreen': '退出全屏',
|
|
5313
|
+
'allWorkspaces': '全部',
|
|
5314
|
+
'switchWorkspace': '切换工作区',
|
|
5315
|
+
'newSession': '新建会话',
|
|
5316
|
+
'noSessions': '还没有会话,点右下角加号开始',
|
|
5317
|
+
'homeStatusOngoing': '运行中',
|
|
5318
|
+
'homeStatusWarning': '待处理',
|
|
5319
|
+
'homeStatusDone': '已完成',
|
|
5320
|
+
'backToList': '返回会话列表',
|
|
5321
|
+
'sessionInfo': '会话信息',
|
|
5322
|
+
'workbench': '工作台',
|
|
5323
|
+
'workbenchClose': '关闭工作台',
|
|
5324
|
+
'switchView': '切换视图',
|
|
5325
|
+
'attach': '添加附件',
|
|
5326
|
+
'attachBusy': '上传中…',
|
|
5327
|
+
'attachFailed': '上传失败,点按重试',
|
|
5328
|
+
'attachRemove': '移除附件 {name}',
|
|
5329
|
+
'infoClose': '关闭',
|
|
5330
|
+
'infoMode': '模式',
|
|
5331
|
+
'infoCwdFallback': '未知目录',
|
|
5332
|
+
'infoSubagents': '{count} 个子代理',
|
|
5333
|
+
'infoJobs': '{count} 个后台任务',
|
|
5334
|
+
'activityOpen': '查看子代理与后台任务',
|
|
5335
|
+
'infoStatTurns': '轮次',
|
|
5336
|
+
'infoStatSteps': '步骤',
|
|
5337
|
+
'infoStatTtft': '首字延迟',
|
|
5338
|
+
'infoStatLlm': '模型耗时',
|
|
5339
|
+
'infoStatTool': '工具耗时',
|
|
5340
|
+
'infoStatCacheHit': '缓存命中',
|
|
5341
|
+
'infoTokenFlow': 'Token {io}',
|
|
5342
|
+
'infoExport': '导出日志',
|
|
5343
|
+
'infoRename': '重命名',
|
|
5344
|
+
'infoRenamePrompt': '会话新名称',
|
|
5345
|
+
'infoFork': 'Fork 会话',
|
|
5346
|
+
'infoArchive': '归档',
|
|
5347
|
+
'infoArchiveConfirm': '归档后将从会话列表隐藏,确定继续?',
|
|
5348
|
+
'infoActionError': '操作失败:{message}',
|
|
5349
|
+
'turnFold': '过程 · {count} 步',
|
|
5350
|
+
'turnFoldRunning': '进行中 · {count} 步',
|
|
5351
|
+
'settings': '设置',
|
|
5352
|
+
'chipTaskboard': '任务看板',
|
|
5353
|
+
'chipSsh': 'SSH',
|
|
5354
|
+
'chipUsage': '用量',
|
|
5355
|
+
'chipCustomize': '自定义入口',
|
|
5493
5356
|
};
|
|
5494
|
-
|
|
5357
|
+
/** English dictionary, key-identical to the Chinese source of truth. */
|
|
5358
|
+
exports.en = {
|
|
5359
|
+
'open': 'Open directory',
|
|
5360
|
+
'close': 'Close directory',
|
|
5361
|
+
'backdrop': 'Click to close directory',
|
|
5362
|
+
'sessionLog': 'Session log',
|
|
5363
|
+
'files': 'Files',
|
|
5364
|
+
'previewFullscreen': 'Fullscreen preview',
|
|
5365
|
+
'previewExitFullscreen': 'Exit fullscreen',
|
|
5366
|
+
'allWorkspaces': 'All',
|
|
5367
|
+
'switchWorkspace': 'Switch workspace',
|
|
5368
|
+
'newSession': 'New session',
|
|
5369
|
+
'noSessions': 'No sessions yet — tap + to start one',
|
|
5370
|
+
'homeStatusOngoing': 'Running',
|
|
5371
|
+
'homeStatusWarning': 'Needs attention',
|
|
5372
|
+
'homeStatusDone': 'Done',
|
|
5373
|
+
'backToList': 'Back to sessions',
|
|
5374
|
+
'sessionInfo': 'Session info',
|
|
5375
|
+
'workbench': 'Workbench',
|
|
5376
|
+
'workbenchClose': 'Close workbench',
|
|
5377
|
+
'switchView': 'Switch view',
|
|
5378
|
+
'attach': 'Add attachment',
|
|
5379
|
+
'attachBusy': 'Uploading…',
|
|
5380
|
+
'attachFailed': 'Upload failed — tap to retry',
|
|
5381
|
+
'attachRemove': 'Remove attachment {name}',
|
|
5382
|
+
'infoClose': 'Close',
|
|
5383
|
+
'infoMode': 'Mode',
|
|
5384
|
+
'infoCwdFallback': 'Unknown directory',
|
|
5385
|
+
'infoSubagents': '{count} subagents',
|
|
5386
|
+
'infoJobs': '{count} background tasks',
|
|
5387
|
+
'activityOpen': 'View subagents and background tasks',
|
|
5388
|
+
'infoStatTurns': 'Turns',
|
|
5389
|
+
'infoStatSteps': 'Steps',
|
|
5390
|
+
'infoStatTtft': 'TTFT',
|
|
5391
|
+
'infoStatLlm': 'LLM time',
|
|
5392
|
+
'infoStatTool': 'Tool time',
|
|
5393
|
+
'infoStatCacheHit': 'Cache hit',
|
|
5394
|
+
'infoTokenFlow': 'Tokens {io}',
|
|
5395
|
+
'infoExport': 'Export log',
|
|
5396
|
+
'infoRename': 'Rename',
|
|
5397
|
+
'infoRenamePrompt': 'New session name',
|
|
5398
|
+
'infoFork': 'Fork session',
|
|
5399
|
+
'infoArchive': 'Archive',
|
|
5400
|
+
'infoArchiveConfirm': 'Archiving hides this session from the list. Continue?',
|
|
5401
|
+
'infoActionError': 'Action failed: {message}',
|
|
5402
|
+
'turnFold': 'Process · {count} steps',
|
|
5403
|
+
'turnFoldRunning': 'Working · {count} steps',
|
|
5404
|
+
'settings': 'Settings',
|
|
5405
|
+
'chipTaskboard': 'Task board',
|
|
5406
|
+
'chipSsh': 'SSH',
|
|
5407
|
+
'chipUsage': 'Usage',
|
|
5408
|
+
'chipCustomize': 'Customize shortcuts',
|
|
5409
|
+
};
|
|
5410
|
+
};
|
|
5411
|
+
__modules["effects/turn-fold.js"] = function (require, module, exports) {
|
|
5495
5412
|
"use strict";
|
|
5496
|
-
// content — chat markdown readability on phones (2026-08-18). Shares no
|
|
5497
|
-
// selector with any other file; appended last in styles/index.ts.
|
|
5498
5413
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5499
|
-
exports.
|
|
5500
|
-
exports.
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
block side agrees. Prose is untouched until something would overflow;
|
|
5525
|
-
the table is already pinned wide by min-width: max-content below. */
|
|
5526
|
-
[data-chat-flow],
|
|
5527
|
-
[class*="_markdown_"] {
|
|
5528
|
-
overflow-wrap: anywhere;
|
|
5529
|
-
}
|
|
5530
|
-
|
|
5531
|
-
/* Transcript density (2026-08-20): the desktop prose rhythm is 28px of
|
|
5532
|
-
leading on 15px text — a 1.87 ratio that reads generously on a wide
|
|
5533
|
-
column but wastes a phone screen, where the same paragraph is three
|
|
5534
|
-
times as many lines. Unitless 1.65 so nested blocks (code, quotes,
|
|
5535
|
-
lists) recompute from their OWN font-size instead of inheriting a fixed
|
|
5536
|
-
28px that is far too loose for 12-13px chrome text. 1.65 is the floor I
|
|
5537
|
-
am willing to go to for mixed CJK/Latin: the glyphs are full-height with
|
|
5538
|
-
no descender relief, so tighter starts to look stacked. Phone only —
|
|
5539
|
-
the desktop column keeps the official rhythm. */
|
|
5540
|
-
[class*="_markdown_"] {
|
|
5541
|
-
line-height: 1.65;
|
|
5542
|
-
}
|
|
5543
|
-
/* Paragraph rhythm. Each paragraph is its own markdown block and the
|
|
5544
|
-
separation comes from the flex container's row-gap, not from margins —
|
|
5545
|
-
at the stock 16px the paragraph baseline step was 40.75px against a
|
|
5546
|
-
24.75px line step, i.e. 1.65x, which is what reads as airy. 12px brings
|
|
5547
|
-
the step to ~36.75px: still clearly more than one line so paragraphs
|
|
5548
|
-
never run together, but noticeably denser. Scoped by :has() to
|
|
5549
|
-
containers that actually hold prose blocks, so tool cards and other
|
|
5550
|
-
flex bodies with the same hashed suffix keep their own spacing. */
|
|
5551
|
-
[data-phase] [class$="_body"]:has(> [class*="_markdown_"]) {
|
|
5552
|
-
row-gap: 12px !important;
|
|
5553
|
-
}
|
|
5554
|
-
|
|
5555
|
-
/* Markdown tables: the official _tableScroll_ wrapper is already a
|
|
5556
|
-
horizontal scroll viewport around a width:max-content table, but its
|
|
5557
|
-
cells are capped at min(30vw, 320px) — barely 120px on a phone, which
|
|
5558
|
-
crushes every column into a vertical word-stack. Let columns size to
|
|
5559
|
-
their content instead and read the overflow by scrolling the wrapper.
|
|
5560
|
-
480px (not a vw cap — 85vw was ~320px on a 375px phone and still
|
|
5561
|
-
wrapped mid-length cells) keeps typical cells on one line; only truly
|
|
5562
|
-
prose-heavy cells wrap, instead of dragging the table kilometers
|
|
5563
|
-
wide. */
|
|
5564
|
-
[class*="_tableScroll_"] th,
|
|
5565
|
-
[class*="_tableScroll_"] td {
|
|
5566
|
-
max-width: 480px !important;
|
|
5567
|
-
}
|
|
5568
|
-
|
|
5569
|
-
/* iOS WebKit fallback: upstream's own table { width: max-content } is what
|
|
5570
|
-
keeps the table at its natural width inside the scroll wrapper, but
|
|
5571
|
-
WebKit's max-content support on table boxes is incomplete — on a real
|
|
5572
|
-
iPhone the table fell back to shrink-to-fit and every column got
|
|
5573
|
-
crushed to its min-width (the "ra/nk" vertical-word-stack screenshot,
|
|
5574
|
-
2026-08-18). Redeclare the natural-width intent with both width and
|
|
5575
|
-
min-width so whichever keyword the engine honors wins; the wrapper
|
|
5576
|
-
already scrolls the overflow. */
|
|
5577
|
-
[class*="_tableScroll_"] table {
|
|
5578
|
-
width: max-content !important;
|
|
5579
|
-
min-width: max-content !important;
|
|
5580
|
-
max-width: none !important;
|
|
5581
|
-
}
|
|
5582
|
-
|
|
5583
|
-
/* Turn-tail metadata (2026-08-18): the timing stats ("18:06 · 用时 29秒 ·
|
|
5584
|
-
首 token 0.9秒 · 163 tok/s") are desktop-hover-revealed (official
|
|
5585
|
-
[data-time-hover-root] gate holds them at opacity 0 until :hover /
|
|
5586
|
-
:focus-within) — a phone never hovers, so they were simply invisible;
|
|
5587
|
-
and even ignoring that, the row is nowrap + overflow:hidden (plus this
|
|
5588
|
-
plugin's own legacy ellipsis clamp in layout.css.ts), which clipped the
|
|
5589
|
-
text. On phones: always visible, own full-width line under the action
|
|
5590
|
-
icons, wrapping freely, one type size down. !important + the extra
|
|
5591
|
-
[class$="_actions"] hop out-specifies both upstream and layout.css.ts. */
|
|
5592
|
-
/* Pin the tail row's width chain before allowing wrap: with wrap enabled
|
|
5593
|
-
the row's intrinsic (shrink-to-fit) width collapses to its widest
|
|
5594
|
-
single icon, taking the whole tail down to ~16px. */
|
|
5595
|
-
[data-chat-flow-kind="turn-tail"] [class$="_root"],
|
|
5596
|
-
[data-chat-flow-kind="turn-tail"] [class$="_actions"] {
|
|
5597
|
-
width: 100%;
|
|
5598
|
-
}
|
|
5599
|
-
[data-chat-flow-kind="turn-tail"] [class$="_actions"] {
|
|
5600
|
-
flex-wrap: wrap;
|
|
5601
|
-
row-gap: 2px;
|
|
5602
|
-
}
|
|
5603
|
-
/* Wide companion to the rule below: mirrors the upstream hover-gate
|
|
5604
|
-
selector shape directly, with no assumptions about the tail's DOM
|
|
5605
|
-
structure, so the stats can't stay invisible even if the phone's tail
|
|
5606
|
-
renders differently than desktop. */
|
|
5607
|
-
[data-time-hover-root] [class$="_timeEnd"],
|
|
5608
|
-
[data-time-hover-root] [class$="_timeStart"] {
|
|
5609
|
-
opacity: 1 !important;
|
|
5610
|
-
transition: none !important;
|
|
5611
|
-
}
|
|
5612
|
-
[data-chat-flow-kind="turn-tail"] [class$="_actions"] [class$="_timeEnd"] {
|
|
5613
|
-
opacity: 1 !important;
|
|
5614
|
-
flex-basis: 100% !important;
|
|
5615
|
-
white-space: normal !important;
|
|
5616
|
-
overflow: visible !important;
|
|
5617
|
-
text-overflow: clip !important;
|
|
5618
|
-
padding-left: 0 !important;
|
|
5619
|
-
font-size: 12px;
|
|
5620
|
-
line-height: 18px;
|
|
5621
|
-
}
|
|
5414
|
+
exports.OPEN = exports.THINK = exports.PROCESS_KINDS = void 0;
|
|
5415
|
+
exports.installTurnFold = installTurnFold;
|
|
5416
|
+
const client_config_ts_1 = require("./client-config.js");
|
|
5417
|
+
const locales_ts_1 = require("./locales.js");
|
|
5418
|
+
/** Phone breakpoint — same query every phone-only effect in this plugin uses. */
|
|
5419
|
+
const PHONE_QUERY = '(max-width: 767px)';
|
|
5420
|
+
/** Desktop fold (issue #2) is switched on either way:
|
|
5421
|
+
* - plugin row `config.turnFoldDesktop: true` — the host republishes it at
|
|
5422
|
+
* {@link CLIENT_CONFIG_ROUTE} because the client bundle ships statically
|
|
5423
|
+
* and never sees the row config;
|
|
5424
|
+
* - per-browser override, same convention as the debug flag: a URL param
|
|
5425
|
+
* writes localStorage (`?mobile-nav-turn-fold=1` opts this browser in,
|
|
5426
|
+
* `=0` opts back out). */
|
|
5427
|
+
const DESKTOP_KEY = 'dsh-mobile-nav.turn-fold-desktop';
|
|
5428
|
+
const DESKTOP_PARAM = 'mobile-nav-turn-fold';
|
|
5429
|
+
/** Root attribute the stylesheet keys the width-independent rules on. */
|
|
5430
|
+
const DESKTOP_ATTR = 'data-mnav-desktop-fold';
|
|
5431
|
+
/** Read (and, when the URL param is present, persist) the per-browser opt-in. */
|
|
5432
|
+
function desktopOptIn() {
|
|
5433
|
+
const param = new URLSearchParams(location.search).get(DESKTOP_PARAM);
|
|
5434
|
+
if (param === '1')
|
|
5435
|
+
localStorage.setItem(DESKTOP_KEY, '1');
|
|
5436
|
+
else if (param === '0')
|
|
5437
|
+
localStorage.removeItem(DESKTOP_KEY);
|
|
5438
|
+
return localStorage.getItem(DESKTOP_KEY) === '1';
|
|
5622
5439
|
}
|
|
5623
|
-
|
|
5624
|
-
|
|
5625
|
-
|
|
5626
|
-
|
|
5627
|
-
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
const
|
|
5632
|
-
|
|
5633
|
-
const
|
|
5634
|
-
const
|
|
5635
|
-
|
|
5636
|
-
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
|
|
5440
|
+
/** True when the plugin row asks for desktop fold; false on any failure —
|
|
5441
|
+
* the shared reader in client-config.ts already swallows those. */
|
|
5442
|
+
async function desktopConfigured() {
|
|
5443
|
+
return (await (0, client_config_ts_1.clientConfig)()).turnFoldDesktop;
|
|
5444
|
+
}
|
|
5445
|
+
/** ChatView's flow column (dsh-client-ui-conversation lib/client.js:5512).
|
|
5446
|
+
* Trajectory and every other view render their own tree and never carry this
|
|
5447
|
+
* marker, so scoping here is what keeps this effect Chat-only. */
|
|
5448
|
+
const FLOW = '[data-chat-flow]';
|
|
5449
|
+
/** Marker + selector of the injected summary row. */
|
|
5450
|
+
const CHIP = 'turn-fold';
|
|
5451
|
+
const CHIP_SELECTOR = '[data-mobile-nav="turn-fold"]';
|
|
5452
|
+
/** Whole-row node kinds that are turn *process*, not conversation content.
|
|
5453
|
+
* Keys are ChatNodeSeat's own `data-chat-flow-kind` dispatch values
|
|
5454
|
+
* (registerChatNodeRenderers, lib/client.js:9322): tool calls, injected
|
|
5455
|
+
* context rows, and slash-command rows. Everything else stays visible —
|
|
5456
|
+
* `user`/`steering` (the question), `assistant-step` (the prose, folded
|
|
5457
|
+
* per-block below), `turn-tail` (the footer with its actions), and the
|
|
5458
|
+
* error notices `turn-error` / `turn-max-tokens` / `model-retry`, which are
|
|
5459
|
+
* exactly what a reader must not have to hunt for. */
|
|
5460
|
+
exports.PROCESS_KINDS = ['context', 'tool-call', 'command'];
|
|
5461
|
+
const PROCESS_KIND_SET = new Set(exports.PROCESS_KINDS);
|
|
5462
|
+
/** Node kinds that open a new turn group. */
|
|
5463
|
+
const TURN_HEAD_KINDS = new Set(['user', 'steering']);
|
|
5464
|
+
/** ReasoningRow's own marker (lib/client.js:8966) — the Think disclosure
|
|
5465
|
+
* rendered inside an assistant-step row, beside that step's prose. */
|
|
5466
|
+
exports.THINK = '[data-variant="think"]';
|
|
5467
|
+
/** Attribute the phone stylesheet turns into `display: none`. Still written
|
|
5468
|
+
* per element for the one case the stylesheet cannot express on its own: an
|
|
5469
|
+
* assistant-step row whose entire body is reasoning, which has to go as a
|
|
5470
|
+
* whole row (see {@link foldsWholeRow}). */
|
|
5471
|
+
const FOLD = 'data-mnav-fold';
|
|
5472
|
+
/** Per-item override written while its turn is expanded. */
|
|
5473
|
+
exports.OPEN = 'data-mnav-fold-open';
|
|
5474
|
+
/** Root attribute written for as long as this effect is attached — i.e. for
|
|
5475
|
+
* exactly the widths where the fold is live. The stylesheet keys its
|
|
5476
|
+
* born-folded rules on it, which is what makes a newly inserted tool call
|
|
5477
|
+
* hidden at its FIRST paint instead of after the next rescan (the "row
|
|
5478
|
+
* flashes in, then folds away" report, 2026-08-22); it also keeps the
|
|
5479
|
+
* stylesheet a no-op — content visible — if this effect never runs. */
|
|
5480
|
+
const ACTIVE_ATTR = 'data-mnav-fold-on';
|
|
5640
5481
|
/**
|
|
5641
|
-
*
|
|
5642
|
-
*
|
|
5643
|
-
*
|
|
5644
|
-
*
|
|
5645
|
-
*
|
|
5646
|
-
*
|
|
5647
|
-
* win that tie too), the turn-process fold (turn-fold, S8, which shares no
|
|
5648
|
-
* selector with any of them), and finally the chips row / settings entry /
|
|
5649
|
-
* portal fix (chips, S5) — all appended in this order so their <768px rules
|
|
5650
|
-
* win ties against the shared <=1023px block, then the chat markdown
|
|
5651
|
-
* readability overrides (content, which shares no selector with any of
|
|
5652
|
-
* them). Injected as ONE <style data-plugin> tag — do not reorder.
|
|
5482
|
+
* True when the reasoning rows are the ONLY thing this row renders, so the
|
|
5483
|
+
* whole flow item has to go: the flow column is `display: flex` with
|
|
5484
|
+
* `gap: 16px`, and an emptied-out flex item still claims its gap — the
|
|
5485
|
+
* reader would see the folded steps as a column of blank bands.
|
|
5486
|
+
* Structural test, no class names: every ancestor from the reasoning rows'
|
|
5487
|
+
* shared parent up to the flow item must be an only child.
|
|
5653
5488
|
*/
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
|
|
5489
|
+
function foldsWholeRow(row, thinks) {
|
|
5490
|
+
const first = thinks[0];
|
|
5491
|
+
if (first === undefined)
|
|
5492
|
+
return false;
|
|
5493
|
+
const body = first.parentElement;
|
|
5494
|
+
if (body === null || body.childElementCount !== thinks.length)
|
|
5495
|
+
return false;
|
|
5496
|
+
let node = body;
|
|
5497
|
+
while (node !== row) {
|
|
5498
|
+
const parent = node.parentElement;
|
|
5499
|
+
if (parent === null)
|
|
5500
|
+
return false;
|
|
5501
|
+
if (parent.childElementCount !== 1)
|
|
5502
|
+
return false;
|
|
5503
|
+
node = parent;
|
|
5504
|
+
}
|
|
5505
|
+
return true;
|
|
5506
|
+
}
|
|
5507
|
+
/** The process items one flow row contributes: the row itself, its reasoning
|
|
5508
|
+
* rows, or nothing at all. */
|
|
5509
|
+
function processItems(row) {
|
|
5510
|
+
const kind = row.getAttribute('data-chat-flow-kind');
|
|
5511
|
+
if (kind === null)
|
|
5512
|
+
return [];
|
|
5513
|
+
if (PROCESS_KIND_SET.has(kind))
|
|
5514
|
+
return [row];
|
|
5515
|
+
if (kind !== 'assistant-step')
|
|
5516
|
+
return [];
|
|
5517
|
+
const thinks = [...row.querySelectorAll(exports.THINK)];
|
|
5518
|
+
if (thinks.length === 0)
|
|
5519
|
+
return [];
|
|
5520
|
+
return foldsWholeRow(row, thinks) ? [row] : thinks;
|
|
5521
|
+
}
|
|
5660
5522
|
/**
|
|
5661
|
-
*
|
|
5662
|
-
*
|
|
5663
|
-
*
|
|
5664
|
-
*
|
|
5523
|
+
* The summary row sitting immediately before `anchor`, created if absent.
|
|
5524
|
+
* Idempotent by construction — React re-rendering the flow either leaves our
|
|
5525
|
+
* button where it is (it only ever inserts and removes its OWN nodes) or
|
|
5526
|
+
* drops it, and the next rescan puts it back.
|
|
5665
5527
|
*/
|
|
5666
|
-
function
|
|
5667
|
-
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
|
|
5684
|
-
|
|
5685
|
-
|
|
5686
|
-
|
|
5687
|
-
const
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
5708
|
-
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
|
|
5714
|
-
|
|
5715
|
-
|
|
5716
|
-
|
|
5528
|
+
function chipBefore(anchor) {
|
|
5529
|
+
const previous = anchor.previousElementSibling;
|
|
5530
|
+
if (previous instanceof HTMLElement && previous.dataset['mobileNav'] === CHIP)
|
|
5531
|
+
return previous;
|
|
5532
|
+
const parent = anchor.parentElement;
|
|
5533
|
+
if (parent === null)
|
|
5534
|
+
return null;
|
|
5535
|
+
const chip = document.createElement('button');
|
|
5536
|
+
chip.type = 'button';
|
|
5537
|
+
chip.dataset['mobileNav'] = CHIP;
|
|
5538
|
+
parent.insertBefore(chip, anchor);
|
|
5539
|
+
return chip;
|
|
5540
|
+
}
|
|
5541
|
+
/** Split the flow column into turns: a group runs from one user/steering row
|
|
5542
|
+
* up to (not including) the next. Rows that precede the first user row —
|
|
5543
|
+
* a turn whose question has not been paged in yet — form a leading group of
|
|
5544
|
+
* their own, so their process never becomes unreachable. */
|
|
5545
|
+
function groupsOf(flow) {
|
|
5546
|
+
const groups = [];
|
|
5547
|
+
let current;
|
|
5548
|
+
for (const row of flow.children) {
|
|
5549
|
+
const kind = row.getAttribute('data-chat-flow-kind');
|
|
5550
|
+
if (kind === null)
|
|
5551
|
+
continue;
|
|
5552
|
+
if (current === undefined || TURN_HEAD_KINDS.has(kind)) {
|
|
5553
|
+
current = { key: row.getAttribute('data-chat-flow-key') ?? `pos${groups.length}`, items: [] };
|
|
5554
|
+
groups.push(current);
|
|
5555
|
+
}
|
|
5556
|
+
current.items.push(...processItems(row));
|
|
5557
|
+
}
|
|
5558
|
+
return groups;
|
|
5559
|
+
}
|
|
5560
|
+
/**
|
|
5561
|
+
* S8 — collapse a turn's process into one summary row (< 768px by default;
|
|
5562
|
+
* every width when the plugin row sets `config.turnFoldDesktop` or a browser
|
|
5563
|
+
* opts itself in via `?mobile-nav-turn-fold=1`).
|
|
5564
|
+
*
|
|
5565
|
+
* Route taken: DOM marking, not the `conversation.chat.node` keyed slot.
|
|
5566
|
+
* That slot dispatches on node kind and a second registration at a key
|
|
5567
|
+
* SHADOWS the official renderer (dsh-client-ui-slots index.d.ts:542 — the
|
|
5568
|
+
* cell's lowest-priority live entry renders, there is no wrapping form and
|
|
5569
|
+
* no `children` handle to the shadowed component). Taking `tool-call` would
|
|
5570
|
+
* mean re-implementing every tool's presentation, its `t` seat comes from
|
|
5571
|
+
* the entry's own declared locale namespace and its services from the
|
|
5572
|
+
* registrant's own inject face — neither is reachable from here — and the
|
|
5573
|
+
* registration is global, so the desktop no-op would be gone too.
|
|
5574
|
+
*
|
|
5575
|
+
* The DOM route needs no class names: ChatNodeSeat stamps every row with
|
|
5576
|
+
* `data-chat-flow-kind` / `data-chat-flow-key` (lib/client.js:5228) and
|
|
5577
|
+
* ReasoningRow stamps `data-variant="think"` — turn grouping is pure
|
|
5578
|
+
* structure (the run of rows following one `user` row), never text.
|
|
5579
|
+
*
|
|
5580
|
+
* ponytail: one rAF-coalesced full rescan per DOM mutation batch, O(rows).
|
|
5581
|
+
* If a very long session ever makes streaming feel heavy, narrow the
|
|
5582
|
+
* observer to the flow column's own childList and diff instead.
|
|
5583
|
+
*/
|
|
5584
|
+
function installTurnFold(ctx) {
|
|
5717
5585
|
ctx.effect(() => {
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5586
|
+
const narrow = window.matchMedia(PHONE_QUERY);
|
|
5587
|
+
const t = ctx.locale.bind(locales_ts_1.NS);
|
|
5588
|
+
/** Turns the reader opened. Deliberately not persisted (spec: a reload
|
|
5589
|
+
* returns to the default folded state). */
|
|
5590
|
+
const expanded = new Set();
|
|
5591
|
+
let observer = null;
|
|
5592
|
+
let frame = 0;
|
|
5593
|
+
const label = (chip, count, running) => {
|
|
5594
|
+
const next = running ? t('turnFoldRunning', { count }) : t('turnFold', { count });
|
|
5595
|
+
if (chip.textContent !== next)
|
|
5596
|
+
chip.textContent = next;
|
|
5597
|
+
};
|
|
5598
|
+
const scan = () => {
|
|
5599
|
+
const flow = document.querySelector(FLOW);
|
|
5600
|
+
if (flow === null)
|
|
5727
5601
|
return;
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
if (
|
|
5736
|
-
|
|
5602
|
+
// TurnStatus, ChatView's own running banner — present only while the
|
|
5603
|
+
// last turn is still working (lib/client.js:5548).
|
|
5604
|
+
const running = flow.querySelector(':scope > [role="status"]') !== null;
|
|
5605
|
+
const groups = groupsOf(flow);
|
|
5606
|
+
const live = new Set();
|
|
5607
|
+
const liveItems = new Set();
|
|
5608
|
+
groups.forEach((group, index) => {
|
|
5609
|
+
if (group.items.length === 0)
|
|
5610
|
+
return;
|
|
5611
|
+
const open = expanded.has(group.key);
|
|
5612
|
+
for (const item of group.items) {
|
|
5613
|
+
liveItems.add(item);
|
|
5614
|
+
item.setAttribute(FOLD, '');
|
|
5615
|
+
if (open)
|
|
5616
|
+
item.setAttribute(exports.OPEN, '');
|
|
5617
|
+
else
|
|
5618
|
+
item.removeAttribute(exports.OPEN);
|
|
5619
|
+
}
|
|
5620
|
+
const first = group.items[0];
|
|
5621
|
+
if (first === undefined)
|
|
5622
|
+
return;
|
|
5623
|
+
const anchor = first.closest('[data-chat-flow-key]') ?? first;
|
|
5624
|
+
const chip = chipBefore(anchor);
|
|
5625
|
+
if (chip === null)
|
|
5626
|
+
return;
|
|
5627
|
+
chip.dataset['foldGroup'] = group.key;
|
|
5628
|
+
chip.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
5629
|
+
if (open)
|
|
5630
|
+
chip.dataset['open'] = '';
|
|
5737
5631
|
else
|
|
5738
|
-
|
|
5739
|
-
|
|
5632
|
+
delete chip.dataset['open'];
|
|
5633
|
+
const groupRunning = running && index === groups.length - 1;
|
|
5634
|
+
if (groupRunning)
|
|
5635
|
+
chip.dataset['running'] = '';
|
|
5636
|
+
else
|
|
5637
|
+
delete chip.dataset['running'];
|
|
5638
|
+
label(chip, group.items.length, groupRunning);
|
|
5639
|
+
live.add(group.key);
|
|
5640
|
+
});
|
|
5641
|
+
for (const chip of flow.querySelectorAll(CHIP_SELECTOR)) {
|
|
5642
|
+
const key = chip.dataset['foldGroup'];
|
|
5643
|
+
if (key === undefined || !live.has(key))
|
|
5644
|
+
chip.remove();
|
|
5645
|
+
}
|
|
5646
|
+
// Unmark elements that stopped being process items. Without this, an
|
|
5647
|
+
// assistant-step row folded whole while it streamed ONLY reasoning kept
|
|
5648
|
+
// its row-level fold after prose started arriving in the same DOM node
|
|
5649
|
+
// (React updates the row in place), hiding the streaming reply until a
|
|
5650
|
+
// completion re-render rebuilt the row — the "reply only appears when
|
|
5651
|
+
// the turn finishes" phone bug (2026-08-18).
|
|
5652
|
+
for (const stale of flow.querySelectorAll(`[${FOLD}]`)) {
|
|
5653
|
+
if (liveItems.has(stale))
|
|
5654
|
+
continue;
|
|
5655
|
+
stale.removeAttribute(FOLD);
|
|
5656
|
+
stale.removeAttribute(exports.OPEN);
|
|
5740
5657
|
}
|
|
5741
5658
|
};
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
|
|
5748
|
-
|
|
5749
|
-
return () => { };
|
|
5750
|
-
const errors = [];
|
|
5751
|
-
const onError = (event) => errors.push(`ERR ${event.message.slice(0, 120)}`);
|
|
5752
|
-
const onRejection = (event) => errors.push(`REJ ${String(event.reason).slice(0, 120)}`);
|
|
5753
|
-
window.addEventListener('error', onError);
|
|
5754
|
-
window.addEventListener('unhandledrejection', onRejection);
|
|
5755
|
-
/* Probe element: reads the REAL env(safe-area-inset-*) as computed padding
|
|
5756
|
-
— the --mnav-* vars can be faked by the inset param, this cannot. */
|
|
5757
|
-
const probe = document.createElement('div');
|
|
5758
|
-
probe.style.cssText =
|
|
5759
|
-
'position:fixed;visibility:hidden;pointer-events:none;' +
|
|
5760
|
-
'padding-top:env(safe-area-inset-top,0px);padding-bottom:env(safe-area-inset-bottom,0px)';
|
|
5761
|
-
document.body.appendChild(probe);
|
|
5762
|
-
const badge = document.createElement('div');
|
|
5763
|
-
badge.style.cssText = [
|
|
5764
|
-
/* Sits BELOW the notch on purpose: at the old flat top:40px the badge
|
|
5765
|
-
painted into the status-bar strip and covered the session header,
|
|
5766
|
-
which read on a screenshot as "the header ran under the notch in
|
|
5767
|
-
debug mode" — a measurement artefact that cost a whole round of
|
|
5768
|
-
diagnosis. env() here, not --mnav-sat: the badge must stay put even
|
|
5769
|
-
when the inset param is faking that variable. */
|
|
5770
|
-
'position:fixed', 'top:calc(env(safe-area-inset-top, 0px) + 8px)', 'right:6px', 'z-index:2147483000',
|
|
5771
|
-
'background:rgba(0,0,0,.82)', 'color:#fff', 'font:11px/1.5 ui-monospace,monospace',
|
|
5772
|
-
'padding:8px 10px', 'border-radius:8px', 'max-width:94vw', 'max-height:70vh',
|
|
5773
|
-
/* pointer-events must be auto for the escape hatch below — with `none`
|
|
5774
|
-
the badge could only be dismissed by editing localStorage, which is
|
|
5775
|
-
not reachable from a standalone PWA. touch-action/user-select keep
|
|
5776
|
-
the double tap from being read as zoom or text selection. */
|
|
5777
|
-
'overflow:auto', 'white-space:pre-wrap', 'pointer-events:auto',
|
|
5778
|
-
'touch-action:manipulation', '-webkit-user-select:none', 'user-select:none',
|
|
5779
|
-
].join(';');
|
|
5780
|
-
/* Escape hatch: two taps on the badge within 600ms turn debug off. The
|
|
5781
|
-
enable gesture lives on the home screen, which a user deep inside a
|
|
5782
|
-
session cannot reach without first getting out of the way of the badge
|
|
5783
|
-
— so the off switch belongs on the badge itself. */
|
|
5784
|
-
/* The full read-out is tall enough to cover the session list, and with
|
|
5785
|
-
pointer-events:auto (required for the escape hatch) it swallowed every
|
|
5786
|
-
tap under it — the user could not open a session with debug on. The
|
|
5787
|
-
badge therefore starts COLLAPSED (a one-line pill showing vh, the
|
|
5788
|
-
number that matters for the viewport-shrink bug) and only expands on
|
|
5789
|
-
demand. Single tap toggles collapsed/expanded (deferred 620ms so it
|
|
5790
|
-
can be distinguished from the exit gesture); double tap still exits. */
|
|
5791
|
-
let lastTap = 0;
|
|
5792
|
-
let collapsed = true;
|
|
5793
|
-
let singleTapTimer;
|
|
5794
|
-
const onBadgeTap = (event) => {
|
|
5795
|
-
event.preventDefault();
|
|
5796
|
-
event.stopPropagation();
|
|
5797
|
-
const now = Date.now();
|
|
5798
|
-
if (now - lastTap < 600) {
|
|
5799
|
-
if (singleTapTimer !== undefined)
|
|
5800
|
-
clearTimeout(singleTapTimer);
|
|
5801
|
-
localStorage.removeItem(DEBUG_KEY);
|
|
5802
|
-
const url = new URL(location.href);
|
|
5803
|
-
url.searchParams.delete('mobile-nav-debug');
|
|
5804
|
-
location.replace(url.toString());
|
|
5659
|
+
const schedule = () => {
|
|
5660
|
+
// The locale subscription below lives outside attach/detach, so this
|
|
5661
|
+
// guard is what keeps a late dictionary registration from marking up
|
|
5662
|
+
// the flow at >= 768px (real WebKit regression: chips and fold
|
|
5663
|
+
// attributes appeared at 1280px — invisible, since every rule that
|
|
5664
|
+
// hides anything is inside the phone media query, but still wrong).
|
|
5665
|
+
if (observer === null)
|
|
5805
5666
|
return;
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5667
|
+
if (frame !== 0)
|
|
5668
|
+
return;
|
|
5669
|
+
frame = requestAnimationFrame(() => {
|
|
5670
|
+
frame = 0;
|
|
5671
|
+
scan();
|
|
5672
|
+
// Drop the records our own chip insertions just queued, so a rescan
|
|
5673
|
+
// can never re-trigger itself (the debug-badge feedback-loop freeze).
|
|
5674
|
+
observer?.takeRecords();
|
|
5675
|
+
});
|
|
5812
5676
|
};
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
|
|
5820
|
-
|
|
5821
|
-
if (
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
`W ${innerWidth} x ${innerHeight} dpr ${devicePixelRatio}`,
|
|
5827
|
-
`mq≤1023 ${matchMedia('(max-width: 1023px)').matches} mq≥1024 ${matchMedia('(min-width: 1024px)').matches}`,
|
|
5828
|
-
`css ${q('style[data-plugin-css*="mobile"]')} frame ${!!frame}`,
|
|
5829
|
-
`previewCol ${vis('[data-aionui-preview-col]')} explorerCol ${vis('[data-aionui-explorer-col]')}`,
|
|
5830
|
-
`previewOpen ${frame?.hasAttribute('data-aionui-preview-open') ?? '?'} explorerOpen ${frame?.hasAttribute('data-aionui-explorer-open') ?? '?'} previewFull ${frame?.hasAttribute('data-mobile-preview-full') ?? '?'}`,
|
|
5831
|
-
`header ${vis('[data-phase] header')} h${Math.round(document.querySelector('[data-phase] header')?.getBoundingClientRect().height ?? 0)} composer ${q('textarea')}`,
|
|
5832
|
-
`sat ${getComputedStyle(document.documentElement).getPropertyValue('--mnav-sat').trim() || '?'} sab ${getComputedStyle(document.documentElement).getPropertyValue('--mnav-sab').trim() || '?'}`,
|
|
5833
|
-
(() => {
|
|
5834
|
-
/* Bottom-gap forensics: real env values + who ends where. */
|
|
5835
|
-
const ps = getComputedStyle(probe);
|
|
5836
|
-
const bottomOf = (sel) => {
|
|
5837
|
-
const el = document.querySelector(sel);
|
|
5838
|
-
return el === null ? '—' : String(Math.round(el.getBoundingClientRect().bottom));
|
|
5839
|
-
};
|
|
5840
|
-
/* Composer ledger: bottom/gap-to-viewport for every layer between
|
|
5841
|
-
the textarea and the screen edge, so a "white band under the
|
|
5842
|
-
composer" screenshot says WHICH layer owns it instead of only
|
|
5843
|
-
where the textarea ends. Anchors per AGENTS.md: InputBar root is
|
|
5844
|
-
the padding carrier, _card is the rounded visual box, _composerSeat
|
|
5845
|
-
is the sticky seat, _scrollBody is the real scroll container. */
|
|
5846
|
-
const pair = (sel) => {
|
|
5847
|
-
const el = document.querySelector(sel);
|
|
5848
|
-
if (el === null)
|
|
5849
|
-
return '—';
|
|
5850
|
-
const b = el.getBoundingClientRect().bottom;
|
|
5851
|
-
return `${Math.round(b)}/${Math.round(innerHeight - b)}`;
|
|
5852
|
-
};
|
|
5853
|
-
const barRoot = document.querySelector('[data-slot="conversation.composer.bar"] > [class$="_root"]');
|
|
5854
|
-
/* Top-edge forensics: rect.top of the surfaces that consume
|
|
5855
|
-
--mnav-sat. These separate the two competing readings of the
|
|
5856
|
-
device numbers — a viewport SUNK below the status bar (origin at
|
|
5857
|
-
screen y=59, so a working sat padding puts the header at 118 and
|
|
5858
|
-
the top gap is doubled) versus a viewport anchored at screen y=0
|
|
5859
|
-
that is merely 59px short at the bottom (header at 59, top
|
|
5860
|
-
correct, the missing 59 showing as a band under the composer).
|
|
5861
|
-
innerHeight alone cannot tell them apart; the viewport ORIGIN can,
|
|
5862
|
-
hence screenY / visualViewport offsets / the edge markers. */
|
|
5863
|
-
const topOf = (sel) => {
|
|
5864
|
-
const el = document.querySelector(sel);
|
|
5865
|
-
return el === null ? '—' : String(Math.round(el.getBoundingClientRect().top));
|
|
5866
|
-
};
|
|
5867
|
-
const framePadTop = frame === null ? '—' : getComputedStyle(frame).paddingTop;
|
|
5868
|
-
const vv = window.visualViewport;
|
|
5869
|
-
return [
|
|
5870
|
-
`env sat ${ps.paddingTop} sab ${ps.paddingBottom}`,
|
|
5871
|
-
`screen ${screen.width}x${screen.height} avail ${screen.availWidth}x${screen.availHeight}`,
|
|
5872
|
-
`vh ${innerHeight} vv ${Math.round(vv?.height ?? -1)} outH ${outerHeight}`,
|
|
5873
|
-
`scrXY ${screenX},${screenY} vvOff ${Math.round(vv?.offsetTop ?? -1)} vvPage ${Math.round(vv?.pageTop ?? -1)} vvScale ${vv?.scale ?? '?'}`,
|
|
5874
|
-
/* The ENFORCED state, not a re-derivation: installSunkInset()
|
|
5875
|
-
stamps data-mnav-sunk, and its absence means the override is
|
|
5876
|
-
not running at all (the inset param owns the variables). sabOvr
|
|
5877
|
-
is the receipt — the inline value it wrote, or (env) when the
|
|
5878
|
-
normal compensation is in force. */
|
|
5879
|
-
`sunk? ${document.documentElement.dataset.mnavSunk ?? 'n/a'} standalone ${matchMedia('(display-mode: standalone)').matches} sabOvr ${document.documentElement.style.getPropertyValue('--mnav-sab') || '(env)'}`,
|
|
5880
|
-
`frameTop ${topOf('[data-mobile-nav="frame"]')} padTop ${framePadTop} headerTop ${topOf('[data-phase] header')} homeTopY ${topOf('[data-mobile-nav="home-top"]')}`,
|
|
5881
|
-
`botFrame ${bottomOf('[data-mobile-nav="frame"]')} composer ${bottomOf('[data-phase] textarea')}`,
|
|
5882
|
-
`taB ${pair('[data-phase] textarea')} cardB ${pair('[data-slot="conversation.composer.bar"] [class$="_card"]')}`,
|
|
5883
|
-
`barB ${pair('[data-slot="conversation.composer.bar"] > [class$="_root"]')} seatB ${pair('[class$="_composerSeat"]')} scrollB ${pair('[class$="_scrollBody"]')}`,
|
|
5884
|
-
`sabPad ${barRoot === null ? '—' : getComputedStyle(barRoot).paddingBottom}`,
|
|
5885
|
-
`botList ${bottomOf('[data-mobile-nav="home-list"]')} fab ${bottomOf('[data-mobile-nav="home-fab"]')}`,
|
|
5886
|
-
`cssLen ${document.querySelector('style[data-plugin-css*="mobile"]')?.textContent?.length ?? 0}`,
|
|
5887
|
-
].join('\n');
|
|
5888
|
-
})(),
|
|
5889
|
-
`genui cards ${document.querySelectorAll('[data-genui]').length} panel ${q('[data-genui-panel]')}`,
|
|
5890
|
-
`phase ${document.querySelector('[data-phase]')?.getAttribute('data-phase') ?? '?'}`,
|
|
5891
|
-
`errs ${errors.slice(-5).join(' | ') || 'none'}`,
|
|
5892
|
-
].join('\n');
|
|
5677
|
+
const onClick = (event) => {
|
|
5678
|
+
const target = event.target;
|
|
5679
|
+
if (!(target instanceof Element))
|
|
5680
|
+
return;
|
|
5681
|
+
const chip = target.closest(CHIP_SELECTOR);
|
|
5682
|
+
const key = chip?.dataset['foldGroup'];
|
|
5683
|
+
if (key === undefined)
|
|
5684
|
+
return;
|
|
5685
|
+
if (expanded.has(key))
|
|
5686
|
+
expanded.delete(key);
|
|
5687
|
+
else
|
|
5688
|
+
expanded.add(key);
|
|
5689
|
+
scan();
|
|
5893
5690
|
};
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
|
|
5901
|
-
badge.textContent = next;
|
|
5691
|
+
const clear = () => {
|
|
5692
|
+
for (const chip of document.querySelectorAll(CHIP_SELECTOR))
|
|
5693
|
+
chip.remove();
|
|
5694
|
+
for (const item of document.querySelectorAll(`[${FOLD}]`)) {
|
|
5695
|
+
item.removeAttribute(FOLD);
|
|
5696
|
+
item.removeAttribute(exports.OPEN);
|
|
5697
|
+
}
|
|
5902
5698
|
};
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
if (mutations.every((m) => m.target === badge || badge.contains(m.target) || m.target === probe))
|
|
5699
|
+
const attach = () => {
|
|
5700
|
+
if (observer !== null)
|
|
5906
5701
|
return;
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
|
|
5702
|
+
document.documentElement.setAttribute(ACTIVE_ATTR, '');
|
|
5703
|
+
observer = new MutationObserver(schedule);
|
|
5704
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
5705
|
+
document.addEventListener('click', onClick);
|
|
5706
|
+
scan();
|
|
5707
|
+
};
|
|
5708
|
+
const detach = () => {
|
|
5709
|
+
document.documentElement.removeAttribute(ACTIVE_ATTR);
|
|
5710
|
+
observer?.disconnect();
|
|
5711
|
+
observer = null;
|
|
5712
|
+
document.removeEventListener('click', onClick);
|
|
5713
|
+
if (frame !== 0)
|
|
5714
|
+
cancelAnimationFrame(frame);
|
|
5715
|
+
frame = 0;
|
|
5716
|
+
expanded.clear();
|
|
5717
|
+
clear();
|
|
5718
|
+
};
|
|
5719
|
+
const onChange = (event) => (event.matches ? attach() : detach());
|
|
5720
|
+
let disposed = false;
|
|
5721
|
+
let desktop = false;
|
|
5722
|
+
/** Attach at every width and drop the breakpoint listener — the
|
|
5723
|
+
* stylesheet's html[DESKTOP_ATTR] rules take over from the phone media
|
|
5724
|
+
* query. Idempotent: the config answer and the local opt-in may both
|
|
5725
|
+
* ask for it. */
|
|
5726
|
+
const enableDesktop = () => {
|
|
5727
|
+
if (desktop || disposed)
|
|
5728
|
+
return;
|
|
5729
|
+
desktop = true;
|
|
5730
|
+
document.documentElement.setAttribute(DESKTOP_ATTR, '');
|
|
5731
|
+
narrow.removeEventListener('change', onChange);
|
|
5732
|
+
attach();
|
|
5733
|
+
};
|
|
5734
|
+
if (narrow.matches)
|
|
5735
|
+
attach();
|
|
5736
|
+
narrow.addEventListener('change', onChange);
|
|
5737
|
+
if (desktopOptIn())
|
|
5738
|
+
enableDesktop();
|
|
5739
|
+
// The row config arrives async; a late enable just attaches then. Not
|
|
5740
|
+
// awaited before phone attach — folding must not wait on a round-trip.
|
|
5741
|
+
else
|
|
5742
|
+
void desktopConfigured().then((on) => (on ? enableDesktop() : undefined));
|
|
5743
|
+
// A language switch changes the chip copy of every already-rendered turn.
|
|
5744
|
+
const stopLocale = ctx.locale.subscribe(schedule);
|
|
5912
5745
|
return () => {
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
badge.remove();
|
|
5746
|
+
disposed = true;
|
|
5747
|
+
document.documentElement.removeAttribute(DESKTOP_ATTR);
|
|
5748
|
+
narrow.removeEventListener('change', onChange);
|
|
5749
|
+
stopLocale();
|
|
5750
|
+
detach();
|
|
5919
5751
|
};
|
|
5920
|
-
}, 'dsh-mobile-nav:
|
|
5752
|
+
}, 'dsh-mobile-nav: turn process fold');
|
|
5753
|
+
}
|
|
5754
|
+
};
|
|
5755
|
+
__modules["styles/turn-fold.css.js"] = function (require, module, exports) {
|
|
5756
|
+
"use strict";
|
|
5757
|
+
// turn-fold — the folded turn process and its summary row (S8, 2026-08-17).
|
|
5758
|
+
// Two blocks, both inert unless the fold is actually running:
|
|
5759
|
+
// * BORN_FOLDED, keyed on html[data-mnav-fold-on] — the root attribute
|
|
5760
|
+
// effects/turn-fold.ts holds for as long as it is attached, so the block
|
|
5761
|
+
// is live at exactly the widths the fold is. It hides process rows by
|
|
5762
|
+
// the HOST's own markers, which means a row is folded at its first paint
|
|
5763
|
+
// rather than a frame after the effect notices it (2026-08-22).
|
|
5764
|
+
// * the `rules()` block below, scoped to (max-width: 767px) OR
|
|
5765
|
+
// html[data-mnav-desktop-fold] (the per-browser desktop opt-in, issue
|
|
5766
|
+
// #2), reading markers this plugin writes: data-mnav-fold on a process
|
|
5767
|
+
// element, data-mobile-nav="turn-fold" on the injected summary row.
|
|
5768
|
+
// effects/turn-fold.ts wipes both when it detaches, so a non-opted-in
|
|
5769
|
+
// tablet/desktop is a no-op twice over, by attribute and by scope. It is
|
|
5770
|
+
// authored once and emitted twice so the two scopes can never drift.
|
|
5771
|
+
// Appended last in styles/index.ts; it shares no selector with any other
|
|
5772
|
+
// file, the position just keeps the "phone files come after the shared
|
|
5773
|
+
// <=1023px block" ordering intact.
|
|
5774
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5775
|
+
exports.TURN_FOLD_CSS = void 0;
|
|
5776
|
+
const turn_fold_ts_1 = require("./effects/turn-fold.js");
|
|
5777
|
+
/** Mirror of the root attribute effects/turn-fold.ts holds while attached. */
|
|
5778
|
+
const ACTIVE = 'data-mnav-fold-on';
|
|
5779
|
+
/**
|
|
5780
|
+
* Born folded — the reason a new tool call no longer blinks into view.
|
|
5781
|
+
*
|
|
5782
|
+
* These selectors read the HOST's own markers (ChatNodeSeat's
|
|
5783
|
+
* `data-chat-flow-kind`, ReasoningRow's `data-variant`), which React sets in
|
|
5784
|
+
* the same commit that inserts the row, so the row is already
|
|
5785
|
+
* `display: none` at its first paint. effects/turn-fold.ts still rescans on
|
|
5786
|
+
* the next animation frame, but by then it is only writing the per-turn
|
|
5787
|
+
* {@link OPEN} override and the summary chip — it is no longer what hides
|
|
5788
|
+
* anything, which is what the reader used to see as a one-frame flash
|
|
5789
|
+
* (2026-08-22).
|
|
5790
|
+
*
|
|
5791
|
+
* Keyed on the root attribute the effect holds while it is attached, so this
|
|
5792
|
+
* block is live for exactly the widths the fold is, and hides nothing at all
|
|
5793
|
+
* when the effect never ran.
|
|
5794
|
+
*
|
|
5795
|
+
* One case stays with the effect: an assistant-step row whose whole body is
|
|
5796
|
+
* reasoning has to be hidden as a row (an emptied-out flex item still claims
|
|
5797
|
+
* the flow column's 16px gap), and "this row has nothing but Think in it" is
|
|
5798
|
+
* not a selector. Its rows therefore still fold a frame late — as blank
|
|
5799
|
+
* space, not as content, since the Think rows below are hidden here.
|
|
5800
|
+
*/
|
|
5801
|
+
const BORN_FOLDED = [
|
|
5802
|
+
...turn_fold_ts_1.PROCESS_KINDS.map((kind) => `html[${ACTIVE}] [data-chat-flow] > [data-chat-flow-kind="${kind}"]:not([${turn_fold_ts_1.OPEN}])`),
|
|
5803
|
+
`html[${ACTIVE}] [data-chat-flow] > [data-chat-flow-kind="assistant-step"] ${turn_fold_ts_1.THINK}:not([${turn_fold_ts_1.OPEN}])`,
|
|
5804
|
+
].join(',\n');
|
|
5805
|
+
/** The fold + chip rules, with every selector prefixed by `scope`. */
|
|
5806
|
+
const rules = (scope) => `
|
|
5807
|
+
/* Folded process: tool-call / context / command rows and the Think
|
|
5808
|
+
disclosures inside an assistant step. !important because the official
|
|
5809
|
+
flow item and ReasoningRow both set their own display. */
|
|
5810
|
+
${scope} [data-mnav-fold]:not([data-mnav-fold-open]) {
|
|
5811
|
+
display: none !important;
|
|
5812
|
+
}
|
|
5813
|
+
|
|
5814
|
+
/* Compact chip, sized like the composer's own pills rather than a button:
|
|
5815
|
+
it is a reading affordance in the middle of the message flow, so it has
|
|
5816
|
+
to stay quieter than the content around it. The flow column is a flex
|
|
5817
|
+
column with gap: 16px (ChatView.module.css) — align-self keeps the chip
|
|
5818
|
+
from stretching, and the negative block margin trims that generous gap
|
|
5819
|
+
back to something a 26px row can live in. */
|
|
5820
|
+
${scope} [data-chat-flow] > [data-mobile-nav="turn-fold"] {
|
|
5821
|
+
display: inline-flex;
|
|
5822
|
+
align-self: flex-start;
|
|
5823
|
+
align-items: center;
|
|
5824
|
+
gap: 6px;
|
|
5825
|
+
margin: -4px 0;
|
|
5826
|
+
padding: 0 10px;
|
|
5827
|
+
height: 26px;
|
|
5828
|
+
border: none;
|
|
5829
|
+
border-radius: 13px;
|
|
5830
|
+
background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06));
|
|
5831
|
+
color: var(--dsw-alias-label-secondary, rgba(0, 0, 0, .55));
|
|
5832
|
+
font: inherit;
|
|
5833
|
+
font-size: 12px;
|
|
5834
|
+
line-height: 26px;
|
|
5835
|
+
white-space: nowrap;
|
|
5836
|
+
cursor: pointer;
|
|
5837
|
+
-webkit-tap-highlight-color: transparent;
|
|
5838
|
+
}
|
|
5839
|
+
${scope} [data-chat-flow] > [data-mobile-nav="turn-fold"]:active {
|
|
5840
|
+
background: var(--dsw-alias-interactive-bg-pressed, rgba(0, 0, 0, .1));
|
|
5841
|
+
}
|
|
5842
|
+
|
|
5843
|
+
/* Leading dot: idle turns get a quiet mark, a running turn gets the same
|
|
5844
|
+
business-primary colour the session header's own status dot uses
|
|
5845
|
+
(styles/header.css.ts) plus a breathing pulse. */
|
|
5846
|
+
${scope} [data-chat-flow] > [data-mobile-nav="turn-fold"]::before {
|
|
5847
|
+
content: '';
|
|
5848
|
+
flex: none;
|
|
5849
|
+
width: 6px;
|
|
5850
|
+
height: 6px;
|
|
5851
|
+
border-radius: 50%;
|
|
5852
|
+
background: currentColor;
|
|
5853
|
+
opacity: .45;
|
|
5854
|
+
}
|
|
5855
|
+
${scope} [data-chat-flow] > [data-mobile-nav="turn-fold"][data-running]::before {
|
|
5856
|
+
background: var(--dsw-alias-state-business-primary, #4f6ef7);
|
|
5857
|
+
opacity: 1;
|
|
5858
|
+
animation: dsh-mobile-nav-breathe 1.4s var(--ds-ease-in-out, ease-in-out) infinite;
|
|
5859
|
+
}
|
|
5860
|
+
|
|
5861
|
+
/* Chevron drawn from two borders — no icon injection, no mask image, and
|
|
5862
|
+
it rotates to point up once the turn is open. */
|
|
5863
|
+
${scope} [data-chat-flow] > [data-mobile-nav="turn-fold"]::after {
|
|
5864
|
+
content: '';
|
|
5865
|
+
flex: none;
|
|
5866
|
+
width: 5px;
|
|
5867
|
+
height: 5px;
|
|
5868
|
+
margin-top: -3px;
|
|
5869
|
+
border-right: 1.5px solid currentColor;
|
|
5870
|
+
border-bottom: 1.5px solid currentColor;
|
|
5871
|
+
transform: rotate(45deg);
|
|
5872
|
+
transition: transform .16s var(--ds-ease-out, ease-in-out);
|
|
5873
|
+
}
|
|
5874
|
+
${scope} [data-chat-flow] > [data-mobile-nav="turn-fold"][data-open]::after {
|
|
5875
|
+
margin-top: 3px;
|
|
5876
|
+
transform: rotate(-135deg);
|
|
5877
|
+
}
|
|
5878
|
+
|
|
5879
|
+
@media (prefers-reduced-motion: reduce) {
|
|
5880
|
+
${scope} [data-chat-flow] > [data-mobile-nav="turn-fold"][data-running]::before {
|
|
5881
|
+
animation: none;
|
|
5882
|
+
}
|
|
5883
|
+
${scope} [data-chat-flow] > [data-mobile-nav="turn-fold"]::after {
|
|
5884
|
+
transition: none;
|
|
5885
|
+
}
|
|
5886
|
+
}
|
|
5887
|
+
`;
|
|
5888
|
+
exports.TURN_FOLD_CSS = `/* ---------- turn process fold (< 768px, or desktop opt-in) ---------- */
|
|
5889
|
+
|
|
5890
|
+
/* Process rows and Think disclosures start folded, before any script has
|
|
5891
|
+
looked at them — see BORN_FOLDED above. */
|
|
5892
|
+
${BORN_FOLDED} {
|
|
5893
|
+
display: none !important;
|
|
5894
|
+
}
|
|
5895
|
+
|
|
5896
|
+
/* The summary row never paints outside its active scope, even if a
|
|
5897
|
+
media-query change raced the effect's own cleanup. */
|
|
5898
|
+
[data-mobile-nav="turn-fold"] {
|
|
5899
|
+
display: none;
|
|
5900
|
+
}
|
|
5901
|
+
|
|
5902
|
+
@media (max-width: 767px) {${rules('')}}
|
|
5903
|
+
|
|
5904
|
+
/* Desktop opt-in: the exact same block, keyed on the root attribute
|
|
5905
|
+
effects/turn-fold.ts sets for a browser opted in via
|
|
5906
|
+
?mobile-nav-turn-fold=1 — active at every width. */
|
|
5907
|
+
${rules('html[data-mnav-desktop-fold]')}
|
|
5908
|
+
|
|
5909
|
+
@keyframes dsh-mobile-nav-breathe {
|
|
5910
|
+
0%, 100% { opacity: 1; transform: scale(1); }
|
|
5911
|
+
50% { opacity: .4; transform: scale(.72); }
|
|
5912
|
+
}
|
|
5913
|
+
`;
|
|
5914
|
+
};
|
|
5915
|
+
__modules["styles/chips.css.js"] = function (require, module, exports) {
|
|
5916
|
+
"use strict";
|
|
5917
|
+
// chips — S5 (2026-08-17): the home-screen plugin-entry chips row, its
|
|
5918
|
+
// customize sheet, the settings gear button, and the settings/usage-stats
|
|
5919
|
+
// dialog portal fix. Appended LAST in index.ts (after home.css.ts) so its
|
|
5920
|
+
// `display` overrides win any tie against the phone shell's blanket
|
|
5921
|
+
// `[data-mobile-nav="frame"] > :first-child { display: none }` rule — though
|
|
5922
|
+
// the extra `:has()` clause already outranks it on specificity alone.
|
|
5923
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5924
|
+
exports.CHIPS_CSS = void 0;
|
|
5925
|
+
exports.CHIPS_CSS = `/* ---------- chips row + settings entry (S5) ---------- */
|
|
5926
|
+
|
|
5927
|
+
@media (max-width: 767px) {
|
|
5928
|
+
/* --- settings dialog / usage-stats panel / scheduled-tasks dialog portal fix ---
|
|
5929
|
+
dsh-client-ui-settings-general's SettingsRoot (trigger button +
|
|
5930
|
+
position:fixed;z-index:1000 modal), dsh-usage-stats' sidebar-footer
|
|
5931
|
+
badge (position:fixed panel), and @opendsh/dsh-plugin-scheduled-tasks'
|
|
5932
|
+
"定时任务" trigger (position:fixed;z-index:120 \`.dshst-overlay\`,
|
|
5933
|
+
verified against 0.2.0 — real-device report 2026-08-17: chip click
|
|
5934
|
+
produced no visible window) all mount as PLAIN CHILDREN of the sidebar
|
|
5935
|
+
tree — not a React portal to document.body — inside
|
|
5936
|
+
sidebar.settings / sidebar.footer.action
|
|
5937
|
+
(dsh-client-ui-sidebar's SidebarRoot: footArea > settingsArea /
|
|
5938
|
+
footerActions). home.css.ts sets the sidebar ROOT
|
|
5939
|
+
(\`[data-mobile-nav="frame"] > :first-child\`) to display:none on the
|
|
5940
|
+
phone breakpoint (the app shell replaced the drawer) — and display:none
|
|
5941
|
+
removes the WHOLE subtree from the render tree, including
|
|
5942
|
+
position:fixed descendants, which cannot escape it the way they escape
|
|
5943
|
+
a merely-offscreen or opacity:0 ancestor. A \`.click()\` on the (still
|
|
5944
|
+
DOM-present) hidden trigger still flips its component-local \`open\`
|
|
5945
|
+
state and mounts the aria-modal dialog (synthetic dispatch bypasses
|
|
5946
|
+
hit-testing — S2.1 precedent), but nothing paints without this fix.
|
|
5947
|
+
|
|
5948
|
+
Un-hiding the WHOLE sidebar would surface the session tree / new-session
|
|
5949
|
+
button underneath the dialog, so this is scoped to exactly the sidebar
|
|
5950
|
+
root plus its OWN direct children that are not on the path to
|
|
5951
|
+
footArea: display:contents on the root removes its own box (no
|
|
5952
|
+
duplicate page background/padding, no grid-column reflow) while
|
|
5953
|
+
letting footArea/footerActions/settingsArea resume their OWN official
|
|
5954
|
+
(non-none) display — logoRow / newSession / regionArea are hidden
|
|
5955
|
+
explicitly instead. :has() matches against the DOM tree regardless of
|
|
5956
|
+
the scope element's own computed display, so this reads correctly even
|
|
5957
|
+
though the whole subtree started at display:none (same precedent as
|
|
5958
|
+
every other :has()-gated rule in this stylesheet, e.g. the settings
|
|
5959
|
+
sheet adaptation below, which depends on exactly this becoming
|
|
5960
|
+
visible).
|
|
5961
|
+
|
|
5962
|
+
\`:first-child\` is a layout-column WRAPPER
|
|
5963
|
+
(\`pI_x6G_sidebarCol\`, dsh-client-ui-layout's AppFrame grid cell), not
|
|
5964
|
+
SidebarRoot's own div — SidebarRoot (\`hHd-Xa_root\`) sits one more empty
|
|
5965
|
+
unstyled div below it (measured 2026-08-17: sidebarCol > div (no
|
|
5966
|
+
class) > div.hHd-Xa_root > logoRow/newSession/regionArea/footArea).
|
|
5967
|
+
display:contents on sidebarCol lets that whole chain resume its own
|
|
5968
|
+
official (non-none) display with no other rule needed, but the
|
|
5969
|
+
"hide the other three" selectors below must therefore be DESCENDANT
|
|
5970
|
+
(unscoped depth), not direct-child \`>\`, or they silently match
|
|
5971
|
+
nothing — confirmed live: an earlier \`>\`-only version left
|
|
5972
|
+
logoRow/newSession/regionArea at \`display: flex\`, which is only
|
|
5973
|
+
hidden from view by being fully underneath the usage-stats panel's
|
|
5974
|
+
opaque body, not by this rule. */
|
|
5975
|
+
[data-mobile-nav="frame"] > :first-child:has([aria-modal="true"], [data-usage-stats-panel], .dshst-overlay) {
|
|
5976
|
+
display: contents !important;
|
|
5977
|
+
}
|
|
5978
|
+
[data-mobile-nav="frame"] > :first-child:has([aria-modal="true"], [data-usage-stats-panel], .dshst-overlay) [class$="_logoRow"],
|
|
5979
|
+
[data-mobile-nav="frame"] > :first-child:has([aria-modal="true"], [data-usage-stats-panel], .dshst-overlay) [class$="_newSession"],
|
|
5980
|
+
[data-mobile-nav="frame"] > :first-child:has([aria-modal="true"], [data-usage-stats-panel], .dshst-overlay) [class$="_regionArea"] {
|
|
5981
|
+
display: none !important;
|
|
5982
|
+
}
|
|
5983
|
+
|
|
5984
|
+
/* Home-top settings gear: 44pt touch target, pushed to the row's end by
|
|
5985
|
+
its own auto margin (home-top itself stays a plain flex row). */
|
|
5986
|
+
[data-mobile-nav="home-settings"] {
|
|
5987
|
+
flex: none;
|
|
5988
|
+
display: inline-flex;
|
|
5989
|
+
align-items: center;
|
|
5990
|
+
justify-content: center;
|
|
5991
|
+
width: 44px;
|
|
5992
|
+
height: 44px;
|
|
5993
|
+
margin-left: auto;
|
|
5994
|
+
padding: 0;
|
|
5995
|
+
border: none;
|
|
5996
|
+
background: transparent;
|
|
5997
|
+
color: var(--dsw-alias-label-secondary, inherit);
|
|
5998
|
+
cursor: pointer;
|
|
5999
|
+
touch-action: manipulation;
|
|
6000
|
+
-webkit-tap-highlight-color: transparent;
|
|
6001
|
+
}
|
|
6002
|
+
[data-mobile-nav="home-settings"]:active {
|
|
6003
|
+
background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06));
|
|
6004
|
+
border-radius: 50%;
|
|
6005
|
+
}
|
|
6006
|
+
|
|
6007
|
+
/* Plugin-entry chips row: one horizontally-scrolling line of 34px pills
|
|
6008
|
+
between the workspace title and the session list (prototype spec).
|
|
6009
|
+
Scrollbar hidden on BOTH engines (2026-08-18 user decision): iOS
|
|
6010
|
+
overlay scrollbars never showed anyway, but desktop browsers painted a
|
|
6011
|
+
permanent 2px strip under the pills. This revisits the AGENTS.md
|
|
6012
|
+
"silently cut off the last tab" lesson deliberately — with 4+ chips the
|
|
6013
|
+
row now always overflows mid-chip (a partially visible pill is the
|
|
6014
|
+
scroll affordance), unlike the early incident where the chips fit
|
|
6015
|
+
flush and the hidden overflow was undiscoverable. */
|
|
6016
|
+
[data-mobile-nav="chip-row"] {
|
|
6017
|
+
flex: 0 0 auto;
|
|
6018
|
+
display: flex;
|
|
6019
|
+
align-items: center;
|
|
6020
|
+
gap: 8px;
|
|
6021
|
+
padding: 0 16px 10px;
|
|
6022
|
+
overflow-x: auto;
|
|
6023
|
+
overscroll-behavior: contain;
|
|
6024
|
+
-webkit-overflow-scrolling: touch;
|
|
6025
|
+
scrollbar-width: none;
|
|
6026
|
+
}
|
|
6027
|
+
[data-mobile-nav="chip-row"]::-webkit-scrollbar {
|
|
6028
|
+
display: none;
|
|
6029
|
+
}
|
|
6030
|
+
[data-mobile-nav="chip"],
|
|
6031
|
+
[data-mobile-nav="chip-more"] {
|
|
6032
|
+
flex: none;
|
|
6033
|
+
display: inline-flex;
|
|
6034
|
+
align-items: center;
|
|
6035
|
+
gap: 6px;
|
|
6036
|
+
height: 34px;
|
|
6037
|
+
padding: 0 14px;
|
|
6038
|
+
border: none;
|
|
6039
|
+
border-radius: 17px;
|
|
6040
|
+
/* Same surface as the list rows: the home page is bg-base since the
|
|
6041
|
+
2026-08-20 swap (see home.css.ts), so a bg-base chip would disappear
|
|
6042
|
+
into it. */
|
|
6043
|
+
background: var(--dsw-specific-sidebar-fill, #f5f5f5);
|
|
6044
|
+
color: var(--dsw-alias-label-primary, inherit);
|
|
6045
|
+
font-family: inherit;
|
|
6046
|
+
font-size: 13px;
|
|
6047
|
+
font-weight: 500;
|
|
6048
|
+
white-space: nowrap;
|
|
6049
|
+
cursor: pointer;
|
|
6050
|
+
touch-action: manipulation;
|
|
6051
|
+
-webkit-tap-highlight-color: transparent;
|
|
6052
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, .06);
|
|
6053
|
+
}
|
|
6054
|
+
[data-mobile-nav="chip-more"] {
|
|
6055
|
+
padding: 0 9px;
|
|
6056
|
+
}
|
|
6057
|
+
[data-mobile-nav="chip"]:active,
|
|
6058
|
+
[data-mobile-nav="chip-more"]:active {
|
|
6059
|
+
background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06));
|
|
6060
|
+
}
|
|
6061
|
+
|
|
6062
|
+
/* Harvested chip icon (S5.1): the cloned <svg> keeps whatever width/height
|
|
6063
|
+
the source plugin gave it (18px for the one verified live example) —
|
|
6064
|
+
force it down to the same 16px box every hand-registered chip icon
|
|
6065
|
+
uses, CSS wins over the presentational SVG attributes. */
|
|
6066
|
+
[data-mobile-nav="chip-harvest-icon"] {
|
|
6067
|
+
display: inline-flex;
|
|
6068
|
+
flex: none;
|
|
6069
|
+
}
|
|
6070
|
+
[data-mobile-nav="chip-harvest-icon"] svg {
|
|
6071
|
+
display: block;
|
|
6072
|
+
width: 16px;
|
|
6073
|
+
height: 16px;
|
|
6074
|
+
}
|
|
6075
|
+
|
|
6076
|
+
/* Customize sheet: toggle rows inside the SAME home-sheet-layer/mask/sheet
|
|
6077
|
+
chrome the workspace switcher declares in home.css.ts — S6's
|
|
6078
|
+
drag-to-close and mask-click-close bind to
|
|
6079
|
+
\`[data-mobile-nav="home-sheet"]\` generically (effects/gestures.ts), so
|
|
6080
|
+
this second use of the marker needs no new gesture wiring. */
|
|
6081
|
+
[data-mobile-nav="chip-toggle-row"] {
|
|
6082
|
+
display: flex;
|
|
6083
|
+
align-items: center;
|
|
6084
|
+
gap: 10px;
|
|
6085
|
+
width: 100%;
|
|
6086
|
+
/* Real-device fix: without border-box this row's used width is
|
|
6087
|
+
100% (of the sheet's own already-padded content box) PLUS the
|
|
6088
|
+
12px+12px padding below, overflowing the sheet by 24px and
|
|
6089
|
+
shoving the flex:none toggle off its right edge (2026-08-17
|
|
6090
|
+
report: "开关跑到屏幕外"). home-sheet-item next door in
|
|
6091
|
+
home.css.ts has the identical width:100%+padding shape and the
|
|
6092
|
+
same latent bug, just with no trailing element to visibly clip. */
|
|
6093
|
+
box-sizing: border-box;
|
|
6094
|
+
min-height: 48px;
|
|
6095
|
+
padding: 0 12px;
|
|
6096
|
+
color: inherit;
|
|
6097
|
+
}
|
|
6098
|
+
[data-mobile-nav="chip-toggle-label"] {
|
|
6099
|
+
flex: 1 1 auto;
|
|
6100
|
+
min-width: 0;
|
|
6101
|
+
font-size: 15px;
|
|
6102
|
+
overflow: hidden;
|
|
6103
|
+
text-overflow: ellipsis;
|
|
6104
|
+
white-space: nowrap;
|
|
6105
|
+
}
|
|
6106
|
+
[data-mobile-nav="chip-toggle"] {
|
|
6107
|
+
flex: none;
|
|
6108
|
+
position: relative;
|
|
6109
|
+
width: 42px;
|
|
6110
|
+
height: 26px;
|
|
6111
|
+
border: none;
|
|
6112
|
+
border-radius: 13px;
|
|
6113
|
+
background: var(--dsw-alias-border-l2, rgba(0, 0, 0, .16));
|
|
6114
|
+
cursor: pointer;
|
|
6115
|
+
touch-action: manipulation;
|
|
6116
|
+
-webkit-tap-highlight-color: transparent;
|
|
6117
|
+
transition: background .15s var(--ds-ease-in-out, ease-in-out);
|
|
6118
|
+
}
|
|
6119
|
+
[data-mobile-nav="chip-toggle"]::after {
|
|
6120
|
+
content: '';
|
|
6121
|
+
position: absolute;
|
|
6122
|
+
top: 2px;
|
|
6123
|
+
left: 2px;
|
|
6124
|
+
width: 22px;
|
|
6125
|
+
height: 22px;
|
|
6126
|
+
border-radius: 50%;
|
|
6127
|
+
background: #ffffff;
|
|
6128
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
|
|
6129
|
+
transition: transform .15s var(--ds-ease-in-out, ease-in-out);
|
|
6130
|
+
}
|
|
6131
|
+
[data-mobile-nav="chip-toggle"][aria-checked="true"] {
|
|
6132
|
+
background: var(--dsw-alias-button-primary-fill, #1a1a1a);
|
|
6133
|
+
}
|
|
6134
|
+
[data-mobile-nav="chip-toggle"][aria-checked="true"]::after {
|
|
6135
|
+
transform: translateX(16px);
|
|
6136
|
+
}
|
|
6137
|
+
@media (prefers-reduced-motion: reduce) {
|
|
6138
|
+
[data-mobile-nav="chip-toggle"],
|
|
6139
|
+
[data-mobile-nav="chip-toggle"]::after {
|
|
6140
|
+
transition: none !important;
|
|
6141
|
+
}
|
|
6142
|
+
}
|
|
5921
6143
|
}
|
|
6144
|
+
`;
|
|
5922
6145
|
};
|
|
5923
|
-
__modules["
|
|
6146
|
+
__modules["styles/content.css.js"] = function (require, module, exports) {
|
|
5924
6147
|
"use strict";
|
|
6148
|
+
// content — chat markdown readability on phones (2026-08-18). Shares no
|
|
6149
|
+
// selector with any other file; appended last in styles/index.ts.
|
|
5925
6150
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5926
|
-
exports.
|
|
5927
|
-
exports.
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
6151
|
+
exports.CONTENT_CSS = void 0;
|
|
6152
|
+
exports.CONTENT_CSS = `/* ---------- chat markdown content (< 768px) ---------- */
|
|
6153
|
+
|
|
6154
|
+
@media (max-width: 767px) {
|
|
6155
|
+
/* Inline code: upstream renders it display:inline-flex — an atomic inline
|
|
6156
|
+
box that can never wrap across lines, so one long code span walks off
|
|
6157
|
+
the right edge of the screen. Plain inline keeps the background pill
|
|
6158
|
+
per line fragment and lets long tokens break anywhere. Deliberately
|
|
6159
|
+
unscoped (any :not(pre) > code, not just the _markdown_ module):
|
|
6160
|
+
inline code also renders outside that container (user bubbles,
|
|
6161
|
+
reasoning rows), and on a phone there is no width at which an
|
|
6162
|
+
unwrappable inline box is right. !important beats the upstream
|
|
6163
|
+
display:inline-flex on specificity ties. */
|
|
6164
|
+
:not(pre) > code {
|
|
6165
|
+
display: inline !important;
|
|
6166
|
+
overflow-wrap: anywhere;
|
|
6167
|
+
/* iOS WebKit: overflow-wrap alone still failed to break long tokens in
|
|
6168
|
+
inline code on a real iPhone (screenshot-verified 2026-08-18, li >
|
|
6169
|
+
code path case) while desktop Chromium broke them fine. break-all is
|
|
6170
|
+
the everywhere-supported hammer; acceptable for code pills. */
|
|
6171
|
+
word-break: break-all;
|
|
6172
|
+
}
|
|
6173
|
+
/* Belt-and-suspenders for engines that compute soft-wrap opportunities
|
|
6174
|
+
from the block container rather than the inline itself (historical
|
|
6175
|
+
WebKit behavior): inherit anywhere through the whole transcript so the
|
|
6176
|
+
block side agrees. Prose is untouched until something would overflow;
|
|
6177
|
+
the table is already pinned wide by min-width: max-content below. */
|
|
6178
|
+
[data-chat-flow],
|
|
6179
|
+
[class*="_markdown_"] {
|
|
6180
|
+
overflow-wrap: anywhere;
|
|
6181
|
+
}
|
|
6182
|
+
|
|
6183
|
+
/* Transcript density (2026-08-20): the desktop prose rhythm is 28px of
|
|
6184
|
+
leading on 15px text — a 1.87 ratio that reads generously on a wide
|
|
6185
|
+
column but wastes a phone screen, where the same paragraph is three
|
|
6186
|
+
times as many lines. Unitless 1.65 so nested blocks (code, quotes,
|
|
6187
|
+
lists) recompute from their OWN font-size instead of inheriting a fixed
|
|
6188
|
+
28px that is far too loose for 12-13px chrome text. 1.65 is the floor I
|
|
6189
|
+
am willing to go to for mixed CJK/Latin: the glyphs are full-height with
|
|
6190
|
+
no descender relief, so tighter starts to look stacked. Phone only —
|
|
6191
|
+
the desktop column keeps the official rhythm. */
|
|
6192
|
+
[class*="_markdown_"] {
|
|
6193
|
+
line-height: 1.65;
|
|
6194
|
+
}
|
|
6195
|
+
/* Paragraph rhythm. Each paragraph is its own markdown block and the
|
|
6196
|
+
separation comes from the flex container's row-gap, not from margins —
|
|
6197
|
+
at the stock 16px the paragraph baseline step was 40.75px against a
|
|
6198
|
+
24.75px line step, i.e. 1.65x, which is what reads as airy. 12px brings
|
|
6199
|
+
the step to ~36.75px: still clearly more than one line so paragraphs
|
|
6200
|
+
never run together, but noticeably denser. Scoped by :has() to
|
|
6201
|
+
containers that actually hold prose blocks, so tool cards and other
|
|
6202
|
+
flex bodies with the same hashed suffix keep their own spacing. */
|
|
6203
|
+
[data-phase] [class$="_body"]:has(> [class*="_markdown_"]) {
|
|
6204
|
+
row-gap: 12px !important;
|
|
6205
|
+
}
|
|
6206
|
+
|
|
6207
|
+
/* Markdown tables: the official _tableScroll_ wrapper is already a
|
|
6208
|
+
horizontal scroll viewport around a width:max-content table, but its
|
|
6209
|
+
cells are capped at min(30vw, 320px) — barely 120px on a phone, which
|
|
6210
|
+
crushes every column into a vertical word-stack. Let columns size to
|
|
6211
|
+
their content instead and read the overflow by scrolling the wrapper.
|
|
6212
|
+
480px (not a vw cap — 85vw was ~320px on a 375px phone and still
|
|
6213
|
+
wrapped mid-length cells) keeps typical cells on one line; only truly
|
|
6214
|
+
prose-heavy cells wrap, instead of dragging the table kilometers
|
|
6215
|
+
wide. */
|
|
6216
|
+
[class*="_tableScroll_"] th,
|
|
6217
|
+
[class*="_tableScroll_"] td {
|
|
6218
|
+
max-width: 480px !important;
|
|
6219
|
+
}
|
|
6220
|
+
|
|
6221
|
+
/* iOS WebKit fallback: upstream's own table { width: max-content } is what
|
|
6222
|
+
keeps the table at its natural width inside the scroll wrapper, but
|
|
6223
|
+
WebKit's max-content support on table boxes is incomplete — on a real
|
|
6224
|
+
iPhone the table fell back to shrink-to-fit and every column got
|
|
6225
|
+
crushed to its min-width (the "ra/nk" vertical-word-stack screenshot,
|
|
6226
|
+
2026-08-18). Redeclare the natural-width intent with both width and
|
|
6227
|
+
min-width so whichever keyword the engine honors wins; the wrapper
|
|
6228
|
+
already scrolls the overflow. */
|
|
6229
|
+
[class*="_tableScroll_"] table {
|
|
6230
|
+
width: max-content !important;
|
|
6231
|
+
min-width: max-content !important;
|
|
6232
|
+
max-width: none !important;
|
|
6233
|
+
}
|
|
6234
|
+
|
|
6235
|
+
/* Turn-tail metadata (2026-08-18): the timing stats ("18:06 · 用时 29秒 ·
|
|
6236
|
+
首 token 0.9秒 · 163 tok/s") are desktop-hover-revealed (official
|
|
6237
|
+
[data-time-hover-root] gate holds them at opacity 0 until :hover /
|
|
6238
|
+
:focus-within) — a phone never hovers, so they were simply invisible;
|
|
6239
|
+
and even ignoring that, the row is nowrap + overflow:hidden (plus this
|
|
6240
|
+
plugin's own legacy ellipsis clamp in layout.css.ts), which clipped the
|
|
6241
|
+
text. On phones: always visible, own full-width line under the action
|
|
6242
|
+
icons, wrapping freely, one type size down. !important + the extra
|
|
6243
|
+
[class$="_actions"] hop out-specifies both upstream and layout.css.ts. */
|
|
6244
|
+
/* Pin the tail row's width chain before allowing wrap: with wrap enabled
|
|
6245
|
+
the row's intrinsic (shrink-to-fit) width collapses to its widest
|
|
6246
|
+
single icon, taking the whole tail down to ~16px. */
|
|
6247
|
+
[data-chat-flow-kind="turn-tail"] [class$="_root"],
|
|
6248
|
+
[data-chat-flow-kind="turn-tail"] [class$="_actions"] {
|
|
6249
|
+
width: 100%;
|
|
6250
|
+
}
|
|
6251
|
+
[data-chat-flow-kind="turn-tail"] [class$="_actions"] {
|
|
6252
|
+
flex-wrap: wrap;
|
|
6253
|
+
row-gap: 2px;
|
|
6254
|
+
}
|
|
6255
|
+
/* Wide companion to the rule below: mirrors the upstream hover-gate
|
|
6256
|
+
selector shape directly, with no assumptions about the tail's DOM
|
|
6257
|
+
structure, so the stats can't stay invisible even if the phone's tail
|
|
6258
|
+
renders differently than desktop. */
|
|
6259
|
+
[data-time-hover-root] [class$="_timeEnd"],
|
|
6260
|
+
[data-time-hover-root] [class$="_timeStart"] {
|
|
6261
|
+
opacity: 1 !important;
|
|
6262
|
+
transition: none !important;
|
|
6263
|
+
}
|
|
6264
|
+
[data-chat-flow-kind="turn-tail"] [class$="_actions"] [class$="_timeEnd"] {
|
|
6265
|
+
opacity: 1 !important;
|
|
6266
|
+
flex-basis: 100% !important;
|
|
6267
|
+
white-space: normal !important;
|
|
6268
|
+
overflow: visible !important;
|
|
6269
|
+
text-overflow: clip !important;
|
|
6270
|
+
padding-left: 0 !important;
|
|
6271
|
+
font-size: 12px;
|
|
6272
|
+
line-height: 18px;
|
|
6273
|
+
}
|
|
5985
6274
|
}
|
|
6275
|
+
`;
|
|
6276
|
+
};
|
|
6277
|
+
__modules["styles/index.js"] = function (require, module, exports) {
|
|
6278
|
+
"use strict";
|
|
6279
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6280
|
+
exports.MOBILE_CSS = void 0;
|
|
6281
|
+
const base_css_ts_1 = require("./styles/base.css.js");
|
|
6282
|
+
const layout_css_ts_1 = require("./styles/layout.css.js");
|
|
6283
|
+
const compat_css_ts_1 = require("./styles/compat.css.js");
|
|
6284
|
+
const misc_css_ts_1 = require("./styles/misc.css.js");
|
|
6285
|
+
const home_css_ts_1 = require("./styles/home.css.js");
|
|
6286
|
+
const header_css_ts_1 = require("./styles/header.css.js");
|
|
6287
|
+
const composer_css_ts_1 = require("./styles/composer.css.js");
|
|
6288
|
+
const info_css_ts_1 = require("./styles/info.css.js");
|
|
6289
|
+
const turn_fold_css_ts_1 = require("./styles/turn-fold.css.js");
|
|
6290
|
+
const chips_css_ts_1 = require("./styles/chips.css.js");
|
|
6291
|
+
const content_css_ts_1 = require("./styles/content.css.js");
|
|
5986
6292
|
/**
|
|
5987
|
-
*
|
|
5988
|
-
*
|
|
5989
|
-
*
|
|
5990
|
-
*
|
|
5991
|
-
*
|
|
5992
|
-
*
|
|
5993
|
-
*
|
|
5994
|
-
*
|
|
5995
|
-
*
|
|
5996
|
-
*
|
|
5997
|
-
*
|
|
5998
|
-
*
|
|
5999
|
-
* indicator lives in the dead strip below the page. installSunkInset() acts
|
|
6000
|
-
* on that, and only on --mnav-sab.
|
|
6001
|
-
*
|
|
6002
|
-
* The `envTop > 0` guard is what keeps every other standalone install out:
|
|
6003
|
-
* landscape iPhone (top inset 0, the notch moves to left/right), iPad, and
|
|
6004
|
-
* Android — where standalone also loses status-bar height off innerHeight but
|
|
6005
|
-
* reports env top 0 — all fall through to false. Kept pure so the boundaries
|
|
6006
|
-
* are checkable off-device: scripts/check-sunk-viewport.mjs.
|
|
6293
|
+
* All mobile styles, concatenated in the exact order of the original
|
|
6294
|
+
* single-file stylesheet (base → layout → compat → misc, where misc keeps
|
|
6295
|
+
* composer → tablet → desktop), followed by the phone app shell (home), the
|
|
6296
|
+
* session-header reflow (header), the composer reflow (composer), and the
|
|
6297
|
+
* session-info sheet (info, which must come last of all: it re-shows a
|
|
6298
|
+
* header.utilities child header.css.ts hides by default, so its rule has to
|
|
6299
|
+
* win that tie too), the turn-process fold (turn-fold, S8, which shares no
|
|
6300
|
+
* selector with any of them), and finally the chips row / settings entry /
|
|
6301
|
+
* portal fix (chips, S5) — all appended in this order so their <768px rules
|
|
6302
|
+
* win ties against the shared <=1023px block, then the chat markdown
|
|
6303
|
+
* readability overrides (content, which shares no selector with any of
|
|
6304
|
+
* them). Injected as ONE <style data-plugin> tag — do not reorder.
|
|
6007
6305
|
*/
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
}
|
|
6306
|
+
exports.MOBILE_CSS = [base_css_ts_1.BASE_CSS, layout_css_ts_1.LAYOUT_CSS, compat_css_ts_1.COMPAT_CSS, misc_css_ts_1.MISC_CSS, home_css_ts_1.HOME_CSS, header_css_ts_1.HEADER_CSS, composer_css_ts_1.COMPOSER_CSS, info_css_ts_1.INFO_CSS, turn_fold_css_ts_1.TURN_FOLD_CSS, chips_css_ts_1.CHIPS_CSS, content_css_ts_1.CONTENT_CSS].join('\n');
|
|
6307
|
+
};
|
|
6308
|
+
__modules["debug.js"] = function (require, module, exports) {
|
|
6309
|
+
"use strict";
|
|
6310
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6311
|
+
exports.installDebugBadge = installDebugBadge;
|
|
6013
6312
|
/**
|
|
6014
|
-
*
|
|
6015
|
-
*
|
|
6016
|
-
*
|
|
6017
|
-
*
|
|
6018
|
-
* a home-screen (standalone) PWA, the layout viewport permanently loses the
|
|
6019
|
-
* status-bar height for the rest of the app session. innerHeight,
|
|
6020
|
-
* visualViewport.height and 100dvh all report the shrunken value together, so
|
|
6021
|
-
* nothing inside the page can see anything wrong — the page simply ends early
|
|
6022
|
-
* and the strip below it is system background that no CSS can reach. Reported
|
|
6023
|
-
* as 932 -> 873 on an iPhone Pro Max; the user's device reads 852 -> 793.
|
|
6024
|
-
* Both are exactly one status bar. See
|
|
6025
|
-
* https://dev.to/cederhook/fixing-the-ios-standalone-pwa-keyboard-bug-that-shrinks-your-viewport-for-good-63d
|
|
6026
|
-
*
|
|
6027
|
-
* This is why the reported symptom is asymmetric and why the earlier
|
|
6028
|
-
* "double-counted top inset" reading was wrong: the top edge is genuinely
|
|
6029
|
-
* correct (env top 59 is paid once, the viewport starts at screen y=0), the
|
|
6030
|
-
* bottom is simply 59px short. It also explains a chat client being hit
|
|
6031
|
-
* hardest — the trigger is typing, which happens on the first message.
|
|
6032
|
-
*
|
|
6033
|
-
* The only known cure is to make WebKit re-measure: drop a full-viewport
|
|
6034
|
-
* element out of the box tree and force a synchronous reflow. Two departures
|
|
6035
|
-
* from the published recipe:
|
|
6036
|
-
* - scrollTop is saved and restored around the toggle. Un-boxing an ancestor
|
|
6037
|
-
* resets every descendant scroller to 0, which in a chat client means the
|
|
6038
|
-
* conversation jumps to its first message. Both happen inside one task, so
|
|
6039
|
-
* no frame is ever painted in between and nothing flickers.
|
|
6040
|
-
* - the baseline is the tallest innerHeight this session has actually seen,
|
|
6041
|
-
* not the screen height. On any device where the viewport is legitimately
|
|
6042
|
-
* short, the baseline equals the current height and this never fires.
|
|
6043
|
-
*
|
|
6044
|
-
* Standalone-gated, so a browser tab (and every desktop, CDP included) is a
|
|
6045
|
-
* strict no-op — which is also why the fix cannot be regression-tested here
|
|
6046
|
-
* and has to be confirmed on the device.
|
|
6313
|
+
* Debug badge — ?mobile-nav-debug=1
|
|
6314
|
+
* Renders a live state overlay (URL, viewport, media queries, shell chrome,
|
|
6315
|
+
* aionui columns, genui cards, captured errors) so a phone-side repro can be
|
|
6316
|
+
* diagnosed without guessing. No-op unless the query param is present.
|
|
6047
6317
|
*/
|
|
6048
|
-
function
|
|
6318
|
+
function installDebugBadge(ctx) {
|
|
6319
|
+
/**
|
|
6320
|
+
* Fake safe-area inset — ?mobile-nav-inset=54 or ?mobile-nav-inset=54,34
|
|
6321
|
+
* env(safe-area-inset-*) is hard 0 on every desktop browser (CDP included),
|
|
6322
|
+
* so notch bugs are invisible until a real phone loads the page — the S2.1
|
|
6323
|
+
* hotfix exists because of exactly that. Overriding --mnav-sat / --mnav-sab
|
|
6324
|
+
* (see styles/base.css.ts) on the root element reproduces a notch anywhere.
|
|
6325
|
+
* No param = the variables keep their env() values = zero behaviour change.
|
|
6326
|
+
*
|
|
6327
|
+
* One value fakes the TOP inset only (the historic behaviour — every S1-S3
|
|
6328
|
+
* verification recipe passes `=54`, and they must keep meaning what they
|
|
6329
|
+
* did). A second, comma-separated value fakes the BOTTOM inset too:
|
|
6330
|
+
* `=54,34` is the iPhone notch + home-indicator pair. The bottom half was
|
|
6331
|
+
* added in S4.1 because the home-bar padding it guards (composer clearance
|
|
6332
|
+
* over the indicator) is otherwise untestable off-device for exactly the
|
|
6333
|
+
* same reason the top half exists.
|
|
6334
|
+
*/
|
|
6049
6335
|
ctx.effect(() => {
|
|
6050
|
-
const
|
|
6051
|
-
|
|
6052
|
-
/* iOS-only on purpose. The ceiling below leans on screen.height being the
|
|
6053
|
-
height the viewport SHOULD have, which holds for an iPhone standalone
|
|
6054
|
-
app under viewport-fit=cover and does not hold on Android, where the
|
|
6055
|
-
navigation bar makes screen.height permanently larger than any viewport
|
|
6056
|
-
and every keyboard dismiss would trigger a pointless reflow. */
|
|
6057
|
-
if (!standalone || !/iPad|iPhone|iPod/.test(navigator.userAgent))
|
|
6336
|
+
const raw = new URLSearchParams(location.search).get('mobile-nav-inset');
|
|
6337
|
+
if (raw === null)
|
|
6058
6338
|
return () => { };
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
|
|
6065
|
-
const
|
|
6066
|
-
|
|
6067
|
-
|
|
6068
|
-
|
|
6069
|
-
|
|
6070
|
-
|
|
6071
|
-
standalone regression family) retrying forever buys nothing. The two
|
|
6072
|
-
budgets are separate on purpose — the cold-start attempts would
|
|
6073
|
-
otherwise spend the whole allowance before the user ever types, and the
|
|
6074
|
-
keyboard shrink IS reflow-curable even on a device whose cold start is
|
|
6075
|
-
not. */
|
|
6076
|
-
const strikes = { boot: 0, blur: 0 };
|
|
6077
|
-
const heal = (kind) => {
|
|
6078
|
-
if (strikes[kind] >= 3 || ceiling() - window.innerHeight <= 4)
|
|
6079
|
-
return;
|
|
6080
|
-
const frame = document.querySelector('[data-mobile-nav="frame"]');
|
|
6081
|
-
if (frame === null)
|
|
6082
|
-
return;
|
|
6083
|
-
const scroller = document.querySelector('[class$="_scrollBody"]');
|
|
6084
|
-
const scrollTop = scroller?.scrollTop ?? 0;
|
|
6085
|
-
const previous = frame.style.display;
|
|
6086
|
-
frame.style.display = 'none';
|
|
6087
|
-
void frame.offsetHeight; /* forces the synchronous reflow that re-measures */
|
|
6088
|
-
frame.style.display = previous;
|
|
6089
|
-
if (scroller !== null)
|
|
6090
|
-
scroller.scrollTop = scrollTop;
|
|
6091
|
-
strikes[kind] = ceiling() - window.innerHeight > 4 ? strikes[kind] + 1 : 0;
|
|
6092
|
-
};
|
|
6093
|
-
/* Blur is when the keyboard starts closing; the viewport settles a beat
|
|
6094
|
-
later. Two attempts because the dismiss animation length is not
|
|
6095
|
-
specified anywhere, and a heal that runs too early is a silent no-op. */
|
|
6096
|
-
const timers = [];
|
|
6097
|
-
const onFocusOut = (event) => {
|
|
6098
|
-
if (!(event.target instanceof HTMLElement))
|
|
6099
|
-
return;
|
|
6100
|
-
if (event.target.matches('textarea, input') !== true)
|
|
6101
|
-
return;
|
|
6102
|
-
timers.push(window.setTimeout(() => heal('blur'), 300), window.setTimeout(() => heal('blur'), 900));
|
|
6339
|
+
const [topRaw, bottomRaw] = raw.split(',');
|
|
6340
|
+
const top = Number(topRaw);
|
|
6341
|
+
if (!Number.isFinite(top))
|
|
6342
|
+
return () => { };
|
|
6343
|
+
const root = document.documentElement;
|
|
6344
|
+
root.style.setProperty('--mnav-sat', `${top}px`);
|
|
6345
|
+
const bottom = bottomRaw === undefined ? Number.NaN : Number(bottomRaw);
|
|
6346
|
+
if (Number.isFinite(bottom))
|
|
6347
|
+
root.style.setProperty('--mnav-sab', `${bottom}px`);
|
|
6348
|
+
return () => {
|
|
6349
|
+
root.style.removeProperty('--mnav-sat');
|
|
6350
|
+
root.style.removeProperty('--mnav-sab');
|
|
6103
6351
|
};
|
|
6104
|
-
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6352
|
+
}, 'dsh-mobile-nav: fake safe-area inset');
|
|
6353
|
+
const DEBUG_KEY = 'dsh-mobile-nav.debug';
|
|
6354
|
+
/**
|
|
6355
|
+
* No-URL toggle — 5 quick taps on the home screen's top bar.
|
|
6356
|
+
* A standalone PWA has no address bar, and the paired-device cookie lives
|
|
6357
|
+
* in the PWA's own jar (Safari hits the pairing wall instead), so the
|
|
6358
|
+
* ?mobile-nav-debug=1 param is unreachable exactly where the badge is
|
|
6359
|
+
* needed most.
|
|
6360
|
+
*
|
|
6361
|
+
* The target is deliberately NOT [data-mobile-nav="ws-switch"] any more:
|
|
6362
|
+
* that button opens the workspace sheet on the FIRST tap, so taps 2..5
|
|
6363
|
+
* land on the sheet backdrop, `closest()` misses, and the counter never
|
|
6364
|
+
* reaches five — the original binding only ever fired by luck, and it left
|
|
6365
|
+
* a user stuck inside debug mode with no way back out. The logo img has no
|
|
6366
|
+
* click handler at all; the home-top container minus the switch button is
|
|
6367
|
+
* the fallback for when the icon 404s and the img is not rendered.
|
|
6368
|
+
*/
|
|
6369
|
+
ctx.effect(() => {
|
|
6370
|
+
let taps = 0;
|
|
6371
|
+
let firstTap = 0;
|
|
6372
|
+
const onTap = (event) => {
|
|
6373
|
+
if (!(event.target instanceof Element))
|
|
6115
6374
|
return;
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6375
|
+
const inTopBar = event.target.closest('[data-mobile-nav="home-logo"]') !== null
|
|
6376
|
+
|| (event.target.closest('[data-mobile-nav="home-top"]') !== null
|
|
6377
|
+
&& event.target.closest('[data-mobile-nav="ws-switch"]') === null);
|
|
6378
|
+
if (!inTopBar)
|
|
6379
|
+
return;
|
|
6380
|
+
const now = Date.now();
|
|
6381
|
+
if (now - firstTap > 2500) {
|
|
6382
|
+
taps = 0;
|
|
6383
|
+
firstTap = now;
|
|
6384
|
+
}
|
|
6385
|
+
taps += 1;
|
|
6386
|
+
if (taps >= 5) {
|
|
6387
|
+
if (localStorage.getItem(DEBUG_KEY) === '1')
|
|
6388
|
+
localStorage.removeItem(DEBUG_KEY);
|
|
6389
|
+
else
|
|
6390
|
+
localStorage.setItem(DEBUG_KEY, '1');
|
|
6391
|
+
location.reload();
|
|
6392
|
+
}
|
|
6127
6393
|
};
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
|
|
6131
|
-
* Sunk-viewport compensation (S1.3, 2026-08-17) — zeroes --mnav-sab ONLY.
|
|
6132
|
-
*
|
|
6133
|
-
* When isViewportSunkBelowStatusBar() holds, the layout viewport is 59px
|
|
6134
|
-
* shorter than the screen while env() still reports the full pair of insets.
|
|
6135
|
-
* The bottom one is then a lie with a cost: the home indicator sits in the
|
|
6136
|
-
* dead strip BELOW the page, so the 34px we reserve for it is 34px of blank
|
|
6137
|
-
* page stacked on top of an already-blank system band. Zeroing it does not
|
|
6138
|
-
* fix the band (nothing in the page can) but stops us widening it.
|
|
6139
|
-
*
|
|
6140
|
-
* --mnav-sat is deliberately left alone: the top edge is measured correct on
|
|
6141
|
-
* the device (header sits right under the notch), and the S1.2 round already
|
|
6142
|
-
* burned a cycle on the theory that it was double-counted.
|
|
6143
|
-
*
|
|
6144
|
-
* Reversible by design. The predicate is re-evaluated on every resize, so a
|
|
6145
|
-
* viewport that comes back to full height — the heal above winning, or Apple
|
|
6146
|
-
* shipping the fix — drops the override and the normal env() compensation
|
|
6147
|
-
* returns without a reload. `data-mnav-sunk` on <html> is what the debug
|
|
6148
|
-
* badge reads, so the badge reports the state that is actually in force
|
|
6149
|
-
* rather than re-deriving it.
|
|
6150
|
-
*/
|
|
6151
|
-
function installSunkInset(ctx) {
|
|
6394
|
+
document.addEventListener('click', onTap, true);
|
|
6395
|
+
return () => document.removeEventListener('click', onTap, true);
|
|
6396
|
+
}, 'dsh-mobile-nav: debug badge tap toggle');
|
|
6152
6397
|
ctx.effect(() => {
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
so the badge shows "n/a" instead of a state nobody is enforcing. */
|
|
6157
|
-
if (new URLSearchParams(location.search).has('mobile-nav-inset'))
|
|
6398
|
+
const enabled = new URLSearchParams(location.search).has('mobile-nav-debug')
|
|
6399
|
+
|| localStorage.getItem('dsh-mobile-nav.debug') === '1';
|
|
6400
|
+
if (!enabled)
|
|
6158
6401
|
return () => { };
|
|
6159
|
-
const
|
|
6160
|
-
|
|
6402
|
+
const errors = [];
|
|
6403
|
+
const onError = (event) => errors.push(`ERR ${event.message.slice(0, 120)}`);
|
|
6404
|
+
const onRejection = (event) => errors.push(`REJ ${String(event.reason).slice(0, 120)}`);
|
|
6405
|
+
window.addEventListener('error', onError);
|
|
6406
|
+
window.addEventListener('unhandledrejection', onRejection);
|
|
6407
|
+
/* Probe element: reads the REAL env(safe-area-inset-*) as computed padding
|
|
6408
|
+
— the --mnav-* vars can be faked by the inset param, this cannot. */
|
|
6161
6409
|
const probe = document.createElement('div');
|
|
6162
6410
|
probe.style.cssText =
|
|
6163
|
-
'position:fixed;
|
|
6164
|
-
'padding-top:env(safe-area-inset-top,0px)';
|
|
6411
|
+
'position:fixed;visibility:hidden;pointer-events:none;' +
|
|
6412
|
+
'padding-top:env(safe-area-inset-top,0px);padding-bottom:env(safe-area-inset-bottom,0px)';
|
|
6165
6413
|
document.body.appendChild(probe);
|
|
6166
|
-
const
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
|
|
6178
|
-
|
|
6414
|
+
const badge = document.createElement('div');
|
|
6415
|
+
badge.style.cssText = [
|
|
6416
|
+
/* Sits BELOW the notch on purpose: at the old flat top:40px the badge
|
|
6417
|
+
painted into the status-bar strip and covered the session header,
|
|
6418
|
+
which read on a screenshot as "the header ran under the notch in
|
|
6419
|
+
debug mode" — a measurement artefact that cost a whole round of
|
|
6420
|
+
diagnosis. env() here, not --mnav-sat: the badge must stay put even
|
|
6421
|
+
when the inset param is faking that variable. */
|
|
6422
|
+
'position:fixed', 'top:calc(env(safe-area-inset-top, 0px) + 8px)', 'right:6px', 'z-index:2147483000',
|
|
6423
|
+
'background:rgba(0,0,0,.82)', 'color:#fff', 'font:11px/1.5 ui-monospace,monospace',
|
|
6424
|
+
'padding:8px 10px', 'border-radius:8px', 'max-width:94vw', 'max-height:70vh',
|
|
6425
|
+
/* pointer-events must be auto for the escape hatch below — with `none`
|
|
6426
|
+
the badge could only be dismissed by editing localStorage, which is
|
|
6427
|
+
not reachable from a standalone PWA. touch-action/user-select keep
|
|
6428
|
+
the double tap from being read as zoom or text selection. */
|
|
6429
|
+
'overflow:auto', 'white-space:pre-wrap', 'pointer-events:auto',
|
|
6430
|
+
'touch-action:manipulation', '-webkit-user-select:none', 'user-select:none',
|
|
6431
|
+
].join(';');
|
|
6432
|
+
/* Escape hatch: two taps on the badge within 600ms turn debug off. The
|
|
6433
|
+
enable gesture lives on the home screen, which a user deep inside a
|
|
6434
|
+
session cannot reach without first getting out of the way of the badge
|
|
6435
|
+
— so the off switch belongs on the badge itself. */
|
|
6436
|
+
/* The full read-out is tall enough to cover the session list, and with
|
|
6437
|
+
pointer-events:auto (required for the escape hatch) it swallowed every
|
|
6438
|
+
tap under it — the user could not open a session with debug on. The
|
|
6439
|
+
badge therefore starts COLLAPSED (a one-line pill showing vh, the
|
|
6440
|
+
number that matters for the viewport-shrink bug) and only expands on
|
|
6441
|
+
demand. Single tap toggles collapsed/expanded (deferred 620ms so it
|
|
6442
|
+
can be distinguished from the exit gesture); double tap still exits. */
|
|
6443
|
+
let lastTap = 0;
|
|
6444
|
+
let collapsed = true;
|
|
6445
|
+
let singleTapTimer;
|
|
6446
|
+
const onBadgeTap = (event) => {
|
|
6447
|
+
event.preventDefault();
|
|
6448
|
+
event.stopPropagation();
|
|
6449
|
+
const now = Date.now();
|
|
6450
|
+
if (now - lastTap < 600) {
|
|
6451
|
+
if (singleTapTimer !== undefined)
|
|
6452
|
+
clearTimeout(singleTapTimer);
|
|
6453
|
+
localStorage.removeItem(DEBUG_KEY);
|
|
6454
|
+
const url = new URL(location.href);
|
|
6455
|
+
url.searchParams.delete('mobile-nav-debug');
|
|
6456
|
+
location.replace(url.toString());
|
|
6457
|
+
return;
|
|
6458
|
+
}
|
|
6459
|
+
lastTap = now;
|
|
6460
|
+
singleTapTimer = setTimeout(() => {
|
|
6461
|
+
collapsed = !collapsed;
|
|
6462
|
+
paint();
|
|
6463
|
+
}, 620);
|
|
6179
6464
|
};
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
6465
|
+
badge.addEventListener('click', onBadgeTap);
|
|
6466
|
+
const read = () => {
|
|
6467
|
+
const q = (sel) => !!document.querySelector(sel);
|
|
6468
|
+
const vis = (sel) => {
|
|
6469
|
+
const el = document.querySelector(sel);
|
|
6470
|
+
return el === null ? 'absent' : getComputedStyle(el).visibility;
|
|
6471
|
+
};
|
|
6472
|
+
const frame = document.querySelector('[data-mobile-nav="frame"]');
|
|
6473
|
+
if (collapsed)
|
|
6474
|
+
return `DBG vh ${innerHeight}/${screen.height} ▸ 单击展开 双击关闭`;
|
|
6475
|
+
return [
|
|
6476
|
+
'▾ 单击收起 · 双击关闭 debug',
|
|
6477
|
+
`URL ${location.pathname}${location.search}`,
|
|
6478
|
+
`W ${innerWidth} x ${innerHeight} dpr ${devicePixelRatio}`,
|
|
6479
|
+
`mq≤1023 ${matchMedia('(max-width: 1023px)').matches} mq≥1024 ${matchMedia('(min-width: 1024px)').matches}`,
|
|
6480
|
+
`css ${q('style[data-plugin-css*="mobile"]')} frame ${!!frame}`,
|
|
6481
|
+
`previewCol ${vis('[data-aionui-preview-col]')} explorerCol ${vis('[data-aionui-explorer-col]')}`,
|
|
6482
|
+
`previewOpen ${frame?.hasAttribute('data-aionui-preview-open') ?? '?'} explorerOpen ${frame?.hasAttribute('data-aionui-explorer-open') ?? '?'} previewFull ${frame?.hasAttribute('data-mobile-preview-full') ?? '?'}`,
|
|
6483
|
+
`header ${vis('[data-phase] header')} h${Math.round(document.querySelector('[data-phase] header')?.getBoundingClientRect().height ?? 0)} composer ${q('textarea')}`,
|
|
6484
|
+
`sat ${getComputedStyle(document.documentElement).getPropertyValue('--mnav-sat').trim() || '?'} sab ${getComputedStyle(document.documentElement).getPropertyValue('--mnav-sab').trim() || '?'}`,
|
|
6485
|
+
(() => {
|
|
6486
|
+
/* Bottom-gap forensics: real env values + who ends where. */
|
|
6487
|
+
const ps = getComputedStyle(probe);
|
|
6488
|
+
const bottomOf = (sel) => {
|
|
6489
|
+
const el = document.querySelector(sel);
|
|
6490
|
+
return el === null ? '—' : String(Math.round(el.getBoundingClientRect().bottom));
|
|
6491
|
+
};
|
|
6492
|
+
/* Composer ledger: bottom/gap-to-viewport for every layer between
|
|
6493
|
+
the textarea and the screen edge, so a "white band under the
|
|
6494
|
+
composer" screenshot says WHICH layer owns it instead of only
|
|
6495
|
+
where the textarea ends. Anchors per AGENTS.md: InputBar root is
|
|
6496
|
+
the padding carrier, _card is the rounded visual box, _composerSeat
|
|
6497
|
+
is the sticky seat, _scrollBody is the real scroll container. */
|
|
6498
|
+
const pair = (sel) => {
|
|
6499
|
+
const el = document.querySelector(sel);
|
|
6500
|
+
if (el === null)
|
|
6501
|
+
return '—';
|
|
6502
|
+
const b = el.getBoundingClientRect().bottom;
|
|
6503
|
+
return `${Math.round(b)}/${Math.round(innerHeight - b)}`;
|
|
6504
|
+
};
|
|
6505
|
+
const barRoot = document.querySelector('[data-slot="conversation.composer.bar"] > [class$="_root"]');
|
|
6506
|
+
/* Top-edge forensics: rect.top of the surfaces that consume
|
|
6507
|
+
--mnav-sat. These separate the two competing readings of the
|
|
6508
|
+
device numbers — a viewport SUNK below the status bar (origin at
|
|
6509
|
+
screen y=59, so a working sat padding puts the header at 118 and
|
|
6510
|
+
the top gap is doubled) versus a viewport anchored at screen y=0
|
|
6511
|
+
that is merely 59px short at the bottom (header at 59, top
|
|
6512
|
+
correct, the missing 59 showing as a band under the composer).
|
|
6513
|
+
innerHeight alone cannot tell them apart; the viewport ORIGIN can,
|
|
6514
|
+
hence screenY / visualViewport offsets / the edge markers. */
|
|
6515
|
+
const topOf = (sel) => {
|
|
6516
|
+
const el = document.querySelector(sel);
|
|
6517
|
+
return el === null ? '—' : String(Math.round(el.getBoundingClientRect().top));
|
|
6518
|
+
};
|
|
6519
|
+
const framePadTop = frame === null ? '—' : getComputedStyle(frame).paddingTop;
|
|
6520
|
+
const vv = window.visualViewport;
|
|
6521
|
+
return [
|
|
6522
|
+
`env sat ${ps.paddingTop} sab ${ps.paddingBottom}`,
|
|
6523
|
+
`screen ${screen.width}x${screen.height} avail ${screen.availWidth}x${screen.availHeight}`,
|
|
6524
|
+
`vh ${innerHeight} vv ${Math.round(vv?.height ?? -1)} outH ${outerHeight}`,
|
|
6525
|
+
`scrXY ${screenX},${screenY} vvOff ${Math.round(vv?.offsetTop ?? -1)} vvPage ${Math.round(vv?.pageTop ?? -1)} vvScale ${vv?.scale ?? '?'}`,
|
|
6526
|
+
/* The ENFORCED state, not a re-derivation: installSunkInset()
|
|
6527
|
+
stamps data-mnav-sunk, and its absence means the override is
|
|
6528
|
+
not running at all (the inset param owns the variables). sabOvr
|
|
6529
|
+
is the receipt — the inline value it wrote, or (env) when the
|
|
6530
|
+
normal compensation is in force. */
|
|
6531
|
+
`sunk? ${document.documentElement.dataset.mnavSunk ?? 'n/a'} standalone ${matchMedia('(display-mode: standalone)').matches} sabOvr ${document.documentElement.style.getPropertyValue('--mnav-sab') || '(env)'}`,
|
|
6532
|
+
`frameTop ${topOf('[data-mobile-nav="frame"]')} padTop ${framePadTop} headerTop ${topOf('[data-phase] header')} homeTopY ${topOf('[data-mobile-nav="home-top"]')}`,
|
|
6533
|
+
`botFrame ${bottomOf('[data-mobile-nav="frame"]')} composer ${bottomOf('[data-phase] textarea')}`,
|
|
6534
|
+
`taB ${pair('[data-phase] textarea')} cardB ${pair('[data-slot="conversation.composer.bar"] [class$="_card"]')}`,
|
|
6535
|
+
`barB ${pair('[data-slot="conversation.composer.bar"] > [class$="_root"]')} seatB ${pair('[class$="_composerSeat"]')} scrollB ${pair('[class$="_scrollBody"]')}`,
|
|
6536
|
+
`sabPad ${barRoot === null ? '—' : getComputedStyle(barRoot).paddingBottom}`,
|
|
6537
|
+
`botList ${bottomOf('[data-mobile-nav="home-list"]')} fab ${bottomOf('[data-mobile-nav="home-fab"]')}`,
|
|
6538
|
+
`cssLen ${document.querySelector('style[data-plugin-css*="mobile"]')?.textContent?.length ?? 0}`,
|
|
6539
|
+
].join('\n');
|
|
6540
|
+
})(),
|
|
6541
|
+
`genui cards ${document.querySelectorAll('[data-genui]').length} panel ${q('[data-genui-panel]')}`,
|
|
6542
|
+
`phase ${document.querySelector('[data-phase]')?.getAttribute('data-phase') ?? '?'}`,
|
|
6543
|
+
`errs ${errors.slice(-5).join(' | ') || 'none'}`,
|
|
6544
|
+
].join('\n');
|
|
6545
|
+
};
|
|
6546
|
+
/* Feedback-loop guards: painting the badge mutates the body subtree the
|
|
6547
|
+
observer watches — without these two checks the observer retriggers
|
|
6548
|
+
itself forever and freezes the page (the badge was rarely actually
|
|
6549
|
+
enabled before, which is how this survived). */
|
|
6550
|
+
const paint = () => {
|
|
6551
|
+
const next = read();
|
|
6552
|
+
if (next !== badge.textContent)
|
|
6553
|
+
badge.textContent = next;
|
|
6554
|
+
};
|
|
6555
|
+
paint();
|
|
6556
|
+
const observer = new MutationObserver((mutations) => {
|
|
6557
|
+
if (mutations.every((m) => m.target === badge || badge.contains(m.target) || m.target === probe))
|
|
6558
|
+
return;
|
|
6559
|
+
paint();
|
|
6560
|
+
});
|
|
6561
|
+
observer.observe(document.body, { childList: true, subtree: true, attributes: true });
|
|
6562
|
+
const timer = setInterval(paint, 1500);
|
|
6563
|
+
document.body.appendChild(badge);
|
|
6186
6564
|
return () => {
|
|
6187
|
-
window.removeEventListener('
|
|
6188
|
-
window.removeEventListener('
|
|
6565
|
+
window.removeEventListener('error', onError);
|
|
6566
|
+
window.removeEventListener('unhandledrejection', onRejection);
|
|
6567
|
+
observer.disconnect();
|
|
6568
|
+
clearInterval(timer);
|
|
6189
6569
|
probe.remove();
|
|
6190
|
-
|
|
6191
|
-
delete root.dataset.mnavSunk;
|
|
6570
|
+
badge.remove();
|
|
6192
6571
|
};
|
|
6193
|
-
}, 'dsh-mobile-nav:
|
|
6572
|
+
}, 'dsh-mobile-nav: debug badge');
|
|
6194
6573
|
}
|
|
6195
6574
|
};
|
|
6196
|
-
__modules["effects/
|
|
6575
|
+
__modules["effects/phone-chrome.js"] = function (require, module, exports) {
|
|
6197
6576
|
"use strict";
|
|
6198
6577
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6199
|
-
exports.
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
|
|
6203
|
-
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
|
|
6578
|
+
exports.installPhoneChrome = installPhoneChrome;
|
|
6579
|
+
exports.isViewportSunkBelowStatusBar = isViewportSunkBelowStatusBar;
|
|
6580
|
+
exports.installViewportHeal = installViewportHeal;
|
|
6581
|
+
exports.installSunkInset = installSunkInset;
|
|
6582
|
+
/**
|
|
6583
|
+
* Phone chrome: KEEP the system status bar (no fullscreen) and make it
|
|
6584
|
+
* blend into the page. On narrow screens:
|
|
6585
|
+
* - The viewport meta gains viewport-fit=cover, so env(safe-area-inset-top)
|
|
6586
|
+
* is the real status-bar / notch height and the stylesheet can push every
|
|
6587
|
+
* surface below it (off notched phones, or in a browser tab where the
|
|
6588
|
+
* layout viewport already sits below the status bar, the inset is 0 and
|
|
6589
|
+
* nothing shifts).
|
|
6590
|
+
* - A theme-color meta tracks the shell background (the official theme is
|
|
6591
|
+
* toggled by body[data-ds-dark-theme], which flips --dsw-alias-bg-base):
|
|
6592
|
+
* Android then paints the status bar / URL bar with the page's own base
|
|
6593
|
+
* color, so the status bar reads as part of the UI instead of a foreign
|
|
6594
|
+
* strip. The drawer paints the same strip on iOS / notch displays.
|
|
6595
|
+
* - gesturestart is suppressed as the legacy-iOS fallback for double-tap
|
|
6596
|
+
* zoom; modern browsers are covered by the stylesheet's
|
|
6597
|
+
* touch-action: manipulation (which keeps pan and pinch zoom).
|
|
6598
|
+
*/
|
|
6599
|
+
function installPhoneChrome(ctx) {
|
|
6212
6600
|
ctx.effect(() => {
|
|
6213
|
-
// Arm on the CURRENT width and re-arm on every width change: the guard
|
|
6214
|
-
// used to run once at apply time, so a wide→narrow transition (desktop
|
|
6215
|
-
// resize, tablet split view) left the markers dead and the explorer /
|
|
6216
|
-
// preview sheets could neither open nor close properly.
|
|
6217
6601
|
const narrow = window.matchMedia('(max-width: 1023px)');
|
|
6218
|
-
|
|
6219
|
-
const
|
|
6220
|
-
|
|
6221
|
-
|
|
6222
|
-
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
|
|
6229
|
-
document.querySelector('[data-mobile-nav="frame"]')?.removeAttribute('data-aionui-explorer-open');
|
|
6230
|
-
};
|
|
6231
|
-
document.addEventListener('click', onChevronClick, true);
|
|
6232
|
-
cleanup = () => document.removeEventListener('click', onChevronClick, true);
|
|
6602
|
+
const viewport = document.querySelector('meta[name="viewport"]');
|
|
6603
|
+
const originalViewport = viewport?.content ?? '';
|
|
6604
|
+
const themeMeta = document.createElement('meta');
|
|
6605
|
+
themeMeta.name = 'theme-color';
|
|
6606
|
+
const bodyBg = () => getComputedStyle(document.body).backgroundColor;
|
|
6607
|
+
const sync = () => {
|
|
6608
|
+
if (viewport !== null)
|
|
6609
|
+
viewport.content = 'width=device-width, initial-scale=1, viewport-fit=cover';
|
|
6610
|
+
themeMeta.content = bodyBg();
|
|
6611
|
+
if (themeMeta.parentElement === null)
|
|
6612
|
+
document.head.appendChild(themeMeta);
|
|
6233
6613
|
};
|
|
6234
|
-
|
|
6235
|
-
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
cleanup?.();
|
|
6614
|
+
const restore = () => {
|
|
6615
|
+
if (viewport !== null)
|
|
6616
|
+
viewport.content = originalViewport;
|
|
6617
|
+
themeMeta.remove();
|
|
6239
6618
|
};
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
// tabs in localStorage and restores them on load, which would pop the
|
|
6243
|
-
// preview sheet over the fresh UI after a reload. Gate it like the
|
|
6244
|
-
// explorer: the stylesheet keeps the column hidden unless the frame
|
|
6245
|
-
// carries `data-aionui-preview-open`; this effect sets that marker when
|
|
6246
|
-
// the user actually taps a file row in the explorer sheet, and clears it
|
|
6247
|
-
// whenever the suite hides the column again (collapse chevron / tab
|
|
6248
|
-
// close), so a restored-but-unwanted sheet never appears.
|
|
6249
|
-
ctx.effect(() => {
|
|
6250
|
-
// Arm on the CURRENT width and re-arm on every width change (see the
|
|
6251
|
-
// explorer marker effect for why).
|
|
6252
|
-
const narrow = window.matchMedia('(max-width: 1023px)');
|
|
6253
|
-
let cleanup;
|
|
6254
|
-
const install = () => {
|
|
6255
|
-
cleanup?.();
|
|
6256
|
-
if (!narrow.matches) {
|
|
6257
|
-
cleanup = undefined;
|
|
6258
|
-
return;
|
|
6259
|
-
}
|
|
6260
|
-
const frame = () => document.querySelector('[data-mobile-nav="frame"]');
|
|
6261
|
-
// Closing the preview sheet also drops the fullscreen marker, so the
|
|
6262
|
-
// next preview starts in the sheet layout again.
|
|
6263
|
-
const closePreview = () => {
|
|
6264
|
-
frame()?.removeAttribute('data-aionui-preview-open');
|
|
6265
|
-
frame()?.removeAttribute('data-mobile-preview-full');
|
|
6266
|
-
};
|
|
6267
|
-
const onTap = (event) => {
|
|
6268
|
-
const target = event.target;
|
|
6269
|
-
if (target === null)
|
|
6270
|
-
return;
|
|
6271
|
-
const row = target.closest('[data-aionui-explorer-col] [class*="_treeRow"]');
|
|
6272
|
-
if (row === null)
|
|
6273
|
-
return;
|
|
6274
|
-
// Only FILE rows open the preview sheet. Directory rows toggle
|
|
6275
|
-
// expansion and must not pop the (possibly stale, restored-from-
|
|
6276
|
-
// localStorage) preview tab over the tree. Substring matching:
|
|
6277
|
-
// the suite's hashed classes carry a hash prefix, so the exact-token
|
|
6278
|
-
// `class~=` form never matches and the trailing `$=` form misses
|
|
6279
|
-
// selected rows (`_treeRowSelected`) and open arrows
|
|
6280
|
-
// (`_treeArrowOpen`) — regressions of issue #8. The arrow gate must
|
|
6281
|
-
// additionally exclude the leaf marker: FILE rows render a
|
|
6282
|
-
// `_treeArrowEmpty` span whose class still contains the `_treeArrow`
|
|
6283
|
-
// substring, so a bare substring match would treat every row as a
|
|
6284
|
-
// directory and no preview would ever open.
|
|
6285
|
-
if (row.querySelector('[class*="_treeArrow"]:not([class*="_treeArrowEmpty"])') !== null)
|
|
6286
|
-
return;
|
|
6287
|
-
frame()?.setAttribute('data-aionui-preview-open', '');
|
|
6288
|
-
};
|
|
6289
|
-
// The preview sheet's own collapse button (the two inward arrows in the
|
|
6290
|
-
// tab bar) closes the AionUI store, but on mobile the suite's layout sync
|
|
6291
|
-
// can be skipped while its shell-track mirror is not ready yet — in that
|
|
6292
|
-
// case the inline visibility never flips to hidden and the visibility
|
|
6293
|
-
// watcher below would never clear our marker. Clear it directly on the
|
|
6294
|
-
// button click so the sheet always closes regardless of the suite's sync.
|
|
6295
|
-
const onCollapse = (event) => {
|
|
6296
|
-
const target = event.target;
|
|
6297
|
-
if (target === null)
|
|
6298
|
-
return;
|
|
6299
|
-
if (target.closest('[data-aionui-preview-col] [class$="_panelCollapse"]') !== null) {
|
|
6300
|
-
closePreview();
|
|
6301
|
-
}
|
|
6302
|
-
};
|
|
6303
|
-
const sync = () => {
|
|
6304
|
-
const pv = document.querySelector('[data-aionui-preview-col]');
|
|
6305
|
-
if (pv === null)
|
|
6306
|
-
return;
|
|
6307
|
-
// Read the suite's inline visibility, not the computed value: while the
|
|
6308
|
-
// `data-aionui-preview-open` marker is present our stylesheet forces the
|
|
6309
|
-
// sheet visible with !important, so getComputedStyle() would never report
|
|
6310
|
-
// hidden and the marker would never be cleared.
|
|
6311
|
-
if (pv.style.visibility === 'hidden')
|
|
6312
|
-
closePreview();
|
|
6313
|
-
};
|
|
6314
|
-
document.addEventListener('click', onTap, true);
|
|
6315
|
-
document.addEventListener('click', onCollapse, true);
|
|
6316
|
-
const observer = new MutationObserver(sync);
|
|
6317
|
-
observer.observe(document.body, { attributes: true, subtree: true, attributeFilter: ['style'] });
|
|
6619
|
+
const onGestureStart = (event) => event.preventDefault();
|
|
6620
|
+
if (narrow.matches)
|
|
6318
6621
|
sync();
|
|
6319
|
-
|
|
6320
|
-
|
|
6321
|
-
|
|
6322
|
-
|
|
6323
|
-
|
|
6324
|
-
};
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
return () => {
|
|
6328
|
-
narrow.removeEventListener('change', install);
|
|
6329
|
-
cleanup?.();
|
|
6330
|
-
};
|
|
6331
|
-
}, 'dsh-mobile-nav: preview sheet open marker');
|
|
6332
|
-
// The dsh-web-ui explorer / preview columns toggle via `visibility`
|
|
6333
|
-
// (their inline style), which never restarts a CSS animation — so the
|
|
6334
|
-
// sheets would only animate on first mount. Replay the rise animation
|
|
6335
|
-
// with the Web Animations API each time a column turns visible, then
|
|
6336
|
-
// leave the resting state to the stylesheet.
|
|
6337
|
-
ctx.effect(() => {
|
|
6338
|
-
// Arm on the CURRENT width and re-arm on every width change (see the
|
|
6339
|
-
// explorer marker effect for why).
|
|
6340
|
-
const narrow = window.matchMedia('(max-width: 1023px)');
|
|
6341
|
-
let cleanup;
|
|
6342
|
-
const install = () => {
|
|
6343
|
-
cleanup?.();
|
|
6344
|
-
if (!narrow.matches) {
|
|
6345
|
-
cleanup = undefined;
|
|
6346
|
-
return;
|
|
6347
|
-
}
|
|
6348
|
-
const cols = ['[data-aionui-explorer-col]', '[data-aionui-preview-col]'];
|
|
6349
|
-
const seen = new Map();
|
|
6350
|
-
const play = (el) => {
|
|
6351
|
-
el.animate([
|
|
6352
|
-
{ opacity: 0, transform: 'translateY(28px)' },
|
|
6353
|
-
{ opacity: 1, transform: 'none' },
|
|
6354
|
-
], { duration: 280, easing: 'cubic-bezier(.16, 1, .3, 1)', fill: 'backwards' });
|
|
6355
|
-
};
|
|
6356
|
-
const check = () => {
|
|
6357
|
-
for (const sel of cols) {
|
|
6358
|
-
const el = document.querySelector(sel);
|
|
6359
|
-
if (el === null)
|
|
6360
|
-
continue;
|
|
6361
|
-
const visible = getComputedStyle(el).visibility === 'visible';
|
|
6362
|
-
const prev = seen.get(sel) ?? false;
|
|
6363
|
-
if (visible && !prev)
|
|
6364
|
-
play(el);
|
|
6365
|
-
seen.set(sel, visible);
|
|
6366
|
-
}
|
|
6367
|
-
};
|
|
6368
|
-
const observer = new MutationObserver(check);
|
|
6369
|
-
// Visibility flips come through inline style mutations (suite) or the
|
|
6370
|
-
// explorer-open marker on the frame; class changes are watched too.
|
|
6371
|
-
observer.observe(document.body, { attributes: true, subtree: true, attributeFilter: ['style', 'class', 'data-aionui-explorer-open'] });
|
|
6372
|
-
check();
|
|
6373
|
-
cleanup = () => {
|
|
6374
|
-
observer.disconnect();
|
|
6375
|
-
};
|
|
6376
|
-
};
|
|
6377
|
-
install();
|
|
6378
|
-
narrow.addEventListener('change', install);
|
|
6622
|
+
const onChange = (event) => (event.matches ? sync() : restore());
|
|
6623
|
+
narrow.addEventListener('change', onChange);
|
|
6624
|
+
const observer = new MutationObserver(() => {
|
|
6625
|
+
if (narrow.matches)
|
|
6626
|
+
themeMeta.content = bodyBg();
|
|
6627
|
+
});
|
|
6628
|
+
observer.observe(document.body, { attributes: true, attributeFilter: ['data-ds-dark-theme'] });
|
|
6629
|
+
document.addEventListener('gesturestart', onGestureStart);
|
|
6379
6630
|
return () => {
|
|
6380
|
-
narrow.removeEventListener('change',
|
|
6381
|
-
|
|
6631
|
+
narrow.removeEventListener('change', onChange);
|
|
6632
|
+
observer.disconnect();
|
|
6633
|
+
document.removeEventListener('gesturestart', onGestureStart);
|
|
6634
|
+
restore();
|
|
6382
6635
|
};
|
|
6383
|
-
}, 'dsh-mobile-nav:
|
|
6636
|
+
}, 'dsh-mobile-nav: status bar theme + viewport + zoom guard');
|
|
6637
|
+
}
|
|
6638
|
+
/**
|
|
6639
|
+
* Does the layout viewport sit shorter than the screen by at least the top
|
|
6640
|
+
* inset env() still claims? (S1.2 predicate, wired in S1.3.)
|
|
6641
|
+
*
|
|
6642
|
+
* Device numbers (iPhone, iOS 26.5, standalone PWA, 393x852): innerHeight
|
|
6643
|
+
* 793, env top 59 (= 852 - 793), env bottom 34, frame bottom 793 flush.
|
|
6644
|
+
* The same device in a Safari TAB reads innerHeight 695 with every surface
|
|
6645
|
+
* flush and every design value correct, which is what rules our own CSS out.
|
|
6646
|
+
*
|
|
6647
|
+
* What the true-branch means, settled by S1.2 + S1.3 device evidence: the
|
|
6648
|
+
* viewport is anchored at screen y=0 and merely cut 59px short at the BOTTOM
|
|
6649
|
+
* — not pushed down below the status bar. So the top inset is paid exactly
|
|
6650
|
+
* once and must be left alone, while the bottom inset is a lie: the home
|
|
6651
|
+
* indicator lives in the dead strip below the page. installSunkInset() acts
|
|
6652
|
+
* on that, and only on --mnav-sab.
|
|
6653
|
+
*
|
|
6654
|
+
* The `envTop > 0` guard is what keeps every other standalone install out:
|
|
6655
|
+
* landscape iPhone (top inset 0, the notch moves to left/right), iPad, and
|
|
6656
|
+
* Android — where standalone also loses status-bar height off innerHeight but
|
|
6657
|
+
* reports env top 0 — all fall through to false. Kept pure so the boundaries
|
|
6658
|
+
* are checkable off-device: scripts/check-sunk-viewport.mjs.
|
|
6659
|
+
*/
|
|
6660
|
+
function isViewportSunkBelowStatusBar(input) {
|
|
6661
|
+
return input.standalone
|
|
6662
|
+
&& input.envTop > 0
|
|
6663
|
+
&& input.screenHeight - input.innerHeight >= input.envTop;
|
|
6384
6664
|
}
|
|
6385
|
-
};
|
|
6386
|
-
__modules["effects/header-status.js"] = function (require, module, exports) {
|
|
6387
|
-
"use strict";
|
|
6388
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6389
|
-
exports.installHeaderStatusDot = installHeaderStatusDot;
|
|
6390
|
-
const session_dot_ts_1 = require("./session-dot.js");
|
|
6391
6665
|
/**
|
|
6392
|
-
*
|
|
6393
|
-
*
|
|
6394
|
-
*
|
|
6395
|
-
*
|
|
6396
|
-
*
|
|
6397
|
-
*
|
|
6398
|
-
*
|
|
6399
|
-
*
|
|
6400
|
-
*
|
|
6401
|
-
*
|
|
6666
|
+
* iOS standalone-PWA keyboard shrink (S1.2, 2026-08-17) — the ~60px white
|
|
6667
|
+
* band under the composer AND under the session list.
|
|
6668
|
+
*
|
|
6669
|
+
* Documented WebKit defect: the first time the software keyboard opens inside
|
|
6670
|
+
* a home-screen (standalone) PWA, the layout viewport permanently loses the
|
|
6671
|
+
* status-bar height for the rest of the app session. innerHeight,
|
|
6672
|
+
* visualViewport.height and 100dvh all report the shrunken value together, so
|
|
6673
|
+
* nothing inside the page can see anything wrong — the page simply ends early
|
|
6674
|
+
* and the strip below it is system background that no CSS can reach. Reported
|
|
6675
|
+
* as 932 -> 873 on an iPhone Pro Max; the user's device reads 852 -> 793.
|
|
6676
|
+
* Both are exactly one status bar. See
|
|
6677
|
+
* https://dev.to/cederhook/fixing-the-ios-standalone-pwa-keyboard-bug-that-shrinks-your-viewport-for-good-63d
|
|
6678
|
+
*
|
|
6679
|
+
* This is why the reported symptom is asymmetric and why the earlier
|
|
6680
|
+
* "double-counted top inset" reading was wrong: the top edge is genuinely
|
|
6681
|
+
* correct (env top 59 is paid once, the viewport starts at screen y=0), the
|
|
6682
|
+
* bottom is simply 59px short. It also explains a chat client being hit
|
|
6683
|
+
* hardest — the trigger is typing, which happens on the first message.
|
|
6684
|
+
*
|
|
6685
|
+
* The only known cure is to make WebKit re-measure: drop a full-viewport
|
|
6686
|
+
* element out of the box tree and force a synchronous reflow. Two departures
|
|
6687
|
+
* from the published recipe:
|
|
6688
|
+
* - scrollTop is saved and restored around the toggle. Un-boxing an ancestor
|
|
6689
|
+
* resets every descendant scroller to 0, which in a chat client means the
|
|
6690
|
+
* conversation jumps to its first message. Both happen inside one task, so
|
|
6691
|
+
* no frame is ever painted in between and nothing flickers.
|
|
6692
|
+
* - the baseline is the tallest innerHeight this session has actually seen,
|
|
6693
|
+
* not the screen height. On any device where the viewport is legitimately
|
|
6694
|
+
* short, the baseline equals the current height and this never fires.
|
|
6695
|
+
*
|
|
6696
|
+
* Standalone-gated, so a browser tab (and every desktop, CDP included) is a
|
|
6697
|
+
* strict no-op — which is also why the fix cannot be regression-tested here
|
|
6698
|
+
* and has to be confirmed on the device.
|
|
6402
6699
|
*/
|
|
6403
|
-
function
|
|
6700
|
+
function installViewportHeal(ctx) {
|
|
6404
6701
|
ctx.effect(() => {
|
|
6405
|
-
const
|
|
6702
|
+
const standalone = window.matchMedia('(display-mode: standalone)').matches
|
|
6703
|
+
|| navigator.standalone === true;
|
|
6704
|
+
/* iOS-only on purpose. The ceiling below leans on screen.height being the
|
|
6705
|
+
height the viewport SHOULD have, which holds for an iPhone standalone
|
|
6706
|
+
app under viewport-fit=cover and does not hold on Android, where the
|
|
6707
|
+
navigation bar makes screen.height permanently larger than any viewport
|
|
6708
|
+
and every keyboard dismiss would trigger a pointless reflow. */
|
|
6709
|
+
if (!standalone || !/iPad|iPhone|iPod/.test(navigator.userAgent))
|
|
6710
|
+
return () => { };
|
|
6711
|
+
/* Ceiling = the tallest this session has seen, floored at the screen —
|
|
6712
|
+
the screen half matters because the shrink outlives a page reload, so a
|
|
6713
|
+
PWA reopened into the broken state would otherwise measure 793 as
|
|
6714
|
+
"normal" and never heal itself. */
|
|
6715
|
+
let tallest = window.innerHeight;
|
|
6716
|
+
const ceiling = () => Math.max(tallest, window.screen.height);
|
|
6717
|
+
const onResize = () => {
|
|
6718
|
+
if (window.innerHeight > tallest)
|
|
6719
|
+
tallest = window.innerHeight;
|
|
6720
|
+
};
|
|
6721
|
+
/* Give up after three ineffective attempts per trigger: on a device whose
|
|
6722
|
+
viewport is short for a reason a reflow cannot fix (the iOS 26.x
|
|
6723
|
+
standalone regression family) retrying forever buys nothing. The two
|
|
6724
|
+
budgets are separate on purpose — the cold-start attempts would
|
|
6725
|
+
otherwise spend the whole allowance before the user ever types, and the
|
|
6726
|
+
keyboard shrink IS reflow-curable even on a device whose cold start is
|
|
6727
|
+
not. */
|
|
6728
|
+
const strikes = { boot: 0, blur: 0 };
|
|
6729
|
+
const heal = (kind) => {
|
|
6730
|
+
if (strikes[kind] >= 3 || ceiling() - window.innerHeight <= 4)
|
|
6731
|
+
return;
|
|
6406
6732
|
const frame = document.querySelector('[data-mobile-nav="frame"]');
|
|
6407
6733
|
if (frame === null)
|
|
6408
6734
|
return;
|
|
6409
|
-
const
|
|
6410
|
-
const
|
|
6411
|
-
const
|
|
6412
|
-
|
|
6413
|
-
|
|
6414
|
-
|
|
6415
|
-
|
|
6735
|
+
const scroller = document.querySelector('[class$="_scrollBody"]');
|
|
6736
|
+
const scrollTop = scroller?.scrollTop ?? 0;
|
|
6737
|
+
const previous = frame.style.display;
|
|
6738
|
+
frame.style.display = 'none';
|
|
6739
|
+
void frame.offsetHeight; /* forces the synchronous reflow that re-measures */
|
|
6740
|
+
frame.style.display = previous;
|
|
6741
|
+
if (scroller !== null)
|
|
6742
|
+
scroller.scrollTop = scrollTop;
|
|
6743
|
+
strikes[kind] = ceiling() - window.innerHeight > 4 ? strikes[kind] + 1 : 0;
|
|
6416
6744
|
};
|
|
6417
|
-
|
|
6418
|
-
|
|
6419
|
-
|
|
6420
|
-
|
|
6421
|
-
|
|
6422
|
-
|
|
6423
|
-
|
|
6424
|
-
|
|
6425
|
-
|
|
6426
|
-
|
|
6427
|
-
|
|
6428
|
-
|
|
6429
|
-
|
|
6430
|
-
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
|
|
6436
|
-
|
|
6437
|
-
|
|
6438
|
-
|
|
6439
|
-
|
|
6440
|
-
|
|
6441
|
-
|
|
6442
|
-
|
|
6443
|
-
|
|
6444
|
-
|
|
6445
|
-
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
|
|
6449
|
-
|
|
6450
|
-
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
sheet.parentElement?.querySelector('[data-mobile-nav$="-mask"]')?.click();
|
|
6745
|
+
/* Blur is when the keyboard starts closing; the viewport settles a beat
|
|
6746
|
+
later. Two attempts because the dismiss animation length is not
|
|
6747
|
+
specified anywhere, and a heal that runs too early is a silent no-op. */
|
|
6748
|
+
const timers = [];
|
|
6749
|
+
const onFocusOut = (event) => {
|
|
6750
|
+
if (!(event.target instanceof HTMLElement))
|
|
6751
|
+
return;
|
|
6752
|
+
if (event.target.matches('textarea, input') !== true)
|
|
6753
|
+
return;
|
|
6754
|
+
timers.push(window.setTimeout(() => heal('blur'), 300), window.setTimeout(() => heal('blur'), 900));
|
|
6755
|
+
};
|
|
6756
|
+
/* Cold start (S1.3): the user's iOS 26.5 device is already 59px short on
|
|
6757
|
+
a freshly killed-and-reopened app that never saw the keyboard, so the
|
|
6758
|
+
keyboard trigger alone is not the whole story. Same reflow, tried once
|
|
6759
|
+
the shell has mounted and again after it has settled, plus every return
|
|
6760
|
+
to the foreground — iOS re-lays a backgrounded PWA out and that is
|
|
6761
|
+
another moment the height can come back wrong. Whether a reflow can win
|
|
6762
|
+
against a system-level regression is unknowable off-device; the strike
|
|
6763
|
+
budget is what keeps a losing attempt cheap. */
|
|
6764
|
+
timers.push(window.setTimeout(() => heal('boot'), 1000), window.setTimeout(() => heal('boot'), 3000));
|
|
6765
|
+
const onVisible = () => {
|
|
6766
|
+
if (document.visibilityState !== 'visible')
|
|
6767
|
+
return;
|
|
6768
|
+
timers.push(window.setTimeout(() => heal('boot'), 300));
|
|
6769
|
+
};
|
|
6770
|
+
window.addEventListener('resize', onResize);
|
|
6771
|
+
document.addEventListener('focusout', onFocusOut, true);
|
|
6772
|
+
document.addEventListener('visibilitychange', onVisible);
|
|
6773
|
+
return () => {
|
|
6774
|
+
window.removeEventListener('resize', onResize);
|
|
6775
|
+
document.removeEventListener('focusout', onFocusOut, true);
|
|
6776
|
+
document.removeEventListener('visibilitychange', onVisible);
|
|
6777
|
+
for (const t of timers)
|
|
6778
|
+
clearTimeout(t);
|
|
6779
|
+
};
|
|
6780
|
+
}, 'dsh-mobile-nav: iOS viewport-shrink heal (boot / foreground / keyboard)');
|
|
6454
6781
|
}
|
|
6455
6782
|
/**
|
|
6456
|
-
*
|
|
6457
|
-
* info, home workspace picker) support "drag down to dismiss", mirroring
|
|
6458
|
-
* the mask-tap close they already have. Official popupSelect menus
|
|
6459
|
-
* (permission/model) are excluded on purpose — the design spec leaves them
|
|
6460
|
-
* mask-tap-only, and they are the suite's own DOM, not ours to add gesture
|
|
6461
|
-
* handling to.
|
|
6783
|
+
* Sunk-viewport compensation (S1.3, 2026-08-17) — zeroes --mnav-sab ONLY.
|
|
6462
6784
|
*
|
|
6463
|
-
*
|
|
6464
|
-
*
|
|
6465
|
-
*
|
|
6466
|
-
*
|
|
6467
|
-
*
|
|
6468
|
-
*
|
|
6785
|
+
* When isViewportSunkBelowStatusBar() holds, the layout viewport is 59px
|
|
6786
|
+
* shorter than the screen while env() still reports the full pair of insets.
|
|
6787
|
+
* The bottom one is then a lie with a cost: the home indicator sits in the
|
|
6788
|
+
* dead strip BELOW the page, so the 34px we reserve for it is 34px of blank
|
|
6789
|
+
* page stacked on top of an already-blank system band. Zeroing it does not
|
|
6790
|
+
* fix the band (nothing in the page can) but stops us widening it.
|
|
6791
|
+
*
|
|
6792
|
+
* --mnav-sat is deliberately left alone: the top edge is measured correct on
|
|
6793
|
+
* the device (header sits right under the notch), and the S1.2 round already
|
|
6794
|
+
* burned a cycle on the theory that it was double-counted.
|
|
6795
|
+
*
|
|
6796
|
+
* Reversible by design. The predicate is re-evaluated on every resize, so a
|
|
6797
|
+
* viewport that comes back to full height — the heal above winning, or Apple
|
|
6798
|
+
* shipping the fix — drops the override and the normal env() compensation
|
|
6799
|
+
* returns without a reload. `data-mnav-sunk` on <html> is what the debug
|
|
6800
|
+
* badge reads, so the badge reports the state that is actually in force
|
|
6801
|
+
* rather than re-deriving it.
|
|
6469
6802
|
*/
|
|
6470
|
-
function
|
|
6803
|
+
function installSunkInset(ctx) {
|
|
6804
|
+
ctx.effect(() => {
|
|
6805
|
+
/* ?mobile-nav-inset outranks this: both write the same inline property on
|
|
6806
|
+
<html>, and the debug param exists precisely to fake insets that this
|
|
6807
|
+
would then erase. Param present = stay out entirely, attribute included,
|
|
6808
|
+
so the badge shows "n/a" instead of a state nobody is enforcing. */
|
|
6809
|
+
if (new URLSearchParams(location.search).has('mobile-nav-inset'))
|
|
6810
|
+
return () => { };
|
|
6811
|
+
const root = document.documentElement;
|
|
6812
|
+
/* env() is only readable through a real element's computed style. */
|
|
6813
|
+
const probe = document.createElement('div');
|
|
6814
|
+
probe.style.cssText =
|
|
6815
|
+
'position:fixed;top:0;left:0;visibility:hidden;pointer-events:none;' +
|
|
6816
|
+
'padding-top:env(safe-area-inset-top,0px)';
|
|
6817
|
+
document.body.appendChild(probe);
|
|
6818
|
+
const sync = () => {
|
|
6819
|
+
const sunk = isViewportSunkBelowStatusBar({
|
|
6820
|
+
standalone: window.matchMedia('(display-mode: standalone)').matches
|
|
6821
|
+
|| navigator.standalone === true,
|
|
6822
|
+
screenHeight: window.screen.height,
|
|
6823
|
+
innerHeight: window.innerHeight,
|
|
6824
|
+
envTop: Number.parseFloat(getComputedStyle(probe).paddingTop) || 0,
|
|
6825
|
+
});
|
|
6826
|
+
root.dataset.mnavSunk = sunk ? '1' : '0';
|
|
6827
|
+
if (sunk)
|
|
6828
|
+
root.style.setProperty('--mnav-sab', '0px');
|
|
6829
|
+
else
|
|
6830
|
+
root.style.removeProperty('--mnav-sab');
|
|
6831
|
+
};
|
|
6832
|
+
sync();
|
|
6833
|
+
/* resize covers rotation and any height the heal above wins back.
|
|
6834
|
+
Deliberately not visualViewport: it fires on every keyboard show/hide
|
|
6835
|
+
while innerHeight, the value read here, does not move on iOS. */
|
|
6836
|
+
window.addEventListener('resize', sync);
|
|
6837
|
+
window.addEventListener('orientationchange', sync);
|
|
6838
|
+
return () => {
|
|
6839
|
+
window.removeEventListener('resize', sync);
|
|
6840
|
+
window.removeEventListener('orientationchange', sync);
|
|
6841
|
+
probe.remove();
|
|
6842
|
+
root.style.removeProperty('--mnav-sab');
|
|
6843
|
+
delete root.dataset.mnavSunk;
|
|
6844
|
+
};
|
|
6845
|
+
}, 'dsh-mobile-nav: sunk-viewport bottom-inset override');
|
|
6846
|
+
}
|
|
6847
|
+
};
|
|
6848
|
+
__modules["effects/aionui-compat.js"] = function (require, module, exports) {
|
|
6849
|
+
"use strict";
|
|
6850
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6851
|
+
exports.installAionuiCompat = installAionuiCompat;
|
|
6852
|
+
/** dsh-web-ui 兼容:explorer / preview 列的显隐标记与升起动画(同域同机制,合并一处)。 */
|
|
6853
|
+
function installAionuiCompat(ctx) {
|
|
6854
|
+
// dsh-web-ui compatibility: the aionui explorer column would render as a
|
|
6855
|
+
// sheet over the whole mobile UI whenever its (persisted) expanded state
|
|
6856
|
+
// is active — including right after a reload, with no way out (the
|
|
6857
|
+
// suite's floating expand button only exists while collapsed). Instead
|
|
6858
|
+
// of fighting the suite's store timing, the mobile stylesheet keeps the
|
|
6859
|
+
// explorer column hidden by default and the header's Files action (plus
|
|
6860
|
+
// the drawer footer entry) opens it via the `data-aionui-explorer-open`
|
|
6861
|
+
// marker on the frame. This effect just clears that marker when the
|
|
6862
|
+
// sheet's own collapse chevron is tapped, so closing is symmetric with
|
|
6863
|
+
// opening.
|
|
6471
6864
|
ctx.effect(() => {
|
|
6472
|
-
|
|
6473
|
-
|
|
6474
|
-
|
|
6475
|
-
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
|
|
6482
|
-
sheet.style.transition = 'none';
|
|
6483
|
-
sheet.style.transform = '';
|
|
6865
|
+
// Arm on the CURRENT width and re-arm on every width change: the guard
|
|
6866
|
+
// used to run once at apply time, so a wide→narrow transition (desktop
|
|
6867
|
+
// resize, tablet split view) left the markers dead and the explorer /
|
|
6868
|
+
// preview sheets could neither open nor close properly.
|
|
6869
|
+
const narrow = window.matchMedia('(max-width: 1023px)');
|
|
6870
|
+
let cleanup;
|
|
6871
|
+
const install = () => {
|
|
6872
|
+
cleanup?.();
|
|
6873
|
+
if (!narrow.matches) {
|
|
6874
|
+
cleanup = undefined;
|
|
6484
6875
|
return;
|
|
6485
6876
|
}
|
|
6486
|
-
|
|
6487
|
-
|
|
6488
|
-
|
|
6489
|
-
|
|
6490
|
-
|
|
6877
|
+
const onChevronClick = (event) => {
|
|
6878
|
+
const target = event.target;
|
|
6879
|
+
if (target === null || !target.closest('.aionui-collapse-chevron'))
|
|
6880
|
+
return;
|
|
6881
|
+
document.querySelector('[data-mobile-nav="frame"]')?.removeAttribute('data-aionui-explorer-open');
|
|
6491
6882
|
};
|
|
6492
|
-
|
|
6883
|
+
document.addEventListener('click', onChevronClick, true);
|
|
6884
|
+
cleanup = () => document.removeEventListener('click', onChevronClick, true);
|
|
6493
6885
|
};
|
|
6494
|
-
|
|
6495
|
-
|
|
6496
|
-
|
|
6497
|
-
|
|
6498
|
-
|
|
6499
|
-
const touch = event.touches[0];
|
|
6500
|
-
const target = event.target;
|
|
6501
|
-
if (touch === undefined || !(target instanceof Element)) {
|
|
6502
|
-
drag = null;
|
|
6503
|
-
return;
|
|
6504
|
-
}
|
|
6505
|
-
const sheet = target.closest(SHEET_SELECTOR);
|
|
6506
|
-
if (sheet === null) {
|
|
6507
|
-
drag = null;
|
|
6508
|
-
return;
|
|
6509
|
-
}
|
|
6510
|
-
drag = { sheet, startY: touch.clientY, lastY: touch.clientY, startTime: Date.now(), dragging: false };
|
|
6886
|
+
install();
|
|
6887
|
+
narrow.addEventListener('change', install);
|
|
6888
|
+
return () => {
|
|
6889
|
+
narrow.removeEventListener('change', install);
|
|
6890
|
+
cleanup?.();
|
|
6511
6891
|
};
|
|
6512
|
-
|
|
6513
|
-
|
|
6514
|
-
|
|
6515
|
-
|
|
6516
|
-
|
|
6892
|
+
}, 'dsh-mobile-nav: aionui explorer close marker');
|
|
6893
|
+
// dsh-web-ui compatibility: the aionui preview column persists its open
|
|
6894
|
+
// tabs in localStorage and restores them on load, which would pop the
|
|
6895
|
+
// preview sheet over the fresh UI after a reload. Gate it like the
|
|
6896
|
+
// explorer: the stylesheet keeps the column hidden unless the frame
|
|
6897
|
+
// carries `data-aionui-preview-open`; this effect sets that marker when
|
|
6898
|
+
// the user actually taps a file row in the explorer sheet, and clears it
|
|
6899
|
+
// whenever the suite hides the column again (collapse chevron / tab
|
|
6900
|
+
// close), so a restored-but-unwanted sheet never appears.
|
|
6901
|
+
ctx.effect(() => {
|
|
6902
|
+
// Arm on the CURRENT width and re-arm on every width change (see the
|
|
6903
|
+
// explorer marker effect for why).
|
|
6904
|
+
const narrow = window.matchMedia('(max-width: 1023px)');
|
|
6905
|
+
let cleanup;
|
|
6906
|
+
const install = () => {
|
|
6907
|
+
cleanup?.();
|
|
6908
|
+
if (!narrow.matches) {
|
|
6909
|
+
cleanup = undefined;
|
|
6517
6910
|
return;
|
|
6518
|
-
|
|
6519
|
-
const
|
|
6520
|
-
|
|
6521
|
-
|
|
6911
|
+
}
|
|
6912
|
+
const frame = () => document.querySelector('[data-mobile-nav="frame"]');
|
|
6913
|
+
// Closing the preview sheet also drops the fullscreen marker, so the
|
|
6914
|
+
// next preview starts in the sheet layout again.
|
|
6915
|
+
const closePreview = () => {
|
|
6916
|
+
frame()?.removeAttribute('data-aionui-preview-open');
|
|
6917
|
+
frame()?.removeAttribute('data-mobile-preview-full');
|
|
6918
|
+
};
|
|
6919
|
+
const onTap = (event) => {
|
|
6920
|
+
const target = event.target;
|
|
6921
|
+
if (target === null)
|
|
6522
6922
|
return;
|
|
6523
|
-
|
|
6923
|
+
const row = target.closest('[data-aionui-explorer-col] [class*="_treeRow"]');
|
|
6924
|
+
if (row === null)
|
|
6524
6925
|
return;
|
|
6525
|
-
|
|
6526
|
-
|
|
6527
|
-
|
|
6528
|
-
|
|
6529
|
-
|
|
6530
|
-
|
|
6531
|
-
|
|
6532
|
-
|
|
6533
|
-
|
|
6534
|
-
|
|
6535
|
-
|
|
6536
|
-
|
|
6537
|
-
|
|
6538
|
-
|
|
6539
|
-
|
|
6540
|
-
|
|
6541
|
-
|
|
6542
|
-
|
|
6543
|
-
|
|
6544
|
-
|
|
6545
|
-
|
|
6546
|
-
|
|
6547
|
-
|
|
6548
|
-
|
|
6549
|
-
|
|
6550
|
-
|
|
6551
|
-
|
|
6552
|
-
|
|
6553
|
-
|
|
6554
|
-
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
|
|
6558
|
-
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6562
|
-
|
|
6563
|
-
|
|
6564
|
-
|
|
6565
|
-
|
|
6566
|
-
|
|
6567
|
-
|
|
6568
|
-
|
|
6569
|
-
|
|
6570
|
-
|
|
6571
|
-
|
|
6572
|
-
|
|
6573
|
-
|
|
6574
|
-
|
|
6575
|
-
|
|
6576
|
-
|
|
6577
|
-
|
|
6578
|
-
|
|
6579
|
-
|
|
6580
|
-
|
|
6581
|
-
|
|
6582
|
-
|
|
6583
|
-
|
|
6584
|
-
|
|
6585
|
-
|
|
6586
|
-
|
|
6587
|
-
|
|
6588
|
-
* mid-workflow (info card open, workbench panel open) gets "close that"
|
|
6589
|
-
* instead of "leave the session" from the same gesture. Checked in the
|
|
6590
|
-
* design doc's own order:
|
|
6591
|
-
* 1. this plugin's session-info sheet, if open
|
|
6592
|
-
* 2. this plugin's home sheet (workspace switcher or S5 chip-customize),
|
|
6593
|
-
* if open
|
|
6594
|
-
* 3. dsh-better-sidebar's workbench panel, if open (代点 its own toggle —
|
|
6595
|
-
* same anchor MobileSessionHeader.tsx's workbench/close buttons use)
|
|
6596
|
-
* 4. otherwise, GO_HOME_EVENT — but only from the session view; the design
|
|
6597
|
-
* doc calls out "在 home 层→无动作" explicitly, and the phone page
|
|
6598
|
-
* stack's current level is read straight off MobileHome's own
|
|
6599
|
-
* `data-view` attribute (no store handle needed here — effects/*.ts is
|
|
6600
|
-
* already all DOM reads, matching every sibling effect in this file).
|
|
6601
|
-
*/
|
|
6602
|
-
function handleEdgeSwipeBack() {
|
|
6603
|
-
const sheet = document.querySelector(SHEET_SELECTOR);
|
|
6604
|
-
if (sheet !== null) {
|
|
6605
|
-
closeSheet(sheet);
|
|
6606
|
-
return;
|
|
6607
|
-
}
|
|
6608
|
-
if (betterSidebarPanelOpen()) {
|
|
6609
|
-
document.querySelector('[data-dsh-better-sidebar] button[class$="_toggleButton"]')?.click();
|
|
6610
|
-
return;
|
|
6611
|
-
}
|
|
6612
|
-
if (document.querySelector('[data-mobile-nav="home"]')?.getAttribute('data-view') === 'session') {
|
|
6613
|
-
window.dispatchEvent(new CustomEvent(nav_store_ts_1.GO_HOME_EVENT));
|
|
6614
|
-
}
|
|
6615
|
-
}
|
|
6616
|
-
/**
|
|
6617
|
-
* S6.1 — left-edge swipe-back gesture install. Passive throughout (never
|
|
6618
|
-
* calls preventDefault) and decided purely from the touchstart/touchend
|
|
6619
|
-
* endpoints, exactly like the S6.1-predecessor content-area swipe this
|
|
6620
|
-
* revision removes — so it never fights vertical (or, at the 24px strip,
|
|
6621
|
-
* horizontal chip-row) scrolling, and needs no mid-drag tracking: the design
|
|
6622
|
-
* doc explicitly allows "一步到位" over a followed-finger animation.
|
|
6623
|
-
*/
|
|
6624
|
-
function installEdgeSwipeBack(ctx) {
|
|
6926
|
+
// Only FILE rows open the preview sheet. Directory rows toggle
|
|
6927
|
+
// expansion and must not pop the (possibly stale, restored-from-
|
|
6928
|
+
// localStorage) preview tab over the tree. Substring matching:
|
|
6929
|
+
// the suite's hashed classes carry a hash prefix, so the exact-token
|
|
6930
|
+
// `class~=` form never matches and the trailing `$=` form misses
|
|
6931
|
+
// selected rows (`_treeRowSelected`) and open arrows
|
|
6932
|
+
// (`_treeArrowOpen`) — regressions of issue #8. The arrow gate must
|
|
6933
|
+
// additionally exclude the leaf marker: FILE rows render a
|
|
6934
|
+
// `_treeArrowEmpty` span whose class still contains the `_treeArrow`
|
|
6935
|
+
// substring, so a bare substring match would treat every row as a
|
|
6936
|
+
// directory and no preview would ever open.
|
|
6937
|
+
if (row.querySelector('[class*="_treeArrow"]:not([class*="_treeArrowEmpty"])') !== null)
|
|
6938
|
+
return;
|
|
6939
|
+
frame()?.setAttribute('data-aionui-preview-open', '');
|
|
6940
|
+
};
|
|
6941
|
+
// The preview sheet's own collapse button (the two inward arrows in the
|
|
6942
|
+
// tab bar) closes the AionUI store, but on mobile the suite's layout sync
|
|
6943
|
+
// can be skipped while its shell-track mirror is not ready yet — in that
|
|
6944
|
+
// case the inline visibility never flips to hidden and the visibility
|
|
6945
|
+
// watcher below would never clear our marker. Clear it directly on the
|
|
6946
|
+
// button click so the sheet always closes regardless of the suite's sync.
|
|
6947
|
+
const onCollapse = (event) => {
|
|
6948
|
+
const target = event.target;
|
|
6949
|
+
if (target === null)
|
|
6950
|
+
return;
|
|
6951
|
+
if (target.closest('[data-aionui-preview-col] [class$="_panelCollapse"]') !== null) {
|
|
6952
|
+
closePreview();
|
|
6953
|
+
}
|
|
6954
|
+
};
|
|
6955
|
+
const sync = () => {
|
|
6956
|
+
const pv = document.querySelector('[data-aionui-preview-col]');
|
|
6957
|
+
if (pv === null)
|
|
6958
|
+
return;
|
|
6959
|
+
// Read the suite's inline visibility, not the computed value: while the
|
|
6960
|
+
// `data-aionui-preview-open` marker is present our stylesheet forces the
|
|
6961
|
+
// sheet visible with !important, so getComputedStyle() would never report
|
|
6962
|
+
// hidden and the marker would never be cleared.
|
|
6963
|
+
if (pv.style.visibility === 'hidden')
|
|
6964
|
+
closePreview();
|
|
6965
|
+
};
|
|
6966
|
+
document.addEventListener('click', onTap, true);
|
|
6967
|
+
document.addEventListener('click', onCollapse, true);
|
|
6968
|
+
const observer = new MutationObserver(sync);
|
|
6969
|
+
observer.observe(document.body, { attributes: true, subtree: true, attributeFilter: ['style'] });
|
|
6970
|
+
sync();
|
|
6971
|
+
cleanup = () => {
|
|
6972
|
+
document.removeEventListener('click', onTap, true);
|
|
6973
|
+
document.removeEventListener('click', onCollapse, true);
|
|
6974
|
+
observer.disconnect();
|
|
6975
|
+
};
|
|
6976
|
+
};
|
|
6977
|
+
install();
|
|
6978
|
+
narrow.addEventListener('change', install);
|
|
6979
|
+
return () => {
|
|
6980
|
+
narrow.removeEventListener('change', install);
|
|
6981
|
+
cleanup?.();
|
|
6982
|
+
};
|
|
6983
|
+
}, 'dsh-mobile-nav: preview sheet open marker');
|
|
6984
|
+
// The dsh-web-ui explorer / preview columns toggle via `visibility`
|
|
6985
|
+
// (their inline style), which never restarts a CSS animation — so the
|
|
6986
|
+
// sheets would only animate on first mount. Replay the rise animation
|
|
6987
|
+
// with the Web Animations API each time a column turns visible, then
|
|
6988
|
+
// leave the resting state to the stylesheet.
|
|
6625
6989
|
ctx.effect(() => {
|
|
6626
|
-
|
|
6627
|
-
|
|
6628
|
-
const
|
|
6629
|
-
|
|
6630
|
-
|
|
6631
|
-
|
|
6632
|
-
|
|
6633
|
-
|
|
6634
|
-
if (touch === undefined) {
|
|
6635
|
-
start = null;
|
|
6990
|
+
// Arm on the CURRENT width and re-arm on every width change (see the
|
|
6991
|
+
// explorer marker effect for why).
|
|
6992
|
+
const narrow = window.matchMedia('(max-width: 1023px)');
|
|
6993
|
+
let cleanup;
|
|
6994
|
+
const install = () => {
|
|
6995
|
+
cleanup?.();
|
|
6996
|
+
if (!narrow.matches) {
|
|
6997
|
+
cleanup = undefined;
|
|
6636
6998
|
return;
|
|
6637
6999
|
}
|
|
6638
|
-
|
|
6639
|
-
|
|
6640
|
-
|
|
6641
|
-
|
|
6642
|
-
|
|
6643
|
-
|
|
6644
|
-
|
|
6645
|
-
|
|
6646
|
-
|
|
6647
|
-
|
|
6648
|
-
|
|
6649
|
-
|
|
6650
|
-
|
|
6651
|
-
|
|
6652
|
-
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
6658
|
-
|
|
6659
|
-
|
|
6660
|
-
|
|
6661
|
-
|
|
7000
|
+
const cols = ['[data-aionui-explorer-col]', '[data-aionui-preview-col]'];
|
|
7001
|
+
const seen = new Map();
|
|
7002
|
+
const play = (el) => {
|
|
7003
|
+
el.animate([
|
|
7004
|
+
{ opacity: 0, transform: 'translateY(28px)' },
|
|
7005
|
+
{ opacity: 1, transform: 'none' },
|
|
7006
|
+
], { duration: 280, easing: 'cubic-bezier(.16, 1, .3, 1)', fill: 'backwards' });
|
|
7007
|
+
};
|
|
7008
|
+
const check = () => {
|
|
7009
|
+
for (const sel of cols) {
|
|
7010
|
+
const el = document.querySelector(sel);
|
|
7011
|
+
if (el === null)
|
|
7012
|
+
continue;
|
|
7013
|
+
const visible = getComputedStyle(el).visibility === 'visible';
|
|
7014
|
+
const prev = seen.get(sel) ?? false;
|
|
7015
|
+
if (visible && !prev)
|
|
7016
|
+
play(el);
|
|
7017
|
+
seen.set(sel, visible);
|
|
7018
|
+
}
|
|
7019
|
+
};
|
|
7020
|
+
const observer = new MutationObserver(check);
|
|
7021
|
+
// Visibility flips come through inline style mutations (suite) or the
|
|
7022
|
+
// explorer-open marker on the frame; class changes are watched too.
|
|
7023
|
+
observer.observe(document.body, { attributes: true, subtree: true, attributeFilter: ['style', 'class', 'data-aionui-explorer-open'] });
|
|
7024
|
+
check();
|
|
7025
|
+
cleanup = () => {
|
|
7026
|
+
observer.disconnect();
|
|
7027
|
+
};
|
|
6662
7028
|
};
|
|
6663
|
-
|
|
6664
|
-
|
|
6665
|
-
const onChange = (event) => (event.matches ? attach() : detach());
|
|
6666
|
-
narrow.addEventListener('change', onChange);
|
|
7029
|
+
install();
|
|
7030
|
+
narrow.addEventListener('change', install);
|
|
6667
7031
|
return () => {
|
|
6668
|
-
narrow.removeEventListener('change',
|
|
6669
|
-
|
|
7032
|
+
narrow.removeEventListener('change', install);
|
|
7033
|
+
cleanup?.();
|
|
6670
7034
|
};
|
|
6671
|
-
}, 'dsh-mobile-nav:
|
|
6672
|
-
}
|
|
6673
|
-
/** S6: the two-gesture set — left-edge swipe-back and sheet drag-to-close.
|
|
6674
|
-
* Both install/uninstall their own document listeners on the phone
|
|
6675
|
-
* breakpoint's matchMedia change, so at >= 768px this is a true no-op (no
|
|
6676
|
-
* listeners attached at all), matching every other effect in this file. */
|
|
6677
|
-
function installGestures(ctx) {
|
|
6678
|
-
installEdgeSwipeBack(ctx);
|
|
6679
|
-
installSheetDragClose(ctx);
|
|
7035
|
+
}, 'dsh-mobile-nav: sheet rise animation replay');
|
|
6680
7036
|
}
|
|
6681
7037
|
};
|
|
6682
|
-
__modules["
|
|
7038
|
+
__modules["effects/header-status.js"] = function (require, module, exports) {
|
|
6683
7039
|
"use strict";
|
|
6684
7040
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6685
|
-
exports.
|
|
6686
|
-
|
|
6687
|
-
|
|
6688
|
-
|
|
6689
|
-
|
|
6690
|
-
|
|
6691
|
-
|
|
6692
|
-
|
|
6693
|
-
|
|
6694
|
-
|
|
6695
|
-
|
|
6696
|
-
|
|
6697
|
-
|
|
6698
|
-
|
|
6699
|
-
|
|
6700
|
-
|
|
6701
|
-
|
|
6702
|
-
|
|
6703
|
-
|
|
6704
|
-
|
|
6705
|
-
|
|
6706
|
-
|
|
6707
|
-
|
|
6708
|
-
|
|
6709
|
-
|
|
6710
|
-
|
|
6711
|
-
|
|
6712
|
-
|
|
6713
|
-
|
|
6714
|
-
|
|
6715
|
-
'
|
|
6716
|
-
|
|
6717
|
-
'infoJobs': '{count} 个后台任务',
|
|
6718
|
-
'activityOpen': '查看子代理与后台任务',
|
|
6719
|
-
'infoStatTurns': '轮次',
|
|
6720
|
-
'infoStatSteps': '步骤',
|
|
6721
|
-
'infoStatTtft': '首字延迟',
|
|
6722
|
-
'infoStatLlm': '模型耗时',
|
|
6723
|
-
'infoStatTool': '工具耗时',
|
|
6724
|
-
'infoStatCacheHit': '缓存命中',
|
|
6725
|
-
'infoTokenFlow': 'Token {io}',
|
|
6726
|
-
'infoExport': '导出日志',
|
|
6727
|
-
'infoRename': '重命名',
|
|
6728
|
-
'infoRenamePrompt': '会话新名称',
|
|
6729
|
-
'infoFork': 'Fork 会话',
|
|
6730
|
-
'infoArchive': '归档',
|
|
6731
|
-
'infoArchiveConfirm': '归档后将从会话列表隐藏,确定继续?',
|
|
6732
|
-
'infoActionError': '操作失败:{message}',
|
|
6733
|
-
'turnFold': '过程 · {count} 步',
|
|
6734
|
-
'turnFoldRunning': '进行中 · {count} 步',
|
|
6735
|
-
'settings': '设置',
|
|
6736
|
-
'chipTaskboard': '任务看板',
|
|
6737
|
-
'chipSsh': 'SSH',
|
|
6738
|
-
'chipUsage': '用量',
|
|
6739
|
-
'chipCustomize': '自定义入口',
|
|
6740
|
-
};
|
|
6741
|
-
/** English dictionary, key-identical to the Chinese source of truth. */
|
|
6742
|
-
exports.en = {
|
|
6743
|
-
'open': 'Open directory',
|
|
6744
|
-
'close': 'Close directory',
|
|
6745
|
-
'backdrop': 'Click to close directory',
|
|
6746
|
-
'sessionLog': 'Session log',
|
|
6747
|
-
'files': 'Files',
|
|
6748
|
-
'previewFullscreen': 'Fullscreen preview',
|
|
6749
|
-
'previewExitFullscreen': 'Exit fullscreen',
|
|
6750
|
-
'allWorkspaces': 'All',
|
|
6751
|
-
'switchWorkspace': 'Switch workspace',
|
|
6752
|
-
'newSession': 'New session',
|
|
6753
|
-
'noSessions': 'No sessions yet — tap + to start one',
|
|
6754
|
-
'homeStatusOngoing': 'Running',
|
|
6755
|
-
'homeStatusWarning': 'Needs attention',
|
|
6756
|
-
'homeStatusDone': 'Done',
|
|
6757
|
-
'backToList': 'Back to sessions',
|
|
6758
|
-
'sessionInfo': 'Session info',
|
|
6759
|
-
'workbench': 'Workbench',
|
|
6760
|
-
'workbenchClose': 'Close workbench',
|
|
6761
|
-
'switchView': 'Switch view',
|
|
6762
|
-
'attach': 'Add attachment',
|
|
6763
|
-
'attachBusy': 'Uploading…',
|
|
6764
|
-
'attachFailed': 'Upload failed — tap to retry',
|
|
6765
|
-
'attachRemove': 'Remove attachment {name}',
|
|
6766
|
-
'infoClose': 'Close',
|
|
6767
|
-
'infoMode': 'Mode',
|
|
6768
|
-
'infoCwdFallback': 'Unknown directory',
|
|
6769
|
-
'infoSubagents': '{count} subagents',
|
|
6770
|
-
'infoJobs': '{count} background tasks',
|
|
6771
|
-
'activityOpen': 'View subagents and background tasks',
|
|
6772
|
-
'infoStatTurns': 'Turns',
|
|
6773
|
-
'infoStatSteps': 'Steps',
|
|
6774
|
-
'infoStatTtft': 'TTFT',
|
|
6775
|
-
'infoStatLlm': 'LLM time',
|
|
6776
|
-
'infoStatTool': 'Tool time',
|
|
6777
|
-
'infoStatCacheHit': 'Cache hit',
|
|
6778
|
-
'infoTokenFlow': 'Tokens {io}',
|
|
6779
|
-
'infoExport': 'Export log',
|
|
6780
|
-
'infoRename': 'Rename',
|
|
6781
|
-
'infoRenamePrompt': 'New session name',
|
|
6782
|
-
'infoFork': 'Fork session',
|
|
6783
|
-
'infoArchive': 'Archive',
|
|
6784
|
-
'infoArchiveConfirm': 'Archiving hides this session from the list. Continue?',
|
|
6785
|
-
'infoActionError': 'Action failed: {message}',
|
|
6786
|
-
'turnFold': 'Process · {count} steps',
|
|
6787
|
-
'turnFoldRunning': 'Working · {count} steps',
|
|
6788
|
-
'settings': 'Settings',
|
|
6789
|
-
'chipTaskboard': 'Task board',
|
|
6790
|
-
'chipSsh': 'SSH',
|
|
6791
|
-
'chipUsage': 'Usage',
|
|
6792
|
-
'chipCustomize': 'Customize shortcuts',
|
|
6793
|
-
};
|
|
7041
|
+
exports.installHeaderStatusDot = installHeaderStatusDot;
|
|
7042
|
+
const session_dot_ts_1 = require("./session-dot.js");
|
|
7043
|
+
/**
|
|
7044
|
+
* Session header running-status dot (S2). No official element exists to
|
|
7045
|
+
* reposition (ConversationSessionHeader renders only the crumb title, the
|
|
7046
|
+
* actions/utilities slots, and the tablist — dsh-client-ui-conversation
|
|
7047
|
+
* lib/client.js:6949-7009), so this reads `ctx.sessions.list` directly
|
|
7048
|
+
* (the same feed `useSessions` wraps) and stamps a data attribute the
|
|
7049
|
+
* mobile stylesheet turns into a `::after` dot on the title crumb —
|
|
7050
|
+
* "read data, self-draw" is the plan's documented fallback for this piece.
|
|
7051
|
+
* Kept outside React: the dot must track the CURRENT session regardless of
|
|
7052
|
+
* which component the header happens to mount, and a plain attribute +
|
|
7053
|
+
* CSS avoids reaching into the official crumb's own DOM subtree.
|
|
7054
|
+
*/
|
|
7055
|
+
function installHeaderStatusDot(ctx) {
|
|
7056
|
+
ctx.effect(() => {
|
|
7057
|
+
const apply = () => {
|
|
7058
|
+
const frame = document.querySelector('[data-mobile-nav="frame"]');
|
|
7059
|
+
if (frame === null)
|
|
7060
|
+
return;
|
|
7061
|
+
const { current, byId } = ctx.sessions.list.getSnapshot();
|
|
7062
|
+
const row = current === undefined ? undefined : byId[current];
|
|
7063
|
+
const state = row === undefined ? undefined : (0, session_dot_ts_1.dotState)(row);
|
|
7064
|
+
if (state === undefined)
|
|
7065
|
+
frame.removeAttribute('data-mobile-nav-dot');
|
|
7066
|
+
else
|
|
7067
|
+
frame.setAttribute('data-mobile-nav-dot', state);
|
|
7068
|
+
};
|
|
7069
|
+
apply();
|
|
7070
|
+
return ctx.sessions.list.subscribe(apply);
|
|
7071
|
+
}, 'dsh-mobile-nav: header status dot');
|
|
7072
|
+
}
|
|
6794
7073
|
};
|
|
6795
|
-
__modules["effects/
|
|
7074
|
+
__modules["effects/gestures.js"] = function (require, module, exports) {
|
|
6796
7075
|
"use strict";
|
|
6797
7076
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6798
|
-
exports.
|
|
6799
|
-
const
|
|
7077
|
+
exports.installGestures = installGestures;
|
|
7078
|
+
const nav_store_ts_1 = require("./nav-store.js");
|
|
6800
7079
|
/** Phone breakpoint — same query every phone-only effect in this plugin uses. */
|
|
6801
7080
|
const PHONE_QUERY = '(max-width: 767px)';
|
|
6802
|
-
/**
|
|
6803
|
-
*
|
|
6804
|
-
*
|
|
6805
|
-
*
|
|
6806
|
-
*
|
|
6807
|
-
*
|
|
6808
|
-
|
|
6809
|
-
const
|
|
6810
|
-
|
|
6811
|
-
|
|
6812
|
-
|
|
6813
|
-
|
|
6814
|
-
const
|
|
6815
|
-
|
|
6816
|
-
|
|
6817
|
-
const param = new URLSearchParams(location.search).get(DESKTOP_PARAM);
|
|
6818
|
-
if (param === '1')
|
|
6819
|
-
localStorage.setItem(DESKTOP_KEY, '1');
|
|
6820
|
-
else if (param === '0')
|
|
6821
|
-
localStorage.removeItem(DESKTOP_KEY);
|
|
6822
|
-
return localStorage.getItem(DESKTOP_KEY) === '1';
|
|
7081
|
+
/** This plugin's two dismissible sheets (session-info, home workspace/chips
|
|
7082
|
+
* picker — MobileHome.tsx's home-sheet hosts both the workspace switcher and
|
|
7083
|
+
* the S5 chip-customize list under the same marker). Official popupSelect
|
|
7084
|
+
* menus (permission/model) are deliberately excluded — the design spec
|
|
7085
|
+
* leaves those mask-tap-only, and they are the suite's own DOM, not ours to
|
|
7086
|
+
* add gesture handling to. */
|
|
7087
|
+
const SHEET_SELECTOR = '[data-mobile-nav="info-sheet"], [data-mobile-nav="home-sheet"]';
|
|
7088
|
+
const CLOSE_DISTANCE = 80;
|
|
7089
|
+
/** px/ms — a fast flick closes the sheet even short of CLOSE_DISTANCE. */
|
|
7090
|
+
const CLOSE_VELOCITY = 0.5;
|
|
7091
|
+
/** Below this the drag is a tap/jitter, not yet a pull — avoids hijacking
|
|
7092
|
+
* the very first pixels of an upward scroll inside the sheet. */
|
|
7093
|
+
const DRAG_COMMIT_PX = 4;
|
|
7094
|
+
function prefersReducedMotion() {
|
|
7095
|
+
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
6823
7096
|
}
|
|
6824
|
-
/**
|
|
6825
|
-
|
|
6826
|
-
|
|
6827
|
-
|
|
6828
|
-
|
|
6829
|
-
|
|
6830
|
-
|
|
6831
|
-
|
|
6832
|
-
|
|
6833
|
-
}
|
|
6834
|
-
catch {
|
|
6835
|
-
return false;
|
|
6836
|
-
}
|
|
7097
|
+
/** The sheet's own mask sibling — both this plugin's sheets name theirs
|
|
7098
|
+
* with a "-mask" suffix (info-mask, home-sheet-mask) and already wire a
|
|
7099
|
+
* click handler that calls the sheet's close(). Reusing that handler (a
|
|
7100
|
+
* synthetic click) needs no access to the sheet's React state at all.
|
|
7101
|
+
* Shared with the S6.1 edge-swipe-back priority chain below — both "drag
|
|
7102
|
+
* down to dismiss" and "swipe back from the edge" close a sheet the exact
|
|
7103
|
+
* same way. */
|
|
7104
|
+
function closeSheet(sheet) {
|
|
7105
|
+
sheet.parentElement?.querySelector('[data-mobile-nav$="-mask"]')?.click();
|
|
6837
7106
|
}
|
|
6838
|
-
/** ChatView's flow column (dsh-client-ui-conversation lib/client.js:5512).
|
|
6839
|
-
* Trajectory and every other view render their own tree and never carry this
|
|
6840
|
-
* marker, so scoping here is what keeps this effect Chat-only. */
|
|
6841
|
-
const FLOW = '[data-chat-flow]';
|
|
6842
|
-
/** Marker + selector of the injected summary row. */
|
|
6843
|
-
const CHIP = 'turn-fold';
|
|
6844
|
-
const CHIP_SELECTOR = '[data-mobile-nav="turn-fold"]';
|
|
6845
|
-
/** Whole-row node kinds that are turn *process*, not conversation content.
|
|
6846
|
-
* Keys are ChatNodeSeat's own `data-chat-flow-kind` dispatch values
|
|
6847
|
-
* (registerChatNodeRenderers, lib/client.js:9322): tool calls, injected
|
|
6848
|
-
* context rows, and slash-command rows. Everything else stays visible —
|
|
6849
|
-
* `user`/`steering` (the question), `assistant-step` (the prose, folded
|
|
6850
|
-
* per-block below), `turn-tail` (the footer with its actions), and the
|
|
6851
|
-
* error notices `turn-error` / `turn-max-tokens` / `model-retry`, which are
|
|
6852
|
-
* exactly what a reader must not have to hunt for. */
|
|
6853
|
-
const PROCESS_KINDS = new Set(['context', 'tool-call', 'command']);
|
|
6854
|
-
/** Node kinds that open a new turn group. */
|
|
6855
|
-
const TURN_HEAD_KINDS = new Set(['user', 'steering']);
|
|
6856
|
-
/** ReasoningRow's own marker (lib/client.js:8966) — the Think disclosure
|
|
6857
|
-
* rendered inside an assistant-step row, beside that step's prose. */
|
|
6858
|
-
const THINK = '[data-variant="think"]';
|
|
6859
|
-
/** Attribute the phone stylesheet turns into `display: none`. */
|
|
6860
|
-
const FOLD = 'data-mnav-fold';
|
|
6861
|
-
/** Per-item override written while its turn is expanded. */
|
|
6862
|
-
const OPEN = 'data-mnav-fold-open';
|
|
6863
7107
|
/**
|
|
6864
|
-
*
|
|
6865
|
-
*
|
|
6866
|
-
*
|
|
6867
|
-
*
|
|
6868
|
-
*
|
|
6869
|
-
*
|
|
7108
|
+
* S6.2 — sheet drag-to-close: this plugin's two own bottom sheets (session
|
|
7109
|
+
* info, home workspace picker) support "drag down to dismiss", mirroring
|
|
7110
|
+
* the mask-tap close they already have. Official popupSelect menus
|
|
7111
|
+
* (permission/model) are excluded on purpose — the design spec leaves them
|
|
7112
|
+
* mask-tap-only, and they are the suite's own DOM, not ours to add gesture
|
|
7113
|
+
* handling to.
|
|
7114
|
+
*
|
|
7115
|
+
* touchmove is registered non-passive (unlike the edge-swipe-back gesture
|
|
7116
|
+
* below) because this gesture needs to preventDefault once it takes over, so
|
|
7117
|
+
* the sheet's own translateY tracks the finger instead of fighting the
|
|
7118
|
+
* scroller's native rubber-band. Before that handoff — while the sheet's
|
|
7119
|
+
* inner content isn't scrolled to top — nothing is intercepted at all, so
|
|
7120
|
+
* a pull down first scrolls the content as normal (spec requirement).
|
|
6870
7121
|
*/
|
|
6871
|
-
function
|
|
6872
|
-
|
|
6873
|
-
|
|
6874
|
-
|
|
6875
|
-
|
|
6876
|
-
|
|
6877
|
-
|
|
6878
|
-
|
|
6879
|
-
|
|
6880
|
-
|
|
6881
|
-
|
|
6882
|
-
|
|
6883
|
-
|
|
6884
|
-
|
|
6885
|
-
|
|
6886
|
-
|
|
6887
|
-
|
|
6888
|
-
|
|
6889
|
-
|
|
6890
|
-
|
|
6891
|
-
|
|
6892
|
-
|
|
6893
|
-
|
|
6894
|
-
|
|
6895
|
-
|
|
6896
|
-
|
|
6897
|
-
|
|
6898
|
-
|
|
6899
|
-
|
|
6900
|
-
|
|
6901
|
-
|
|
6902
|
-
|
|
7122
|
+
function installSheetDragClose(ctx) {
|
|
7123
|
+
ctx.effect(() => {
|
|
7124
|
+
const narrow = window.matchMedia(PHONE_QUERY);
|
|
7125
|
+
let drag = null;
|
|
7126
|
+
const settle = (sheet, close) => {
|
|
7127
|
+
if (close) {
|
|
7128
|
+
sheet.style.transform = '';
|
|
7129
|
+
sheet.style.transition = '';
|
|
7130
|
+
closeSheet(sheet);
|
|
7131
|
+
return;
|
|
7132
|
+
}
|
|
7133
|
+
if (prefersReducedMotion()) {
|
|
7134
|
+
sheet.style.transition = 'none';
|
|
7135
|
+
sheet.style.transform = '';
|
|
7136
|
+
return;
|
|
7137
|
+
}
|
|
7138
|
+
sheet.style.transition = 'transform .22s var(--ds-ease-out, ease-in-out)';
|
|
7139
|
+
sheet.style.transform = '';
|
|
7140
|
+
const clear = () => {
|
|
7141
|
+
sheet.style.transition = '';
|
|
7142
|
+
sheet.removeEventListener('transitionend', clear);
|
|
7143
|
+
};
|
|
7144
|
+
sheet.addEventListener('transitionend', clear);
|
|
7145
|
+
};
|
|
7146
|
+
const onTouchStart = (event) => {
|
|
7147
|
+
if (event.touches.length !== 1) {
|
|
7148
|
+
drag = null;
|
|
7149
|
+
return;
|
|
7150
|
+
}
|
|
7151
|
+
const touch = event.touches[0];
|
|
7152
|
+
const target = event.target;
|
|
7153
|
+
if (touch === undefined || !(target instanceof Element)) {
|
|
7154
|
+
drag = null;
|
|
7155
|
+
return;
|
|
7156
|
+
}
|
|
7157
|
+
const sheet = target.closest(SHEET_SELECTOR);
|
|
7158
|
+
if (sheet === null) {
|
|
7159
|
+
drag = null;
|
|
7160
|
+
return;
|
|
7161
|
+
}
|
|
7162
|
+
drag = { sheet, startY: touch.clientY, lastY: touch.clientY, startTime: Date.now(), dragging: false };
|
|
7163
|
+
};
|
|
7164
|
+
const onTouchMove = (event) => {
|
|
7165
|
+
if (drag === null)
|
|
7166
|
+
return;
|
|
7167
|
+
const touch = event.touches[0];
|
|
7168
|
+
if (touch === undefined)
|
|
7169
|
+
return;
|
|
7170
|
+
drag.lastY = touch.clientY;
|
|
7171
|
+
const dy = touch.clientY - drag.startY;
|
|
7172
|
+
if (!drag.dragging) {
|
|
7173
|
+
if (drag.sheet.scrollTop > 0)
|
|
7174
|
+
return;
|
|
7175
|
+
if (dy <= DRAG_COMMIT_PX)
|
|
7176
|
+
return;
|
|
7177
|
+
drag.dragging = true;
|
|
7178
|
+
drag.sheet.style.transition = 'none';
|
|
7179
|
+
}
|
|
7180
|
+
event.preventDefault();
|
|
7181
|
+
drag.sheet.style.transform = `translateY(${Math.max(0, dy)}px)`;
|
|
7182
|
+
};
|
|
7183
|
+
const finish = () => {
|
|
7184
|
+
if (drag === null)
|
|
7185
|
+
return;
|
|
7186
|
+
const { sheet, dragging, startY, lastY, startTime } = drag;
|
|
7187
|
+
drag = null;
|
|
7188
|
+
if (!dragging)
|
|
7189
|
+
return;
|
|
7190
|
+
const dy = Math.max(0, lastY - startY);
|
|
7191
|
+
const elapsed = Math.max(1, Date.now() - startTime);
|
|
7192
|
+
const velocity = dy / elapsed;
|
|
7193
|
+
settle(sheet, dy > CLOSE_DISTANCE || velocity > CLOSE_VELOCITY);
|
|
7194
|
+
};
|
|
7195
|
+
const attach = () => {
|
|
7196
|
+
document.addEventListener('touchstart', onTouchStart, { passive: true });
|
|
7197
|
+
document.addEventListener('touchmove', onTouchMove, { passive: false });
|
|
7198
|
+
document.addEventListener('touchend', finish, { passive: true });
|
|
7199
|
+
document.addEventListener('touchcancel', finish, { passive: true });
|
|
7200
|
+
};
|
|
7201
|
+
const detach = () => {
|
|
7202
|
+
document.removeEventListener('touchstart', onTouchStart);
|
|
7203
|
+
document.removeEventListener('touchmove', onTouchMove);
|
|
7204
|
+
document.removeEventListener('touchend', finish);
|
|
7205
|
+
document.removeEventListener('touchcancel', finish);
|
|
7206
|
+
drag = null;
|
|
7207
|
+
};
|
|
7208
|
+
if (narrow.matches)
|
|
7209
|
+
attach();
|
|
7210
|
+
const onChange = (event) => (event.matches ? attach() : detach());
|
|
7211
|
+
narrow.addEventListener('change', onChange);
|
|
7212
|
+
return () => {
|
|
7213
|
+
narrow.removeEventListener('change', onChange);
|
|
7214
|
+
detach();
|
|
7215
|
+
};
|
|
7216
|
+
}, 'dsh-mobile-nav: sheet drag-to-close');
|
|
6903
7217
|
}
|
|
7218
|
+
/** Left-edge start zone for S6.1's swipe-back, in CSS px from the viewport's
|
|
7219
|
+
* left edge. This used to be the gateway half's own edge-swipe-back hot zone
|
|
7220
|
+
* (history.back, useless against an SPA) — that gesture is removed in the
|
|
7221
|
+
* same revision that adds this one, so the 24px strip changes owner instead
|
|
7222
|
+
* of staying carved out (design doc "手势" row, 2026-08-17 fourth revision). */
|
|
7223
|
+
const EDGE_ZONE_PX = 24;
|
|
7224
|
+
const EDGE_SWIPE_MIN_DX = 90;
|
|
7225
|
+
const EDGE_SWIPE_RATIO = 1.6;
|
|
6904
7226
|
/**
|
|
6905
|
-
*
|
|
6906
|
-
*
|
|
6907
|
-
*
|
|
6908
|
-
*
|
|
7227
|
+
* dsh-better-sidebar's panel-open read (AGENTS.md pitfall "第三方浮层的开合
|
|
7228
|
+
* 态可以纯 CSS `:has()` 读"): the panel's class ends in "_panel" only while
|
|
7229
|
+
* open — "_panelHidden" is appended once closed, so the string no longer
|
|
7230
|
+
* ends in "_panel" and this selector stops matching. Same anchor
|
|
7231
|
+
* MobileSessionHeader.tsx's workbench button and its own close button use.
|
|
6909
7232
|
*/
|
|
6910
|
-
function
|
|
6911
|
-
|
|
6912
|
-
if (previous instanceof HTMLElement && previous.dataset['mobileNav'] === CHIP)
|
|
6913
|
-
return previous;
|
|
6914
|
-
const parent = anchor.parentElement;
|
|
6915
|
-
if (parent === null)
|
|
6916
|
-
return null;
|
|
6917
|
-
const chip = document.createElement('button');
|
|
6918
|
-
chip.type = 'button';
|
|
6919
|
-
chip.dataset['mobileNav'] = CHIP;
|
|
6920
|
-
parent.insertBefore(chip, anchor);
|
|
6921
|
-
return chip;
|
|
7233
|
+
function betterSidebarPanelOpen() {
|
|
7234
|
+
return document.querySelector('[data-dsh-better-sidebar] [class$="_panel"]') !== null;
|
|
6922
7235
|
}
|
|
6923
|
-
/**
|
|
6924
|
-
*
|
|
6925
|
-
* a
|
|
6926
|
-
*
|
|
6927
|
-
|
|
6928
|
-
|
|
6929
|
-
|
|
6930
|
-
|
|
6931
|
-
|
|
6932
|
-
|
|
6933
|
-
|
|
6934
|
-
|
|
6935
|
-
|
|
6936
|
-
|
|
6937
|
-
|
|
6938
|
-
|
|
7236
|
+
/**
|
|
7237
|
+
* S6.1 — left-edge swipe-back priority chain (design doc "手势" row, fourth
|
|
7238
|
+
* revision): a swipe starting in the left {@link EDGE_ZONE_PX} always closes
|
|
7239
|
+
* the TOPMOST dismissible surface rather than always navigating, so a user
|
|
7240
|
+
* mid-workflow (info card open, workbench panel open) gets "close that"
|
|
7241
|
+
* instead of "leave the session" from the same gesture. Checked in the
|
|
7242
|
+
* design doc's own order:
|
|
7243
|
+
* 1. this plugin's session-info sheet, if open
|
|
7244
|
+
* 2. this plugin's home sheet (workspace switcher or S5 chip-customize),
|
|
7245
|
+
* if open
|
|
7246
|
+
* 3. dsh-better-sidebar's workbench panel, if open (代点 its own toggle —
|
|
7247
|
+
* same anchor MobileSessionHeader.tsx's workbench/close buttons use)
|
|
7248
|
+
* 4. otherwise, GO_HOME_EVENT — but only from the session view; the design
|
|
7249
|
+
* doc calls out "在 home 层→无动作" explicitly, and the phone page
|
|
7250
|
+
* stack's current level is read straight off MobileHome's own
|
|
7251
|
+
* `data-view` attribute (no store handle needed here — effects/*.ts is
|
|
7252
|
+
* already all DOM reads, matching every sibling effect in this file).
|
|
7253
|
+
*/
|
|
7254
|
+
function handleEdgeSwipeBack() {
|
|
7255
|
+
const sheet = document.querySelector(SHEET_SELECTOR);
|
|
7256
|
+
if (sheet !== null) {
|
|
7257
|
+
closeSheet(sheet);
|
|
7258
|
+
return;
|
|
7259
|
+
}
|
|
7260
|
+
if (betterSidebarPanelOpen()) {
|
|
7261
|
+
document.querySelector('[data-dsh-better-sidebar] button[class$="_toggleButton"]')?.click();
|
|
7262
|
+
return;
|
|
7263
|
+
}
|
|
7264
|
+
if (document.querySelector('[data-mobile-nav="home"]')?.getAttribute('data-view') === 'session') {
|
|
7265
|
+
window.dispatchEvent(new CustomEvent(nav_store_ts_1.GO_HOME_EVENT));
|
|
6939
7266
|
}
|
|
6940
|
-
return groups;
|
|
6941
7267
|
}
|
|
6942
7268
|
/**
|
|
6943
|
-
*
|
|
6944
|
-
*
|
|
6945
|
-
*
|
|
6946
|
-
*
|
|
6947
|
-
*
|
|
6948
|
-
*
|
|
6949
|
-
* SHADOWS the official renderer (dsh-client-ui-slots index.d.ts:542 — the
|
|
6950
|
-
* cell's lowest-priority live entry renders, there is no wrapping form and
|
|
6951
|
-
* no `children` handle to the shadowed component). Taking `tool-call` would
|
|
6952
|
-
* mean re-implementing every tool's presentation, its `t` seat comes from
|
|
6953
|
-
* the entry's own declared locale namespace and its services from the
|
|
6954
|
-
* registrant's own inject face — neither is reachable from here — and the
|
|
6955
|
-
* registration is global, so the desktop no-op would be gone too.
|
|
6956
|
-
*
|
|
6957
|
-
* The DOM route needs no class names: ChatNodeSeat stamps every row with
|
|
6958
|
-
* `data-chat-flow-kind` / `data-chat-flow-key` (lib/client.js:5228) and
|
|
6959
|
-
* ReasoningRow stamps `data-variant="think"` — turn grouping is pure
|
|
6960
|
-
* structure (the run of rows following one `user` row), never text.
|
|
6961
|
-
*
|
|
6962
|
-
* ponytail: one rAF-coalesced full rescan per DOM mutation batch, O(rows).
|
|
6963
|
-
* If a very long session ever makes streaming feel heavy, narrow the
|
|
6964
|
-
* observer to the flow column's own childList and diff instead.
|
|
7269
|
+
* S6.1 — left-edge swipe-back gesture install. Passive throughout (never
|
|
7270
|
+
* calls preventDefault) and decided purely from the touchstart/touchend
|
|
7271
|
+
* endpoints, exactly like the S6.1-predecessor content-area swipe this
|
|
7272
|
+
* revision removes — so it never fights vertical (or, at the 24px strip,
|
|
7273
|
+
* horizontal chip-row) scrolling, and needs no mid-drag tracking: the design
|
|
7274
|
+
* doc explicitly allows "一步到位" over a followed-finger animation.
|
|
6965
7275
|
*/
|
|
6966
|
-
function
|
|
7276
|
+
function installEdgeSwipeBack(ctx) {
|
|
6967
7277
|
ctx.effect(() => {
|
|
6968
7278
|
const narrow = window.matchMedia(PHONE_QUERY);
|
|
6969
|
-
|
|
6970
|
-
|
|
6971
|
-
|
|
6972
|
-
|
|
6973
|
-
let observer = null;
|
|
6974
|
-
let frame = 0;
|
|
6975
|
-
const label = (chip, count, running) => {
|
|
6976
|
-
const next = running ? t('turnFoldRunning', { count }) : t('turnFold', { count });
|
|
6977
|
-
if (chip.textContent !== next)
|
|
6978
|
-
chip.textContent = next;
|
|
6979
|
-
};
|
|
6980
|
-
const scan = () => {
|
|
6981
|
-
const flow = document.querySelector(FLOW);
|
|
6982
|
-
if (flow === null)
|
|
7279
|
+
let start = null;
|
|
7280
|
+
const onTouchStart = (event) => {
|
|
7281
|
+
if (event.touches.length !== 1) {
|
|
7282
|
+
start = null;
|
|
6983
7283
|
return;
|
|
6984
|
-
// TurnStatus, ChatView's own running banner — present only while the
|
|
6985
|
-
// last turn is still working (lib/client.js:5548).
|
|
6986
|
-
const running = flow.querySelector(':scope > [role="status"]') !== null;
|
|
6987
|
-
const groups = groupsOf(flow);
|
|
6988
|
-
const live = new Set();
|
|
6989
|
-
const liveItems = new Set();
|
|
6990
|
-
groups.forEach((group, index) => {
|
|
6991
|
-
if (group.items.length === 0)
|
|
6992
|
-
return;
|
|
6993
|
-
const open = expanded.has(group.key);
|
|
6994
|
-
for (const item of group.items) {
|
|
6995
|
-
liveItems.add(item);
|
|
6996
|
-
item.setAttribute(FOLD, '');
|
|
6997
|
-
if (open)
|
|
6998
|
-
item.setAttribute(OPEN, '');
|
|
6999
|
-
else
|
|
7000
|
-
item.removeAttribute(OPEN);
|
|
7001
|
-
}
|
|
7002
|
-
const first = group.items[0];
|
|
7003
|
-
if (first === undefined)
|
|
7004
|
-
return;
|
|
7005
|
-
const anchor = first.closest('[data-chat-flow-key]') ?? first;
|
|
7006
|
-
const chip = chipBefore(anchor);
|
|
7007
|
-
if (chip === null)
|
|
7008
|
-
return;
|
|
7009
|
-
chip.dataset['foldGroup'] = group.key;
|
|
7010
|
-
chip.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
7011
|
-
if (open)
|
|
7012
|
-
chip.dataset['open'] = '';
|
|
7013
|
-
else
|
|
7014
|
-
delete chip.dataset['open'];
|
|
7015
|
-
const groupRunning = running && index === groups.length - 1;
|
|
7016
|
-
if (groupRunning)
|
|
7017
|
-
chip.dataset['running'] = '';
|
|
7018
|
-
else
|
|
7019
|
-
delete chip.dataset['running'];
|
|
7020
|
-
label(chip, group.items.length, groupRunning);
|
|
7021
|
-
live.add(group.key);
|
|
7022
|
-
});
|
|
7023
|
-
for (const chip of flow.querySelectorAll(CHIP_SELECTOR)) {
|
|
7024
|
-
const key = chip.dataset['foldGroup'];
|
|
7025
|
-
if (key === undefined || !live.has(key))
|
|
7026
|
-
chip.remove();
|
|
7027
7284
|
}
|
|
7028
|
-
|
|
7029
|
-
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
// completion re-render rebuilt the row — the "reply only appears when
|
|
7033
|
-
// the turn finishes" phone bug (2026-08-18).
|
|
7034
|
-
for (const stale of flow.querySelectorAll(`[${FOLD}]`)) {
|
|
7035
|
-
if (liveItems.has(stale))
|
|
7036
|
-
continue;
|
|
7037
|
-
stale.removeAttribute(FOLD);
|
|
7038
|
-
stale.removeAttribute(OPEN);
|
|
7285
|
+
const touch = event.touches[0];
|
|
7286
|
+
if (touch === undefined) {
|
|
7287
|
+
start = null;
|
|
7288
|
+
return;
|
|
7039
7289
|
}
|
|
7290
|
+
start = { x: touch.clientX, y: touch.clientY, eligible: touch.clientX <= EDGE_ZONE_PX };
|
|
7040
7291
|
};
|
|
7041
|
-
const
|
|
7042
|
-
|
|
7043
|
-
|
|
7044
|
-
|
|
7045
|
-
// attributes appeared at 1280px — invisible, since every rule that
|
|
7046
|
-
// hides anything is inside the phone media query, but still wrong).
|
|
7047
|
-
if (observer === null)
|
|
7048
|
-
return;
|
|
7049
|
-
if (frame !== 0)
|
|
7292
|
+
const onTouchEnd = (event) => {
|
|
7293
|
+
const state = start;
|
|
7294
|
+
start = null;
|
|
7295
|
+
if (state === null || !state.eligible)
|
|
7050
7296
|
return;
|
|
7051
|
-
|
|
7052
|
-
|
|
7053
|
-
scan();
|
|
7054
|
-
// Drop the records our own chip insertions just queued, so a rescan
|
|
7055
|
-
// can never re-trigger itself (the debug-badge feedback-loop freeze).
|
|
7056
|
-
observer?.takeRecords();
|
|
7057
|
-
});
|
|
7058
|
-
};
|
|
7059
|
-
const onClick = (event) => {
|
|
7060
|
-
const target = event.target;
|
|
7061
|
-
if (!(target instanceof Element))
|
|
7297
|
+
const touch = event.changedTouches[0];
|
|
7298
|
+
if (touch === undefined)
|
|
7062
7299
|
return;
|
|
7063
|
-
const
|
|
7064
|
-
const
|
|
7065
|
-
if (
|
|
7300
|
+
const dx = touch.clientX - state.x;
|
|
7301
|
+
const dy = touch.clientY - state.y;
|
|
7302
|
+
if (dx <= EDGE_SWIPE_MIN_DX || Math.abs(dx) <= EDGE_SWIPE_RATIO * Math.abs(dy))
|
|
7066
7303
|
return;
|
|
7067
|
-
|
|
7068
|
-
expanded.delete(key);
|
|
7069
|
-
else
|
|
7070
|
-
expanded.add(key);
|
|
7071
|
-
scan();
|
|
7072
|
-
};
|
|
7073
|
-
const clear = () => {
|
|
7074
|
-
for (const chip of document.querySelectorAll(CHIP_SELECTOR))
|
|
7075
|
-
chip.remove();
|
|
7076
|
-
for (const item of document.querySelectorAll(`[${FOLD}]`)) {
|
|
7077
|
-
item.removeAttribute(FOLD);
|
|
7078
|
-
item.removeAttribute(OPEN);
|
|
7079
|
-
}
|
|
7304
|
+
handleEdgeSwipeBack();
|
|
7080
7305
|
};
|
|
7081
7306
|
const attach = () => {
|
|
7082
|
-
|
|
7083
|
-
|
|
7084
|
-
observer = new MutationObserver(schedule);
|
|
7085
|
-
observer.observe(document.body, { childList: true, subtree: true });
|
|
7086
|
-
document.addEventListener('click', onClick);
|
|
7087
|
-
scan();
|
|
7307
|
+
document.addEventListener('touchstart', onTouchStart, { passive: true });
|
|
7308
|
+
document.addEventListener('touchend', onTouchEnd, { passive: true });
|
|
7088
7309
|
};
|
|
7089
7310
|
const detach = () => {
|
|
7090
|
-
|
|
7091
|
-
|
|
7092
|
-
|
|
7093
|
-
if (frame !== 0)
|
|
7094
|
-
cancelAnimationFrame(frame);
|
|
7095
|
-
frame = 0;
|
|
7096
|
-
expanded.clear();
|
|
7097
|
-
clear();
|
|
7098
|
-
};
|
|
7099
|
-
const onChange = (event) => (event.matches ? attach() : detach());
|
|
7100
|
-
let disposed = false;
|
|
7101
|
-
let desktop = false;
|
|
7102
|
-
/** Attach at every width and drop the breakpoint listener — the
|
|
7103
|
-
* stylesheet's html[DESKTOP_ATTR] rules take over from the phone media
|
|
7104
|
-
* query. Idempotent: the config answer and the local opt-in may both
|
|
7105
|
-
* ask for it. */
|
|
7106
|
-
const enableDesktop = () => {
|
|
7107
|
-
if (desktop || disposed)
|
|
7108
|
-
return;
|
|
7109
|
-
desktop = true;
|
|
7110
|
-
document.documentElement.setAttribute(DESKTOP_ATTR, '');
|
|
7111
|
-
narrow.removeEventListener('change', onChange);
|
|
7112
|
-
attach();
|
|
7311
|
+
document.removeEventListener('touchstart', onTouchStart);
|
|
7312
|
+
document.removeEventListener('touchend', onTouchEnd);
|
|
7313
|
+
start = null;
|
|
7113
7314
|
};
|
|
7114
7315
|
if (narrow.matches)
|
|
7115
7316
|
attach();
|
|
7317
|
+
const onChange = (event) => (event.matches ? attach() : detach());
|
|
7116
7318
|
narrow.addEventListener('change', onChange);
|
|
7117
|
-
if (desktopOptIn())
|
|
7118
|
-
enableDesktop();
|
|
7119
|
-
// The row config arrives async; a late enable just attaches then. Not
|
|
7120
|
-
// awaited before phone attach — folding must not wait on a round-trip.
|
|
7121
|
-
else
|
|
7122
|
-
void desktopConfigured().then((on) => (on ? enableDesktop() : undefined));
|
|
7123
|
-
// A language switch changes the chip copy of every already-rendered turn.
|
|
7124
|
-
const stopLocale = ctx.locale.subscribe(schedule);
|
|
7125
7319
|
return () => {
|
|
7126
|
-
disposed = true;
|
|
7127
|
-
document.documentElement.removeAttribute(DESKTOP_ATTR);
|
|
7128
7320
|
narrow.removeEventListener('change', onChange);
|
|
7129
|
-
stopLocale();
|
|
7130
7321
|
detach();
|
|
7131
7322
|
};
|
|
7132
|
-
}, 'dsh-mobile-nav:
|
|
7323
|
+
}, 'dsh-mobile-nav: left-edge swipe-back');
|
|
7324
|
+
}
|
|
7325
|
+
/** S6: the two-gesture set — left-edge swipe-back and sheet drag-to-close.
|
|
7326
|
+
* Both install/uninstall their own document listeners on the phone
|
|
7327
|
+
* breakpoint's matchMedia change, so at >= 768px this is a true no-op (no
|
|
7328
|
+
* listeners attached at all), matching every other effect in this file. */
|
|
7329
|
+
function installGestures(ctx) {
|
|
7330
|
+
installEdgeSwipeBack(ctx);
|
|
7331
|
+
installSheetDragClose(ctx);
|
|
7133
7332
|
}
|
|
7134
7333
|
};
|
|
7135
7334
|
__modules["effects/modal-back.js"] = function (require, module, exports) {
|
|
@@ -7433,6 +7632,7 @@ exports.estimatedLift = estimatedLift;
|
|
|
7433
7632
|
exports.keyboardLift = keyboardLift;
|
|
7434
7633
|
exports.composerLift = composerLift;
|
|
7435
7634
|
exports.installKeyboardAvoid = installKeyboardAvoid;
|
|
7635
|
+
const client_config_ts_1 = require("./client-config.js");
|
|
7436
7636
|
/** Phone breakpoint — same query every phone-only effect in this plugin uses. */
|
|
7437
7637
|
const PHONE_QUERY = '(max-width: 767px)';
|
|
7438
7638
|
/** Root CSS variable composer.css.ts turns into a composer lift. */
|
|
@@ -7455,13 +7655,18 @@ const MAX_SCALE = 1.01;
|
|
|
7455
7655
|
*
|
|
7456
7656
|
* ANDROID ONLY ({@link safetyPad}). The under-reporting is an Android IME
|
|
7457
7657
|
* behaviour; iOS has one system keyboard whose height WebKit reports exactly,
|
|
7458
|
-
* so padding there would be a visible gap bought for nothing.
|
|
7459
|
-
|
|
7460
|
-
|
|
7461
|
-
*
|
|
7462
|
-
*
|
|
7463
|
-
|
|
7464
|
-
|
|
7658
|
+
* so padding there would be a visible gap bought for nothing.
|
|
7659
|
+
*
|
|
7660
|
+
* The 15px default is what shipped; a device whose IME hides more of the
|
|
7661
|
+
* composer than that can raise it through the plugin row's
|
|
7662
|
+
* `config.keyboardSafetyPadPx` ({@link KeyboardTuning}). */
|
|
7663
|
+
const SAFETY_PAD_PX = client_config_ts_1.KEYBOARD_DEFAULTS.safetyPadPx;
|
|
7664
|
+
/** The safety pad this platform gets: the configured clearance, or nothing
|
|
7665
|
+
* off Android. UA sniffing is the right tool here — the target is a platform
|
|
7666
|
+
* defect, not a feature that could be detected.
|
|
7667
|
+
* @param padPx - the row's clearance; defaults to the shipped 15px. */
|
|
7668
|
+
function safetyPad(userAgent, padPx = SAFETY_PAD_PX) {
|
|
7669
|
+
return /Android/iu.test(userAgent) ? padPx : 0;
|
|
7465
7670
|
}
|
|
7466
7671
|
/** A viewport that lost at least this much against its no-keyboard baseline
|
|
7467
7672
|
* has a keyboard up. Comfortably above the ~56px a collapsing URL bar moves,
|
|
@@ -7488,11 +7693,16 @@ const PROBE_EPSILON_PX = 8;
|
|
|
7488
7693
|
* There is no signal to measure, so this is an estimate: the reporter's
|
|
7489
7694
|
* measured WeType is ~315 CSS px on a 858px viewport (~37%); 42% capped at
|
|
7490
7695
|
* 400px covers taller IME toolbars without stranding the composer mid-screen.
|
|
7491
|
-
*
|
|
7492
|
-
*
|
|
7696
|
+
*
|
|
7697
|
+
* Both numbers are a guess about someone else's hardware, so both are knobs:
|
|
7698
|
+
* `config.keyboardLiftRatio` / `config.keyboardLiftMaxPx` on the plugin row
|
|
7699
|
+
* ({@link KeyboardTuning}). The shipped pair stays the default — an install
|
|
7700
|
+
* that never touches them behaves exactly as before.
|
|
7701
|
+
*
|
|
7702
|
+
* @param tuning - the row's tuning; defaults to the shipped estimate.
|
|
7493
7703
|
*/
|
|
7494
|
-
function estimatedLift(innerHeight) {
|
|
7495
|
-
return Math.min(Math.round(innerHeight *
|
|
7704
|
+
function estimatedLift(innerHeight, tuning = client_config_ts_1.KEYBOARD_DEFAULTS) {
|
|
7705
|
+
return Math.min(Math.round(innerHeight * tuning.liftRatio), tuning.liftMaxPx);
|
|
7496
7706
|
}
|
|
7497
7707
|
/**
|
|
7498
7708
|
* How far the composer must rise so its bottom edge sits on the visual
|
|
@@ -7594,7 +7804,16 @@ function installKeyboardAvoid(ctx) {
|
|
|
7594
7804
|
* it. Re-read on every sync that finds the composer unfocused, so a
|
|
7595
7805
|
* rotation or a URL-bar change rebases it without extra listeners. */
|
|
7596
7806
|
let baseline = 0;
|
|
7597
|
-
|
|
7807
|
+
/** Row tuning for the estimate path. Starts on the shipped defaults and
|
|
7808
|
+
* is replaced once the config request lands (a few ms into the page, and
|
|
7809
|
+
* always long before a touch focus can raise a keyboard) — no re-sync is
|
|
7810
|
+
* forced, the next focus or viewport event picks the new values up. */
|
|
7811
|
+
let tuning = client_config_ts_1.KEYBOARD_DEFAULTS;
|
|
7812
|
+
let pad = safetyPad(navigator.userAgent);
|
|
7813
|
+
void (0, client_config_ts_1.clientConfig)().then((loaded) => {
|
|
7814
|
+
tuning = loaded.keyboard;
|
|
7815
|
+
pad = safetyPad(navigator.userAgent, tuning.safetyPadPx);
|
|
7816
|
+
});
|
|
7598
7817
|
const composerTextarea = (node) => node instanceof HTMLElement && node.tagName === 'TEXTAREA' && node.closest(COMPOSER) !== null;
|
|
7599
7818
|
const sync = () => {
|
|
7600
7819
|
const focused = composerTextarea(document.activeElement);
|
|
@@ -7677,7 +7896,7 @@ function installKeyboardAvoid(ctx) {
|
|
|
7677
7896
|
// the probe below still runs and revokes the verdict if the viewport
|
|
7678
7897
|
// turns out to react after all.
|
|
7679
7898
|
if (localStorage.getItem(DUMB_KEY) === '1') {
|
|
7680
|
-
estimate = estimatedLift(window.innerHeight);
|
|
7899
|
+
estimate = estimatedLift(window.innerHeight, tuning);
|
|
7681
7900
|
sync();
|
|
7682
7901
|
}
|
|
7683
7902
|
const h0 = viewport.height;
|
|
@@ -7709,7 +7928,7 @@ function installKeyboardAvoid(ctx) {
|
|
|
7709
7928
|
if (!composerTextarea(document.activeElement))
|
|
7710
7929
|
return;
|
|
7711
7930
|
localStorage.setItem(DUMB_KEY, '1');
|
|
7712
|
-
estimate = estimatedLift(window.innerHeight);
|
|
7931
|
+
estimate = estimatedLift(window.innerHeight, tuning);
|
|
7713
7932
|
sync();
|
|
7714
7933
|
};
|
|
7715
7934
|
probeTimer = window.setTimeout(step, PROBE_INTERVAL_MS);
|