dsh-zen-remote 2.3.8 → 2.3.10
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/CHANGELOG.md +41 -0
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/docs/interface.md +1 -1
- package/docs/remote-access.en.md +1 -1
- package/docs/remote-access.md +1 -1
- package/lib/adapters/dsh-better-sidebar/index.js +21 -3
- package/lib/adapters/dsh-better-sidebar/index.js.map +1 -1
- package/lib/client.js +522 -89
- package/lib/types/adapters/dsh-better-sidebar/index.d.ts +1 -1
- package/lib/types/adapters/dsh-better-sidebar/index.d.ts.map +1 -1
- package/lib/types/client/MobileHome.d.ts.map +1 -1
- package/lib/types/client/MobileNavOverlay.d.ts.map +1 -1
- package/lib/types/client/MobileSessionHeader.d.ts.map +1 -1
- package/lib/types/client/effects/native-trigger-overlay.d.ts.map +1 -1
- package/lib/types/client/effects/permission-sheet.d.ts +51 -10
- package/lib/types/client/effects/permission-sheet.d.ts.map +1 -1
- package/lib/types/client/locales.d.ts +1 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/settings/SettingsSection.d.ts.map +1 -1
- package/lib/types/client/styles/chips.css.d.ts +1 -1
- package/lib/types/client/styles/chips.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/header.css.d.ts +1 -1
- package/lib/types/client/styles/header.css.d.ts.map +1 -1
- package/lib/types/client/styles/home.css.d.ts +1 -1
- package/lib/types/client/styles/home.css.d.ts.map +1 -1
- package/lib/types/client/styles/layout.css.d.ts +1 -1
- package/lib/types/client/styles/layout.css.d.ts.map +1 -1
- package/lib/types/client/wizard/GatewayWizard.d.ts.map +1 -1
- package/lib/types/client-data/new-session.d.ts +26 -0
- package/lib/types/client-data/new-session.d.ts.map +1 -0
- package/lib/types/client-data/settings-form.d.ts +36 -0
- package/lib/types/client-data/settings-form.d.ts.map +1 -1
- package/package.json +1 -1
- package/scripts/check-settings-form.mjs +63 -0
- package/src/adapters/dsh-better-sidebar/index.ts +23 -3
- package/src/client/MobileHome.tsx +63 -8
- package/src/client/MobileNavOverlay.tsx +3 -1
- package/src/client/MobileSessionHeader.tsx +7 -1
- package/src/client/effects/native-trigger-overlay.ts +50 -11
- package/src/client/effects/permission-sheet.ts +199 -22
- package/src/client/locales.ts +2 -0
- package/src/client/settings/SettingsSection.tsx +43 -1
- package/src/client/styles/chips.css.ts +7 -2
- package/src/client/styles/composer.css.ts +24 -0
- package/src/client/styles/header.css.ts +58 -46
- package/src/client/styles/home.css.ts +6 -2
- package/src/client/styles/layout.css.ts +43 -2
- package/src/client/wizard/GatewayWizard.tsx +23 -1
- package/src/client-data/new-session.ts +30 -0
- package/src/client-data/settings-form.ts +57 -0
package/lib/client.js
CHANGED
|
@@ -181,7 +181,7 @@ function MobileNavOverlay({ toggleSidebar, t }) {
|
|
|
181
181
|
const target = event.target;
|
|
182
182
|
if (target === null)
|
|
183
183
|
return;
|
|
184
|
-
const drawer = document.querySelector('[data-mobile-nav="frame"] > :first-child');
|
|
184
|
+
const drawer = document.querySelector('[data-mobile-nav="frame"] > [class$="_sidebarCol"], [data-mobile-nav="frame"] > :first-child:not([data-sidebar-mask])');
|
|
185
185
|
if (drawer === null || !drawer.contains(target))
|
|
186
186
|
return;
|
|
187
187
|
// A session row's own action buttons — the "Session actions" kebab
|
|
@@ -754,6 +754,18 @@ function dotState(row, pending) {
|
|
|
754
754
|
return undefined;
|
|
755
755
|
}
|
|
756
756
|
};
|
|
757
|
+
__modules["client-data/new-session.js"] = function (require, module, exports) {
|
|
758
|
+
"use strict";
|
|
759
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
760
|
+
exports.decideNewSessionAction = decideNewSessionAction;
|
|
761
|
+
function decideNewSessionAction(selectedWorkspaceId, workspaceIds) {
|
|
762
|
+
if (selectedWorkspaceId !== undefined)
|
|
763
|
+
return { kind: 'start', workspaceId: selectedWorkspaceId };
|
|
764
|
+
if (workspaceIds.length <= 1)
|
|
765
|
+
return { kind: 'start', workspaceId: workspaceIds[0] };
|
|
766
|
+
return { kind: 'pick-workspace' };
|
|
767
|
+
}
|
|
768
|
+
};
|
|
757
769
|
__modules["client/chips-store.js"] = function (require, module, exports) {
|
|
758
770
|
"use strict";
|
|
759
771
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -1220,6 +1232,7 @@ const history_nav_ts_1 = require("./client/history-nav.js");
|
|
|
1220
1232
|
/** The phone shell's one page-stack step: session list -> session. */
|
|
1221
1233
|
const SESSION_LAYER = 'session';
|
|
1222
1234
|
const session_dot_ts_1 = require("./client/session-dot.js");
|
|
1235
|
+
const new_session_ts_1 = require("./client-data/new-session.js");
|
|
1223
1236
|
const MobileHomeChips_tsx_1 = require("./client/MobileHomeChips.js");
|
|
1224
1237
|
/** Phone breakpoint: below the tablet range, where the app-shell layout applies. */
|
|
1225
1238
|
const PHONE_QUERY = '(max-width: 767px)';
|
|
@@ -1514,7 +1527,26 @@ function MobileHome({ useSessions, useWorkspaces, useSessionPendingInteraction,
|
|
|
1514
1527
|
return;
|
|
1515
1528
|
}
|
|
1516
1529
|
frame.setAttribute('data-mobile-view', view);
|
|
1517
|
-
|
|
1530
|
+
// DSH 0.2.2-rc.1+ puts `inert=""` on `[class$="_sidebarCol"]` while
|
|
1531
|
+
// `drawer && sidebarCollapsed` (< 768px). Because home.css.ts already
|
|
1532
|
+
// hides sidebarCol with `display: none !important`, `inert` is redundant
|
|
1533
|
+
// for accessibility/tab order, but per HTML spec an `inert` ancestor
|
|
1534
|
+
// suppresses synthetic `.click()` (breaking openSettings and harvested
|
|
1535
|
+
// sidebar-footer chips) and blocks pointer events inside non-portal
|
|
1536
|
+
// sidebar dialogs. Strip it and keep it stripped while the phone shell
|
|
1537
|
+
// owns the viewport.
|
|
1538
|
+
const sidebarCol = frame.querySelector(':scope > [class$="_sidebarCol"]');
|
|
1539
|
+
const clearInert = () => {
|
|
1540
|
+
if (sidebarCol?.hasAttribute('inert'))
|
|
1541
|
+
sidebarCol.removeAttribute('inert');
|
|
1542
|
+
};
|
|
1543
|
+
clearInert();
|
|
1544
|
+
const observer = sidebarCol ? new MutationObserver(clearInert) : null;
|
|
1545
|
+
observer?.observe(sidebarCol, { attributes: true, attributeFilter: ['inert'] });
|
|
1546
|
+
return () => {
|
|
1547
|
+
observer?.disconnect();
|
|
1548
|
+
frame.removeAttribute('data-mobile-view');
|
|
1549
|
+
};
|
|
1518
1550
|
}, [phone, view]);
|
|
1519
1551
|
const enter = (start) => {
|
|
1520
1552
|
start();
|
|
@@ -1600,12 +1632,29 @@ function MobileHome({ useSessions, useWorkspaces, useSessionPendingInteraction,
|
|
|
1600
1632
|
const status = statusLine(row, dot, t);
|
|
1601
1633
|
const initial = row.displayTitle.trim().charAt(0).toUpperCase();
|
|
1602
1634
|
return ((0, jsx_runtime_1.jsx)(SwipeRow, { open: swipeOpen === row.id, onOpenChange: (open) => setSwipeOpen(open ? row.id : null), onArchive: () => onArchiveRow(row.id), archiveLabel: t('infoArchive'), children: (guardClick) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", "data-mobile-nav": "home-row", "data-current": row.id === current ? '' : undefined, onClick: () => guardClick(() => enter(() => openSession(row.id))), children: [(0, jsx_runtime_1.jsx)("span", { "data-mobile-nav": "home-row-avatar", "aria-hidden": "true", children: dot !== undefined ? (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.StateDot, { state: dot, size: 10 }) : initial }), (0, jsx_runtime_1.jsxs)("span", { "data-mobile-nav": "home-row-body", children: [(0, jsx_runtime_1.jsx)("span", { "data-mobile-nav": "home-row-title", children: row.displayTitle }), status !== undefined && ((0, jsx_runtime_1.jsx)("span", { "data-mobile-nav": "home-row-status", children: status }))] }), (0, jsx_runtime_1.jsx)("time", { "data-mobile-nav": "home-row-time", dateTime: new Date(row.updatedAt).toISOString(), children: relativeTime(row.updatedAt) })] })) }, row.id));
|
|
1603
|
-
}), rows.length === 0 && (0, jsx_runtime_1.jsx)("li", { "data-mobile-nav": "home-empty", children: t('noSessions') })] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", "data-mobile-nav": "home-fab", "aria-label": t('newSession'), title: t('newSession'), onClick: () =>
|
|
1635
|
+
}), rows.length === 0 && (0, jsx_runtime_1.jsx)("li", { "data-mobile-nav": "home-empty", children: t('noSessions') })] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", "data-mobile-nav": "home-fab", "aria-label": t('newSession'), title: t('newSession'), onClick: () => {
|
|
1636
|
+
// UX-3: never enter a dangling no-workspace session. With "all"
|
|
1637
|
+
// selected (the common case — the host's own sessions belong to no
|
|
1638
|
+
// workspace) the decision either auto-starts the only workspace or
|
|
1639
|
+
// opens the workspace drawer first; the session then starts with a
|
|
1640
|
+
// workspace attached from the first frame.
|
|
1641
|
+
const action = (0, new_session_ts_1.decideNewSessionAction)(selectedWorkspace?.workspaceId, workspaces.items.map((item) => item.workspaceId));
|
|
1642
|
+
if (action.kind === 'start')
|
|
1643
|
+
enter(() => startSession(action.workspaceId));
|
|
1644
|
+
else
|
|
1645
|
+
setSheet('new-session');
|
|
1646
|
+
}, children: [(0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.IconPlusOutlineRegular, { size: 18 }), (0, jsx_runtime_1.jsx)("span", { children: t('newSession') })] }), sheet !== null && ((0, jsx_runtime_1.jsxs)("div", { "data-mobile-nav": "home-sheet-layer", children: [(0, jsx_runtime_1.jsx)("div", { "data-mobile-nav": "home-sheet-mask", role: "button", tabIndex: -1, "aria-label": t('close'), onClick: () => setSheet(null) }), (0, jsx_runtime_1.jsxs)("div", { "data-mobile-nav": "home-sheet", role: sheet === 'chips' ? undefined : 'menu', children: [sheet === 'filter' && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("div", { "data-mobile-nav": "home-sheet-title", children: t('switchWorkspace') }), (0, jsx_runtime_1.jsx)("button", { type: "button", role: "menuitem", "data-mobile-nav": "home-sheet-item", "data-selected": selected === 'all' ? '' : undefined, onClick: () => {
|
|
1604
1647
|
actions.filter('all');
|
|
1605
1648
|
setSheet(null);
|
|
1606
1649
|
}, children: t('allWorkspaces') }), workspaces.items.map((item) => ((0, jsx_runtime_1.jsx)("button", { type: "button", role: "menuitem", "data-mobile-nav": "home-sheet-item", "data-selected": selected === item.workspaceId ? '' : undefined, onClick: () => {
|
|
1607
1650
|
actions.filter(item.workspaceId);
|
|
1608
1651
|
setSheet(null);
|
|
1652
|
+
}, children: item.title }, item.workspaceId)))] })), sheet === 'new-session' && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("div", { "data-mobile-nav": "home-sheet-title", children: t('pickWorkspaceNew') }), workspaces.items.map((item) => ((0, jsx_runtime_1.jsx)("button", { type: "button", role: "menuitem", "data-mobile-nav": "home-sheet-item", onClick: () => {
|
|
1653
|
+
// Pin the list to the picked workspace (so backing out
|
|
1654
|
+
// of the blank session lands on ITS list) and start the
|
|
1655
|
+
// session with that workspace from the first frame.
|
|
1656
|
+
actions.filter(item.workspaceId);
|
|
1657
|
+
enter(() => startSession(item.workspaceId));
|
|
1609
1658
|
}, children: item.title }, item.workspaceId)))] })), sheet === 'chips' && (0, jsx_runtime_1.jsx)(MobileHomeChips_tsx_1.MobileHomeChipsSheetBody, { t: t })] })] }))] })] }));
|
|
1610
1659
|
}
|
|
1611
1660
|
};
|
|
@@ -1665,7 +1714,13 @@ function IconTaskGlyph({ size = 14 }) {
|
|
|
1665
1714
|
* the root through `:has()` instead of naming it.
|
|
1666
1715
|
*/
|
|
1667
1716
|
const HEADER = '[data-phase] header';
|
|
1668
|
-
|
|
1717
|
+
/* UX-2 (2026-11-04): narrowed by the action-button class tail. The bare
|
|
1718
|
+
`button[aria-haspopup="tree"]` matched the breadcrumb's parent-session
|
|
1719
|
+
switcher first (`…_switcherTrigger` — also a tree popup), so both the
|
|
1720
|
+
overlay and this fallback check resolved to the wrong button. The suffix
|
|
1721
|
+
matcher is case-sensitive and `_switcherTrigger` does not end in
|
|
1722
|
+
`_trigger`. Must stay in lockstep with effects/native-trigger-overlay.ts. */
|
|
1723
|
+
const SUBAGENT_TRIGGER = `${HEADER} button[class$="_trigger"][aria-haspopup="tree"]`;
|
|
1669
1724
|
const JOBS_TRIGGER = `${HEADER} button[class$="_trigger"]:not([aria-haspopup])`;
|
|
1670
1725
|
/** One glanceable group: icon + count + state dot; opens the official popover. */
|
|
1671
1726
|
function ActivityPill({ kind, count, state, label, trigger, onFallback, Icon }) {
|
|
@@ -2072,6 +2127,8 @@ exports.deriveClientStatusView = deriveClientStatusView;
|
|
|
2072
2127
|
exports.clientStatusLineOf = clientStatusLineOf;
|
|
2073
2128
|
exports.normalizePairingCode = normalizePairingCode;
|
|
2074
2129
|
exports.shouldFollowServerDeviceName = shouldFollowServerDeviceName;
|
|
2130
|
+
exports.shouldReloadAfterClientActivation = shouldReloadAfterClientActivation;
|
|
2131
|
+
exports.awaitClientStatusReady = awaitClientStatusReady;
|
|
2075
2132
|
exports.createLatestGate = createLatestGate;
|
|
2076
2133
|
exports.savedRowRole = savedRowRole;
|
|
2077
2134
|
exports.settingsRoleOf = settingsRoleOf;
|
|
@@ -2273,6 +2330,37 @@ function normalizePairingCode(input) {
|
|
|
2273
2330
|
function shouldFollowServerDeviceName(deviceName, rowName, hasDraft) {
|
|
2274
2331
|
return deviceName !== '' && deviceName !== rowName && !hasDraft;
|
|
2275
2332
|
}
|
|
2333
|
+
function shouldReloadAfterClientActivation(input) {
|
|
2334
|
+
if (!input.savedOrPaired || !input.nowClientRole)
|
|
2335
|
+
return false;
|
|
2336
|
+
if (!input.wasEffectiveClient)
|
|
2337
|
+
return true;
|
|
2338
|
+
if (input.streamInterceptAlreadyActive === false)
|
|
2339
|
+
return true;
|
|
2340
|
+
return false;
|
|
2341
|
+
}
|
|
2342
|
+
/**
|
|
2343
|
+
* Poll {@link CLIENT_STATUS_ROUTE} until the backend answers ok — the bounded
|
|
2344
|
+
* wait before the post-activation reload, covering the restart-watcher's row
|
|
2345
|
+
* reload window (the route 404s / fails while the row restarts). Resolves
|
|
2346
|
+
* after `maxMs` regardless, so the reload always fires. Injectable fetch and
|
|
2347
|
+
* timings keep the check script fast; defaults are the page's 3 s / 250 ms.
|
|
2348
|
+
*/
|
|
2349
|
+
async function awaitClientStatusReady(fetchFn = fetch, maxMs = 3000, intervalMs = 250) {
|
|
2350
|
+
const deadline = Date.now() + maxMs;
|
|
2351
|
+
while (true) {
|
|
2352
|
+
try {
|
|
2353
|
+
if ((await fetchFn(exports.CLIENT_STATUS_ROUTE)).ok)
|
|
2354
|
+
return;
|
|
2355
|
+
}
|
|
2356
|
+
catch {
|
|
2357
|
+
// the row is mid-reload; keep polling
|
|
2358
|
+
}
|
|
2359
|
+
if (Date.now() >= deadline)
|
|
2360
|
+
return;
|
|
2361
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2276
2364
|
function createLatestGate() {
|
|
2277
2365
|
let current = 0;
|
|
2278
2366
|
return {
|
|
@@ -3063,6 +3151,7 @@ exports.zh = {
|
|
|
3063
3151
|
'previewExitFullscreen': '退出全屏',
|
|
3064
3152
|
'allWorkspaces': '全部',
|
|
3065
3153
|
'switchWorkspace': '切换工作区',
|
|
3154
|
+
'pickWorkspaceNew': '选择工作区开始',
|
|
3066
3155
|
'newSession': '新建会话',
|
|
3067
3156
|
'noSessions': '暂无会话,点击右下角加号新建',
|
|
3068
3157
|
'homeStatusOngoing': '运行中',
|
|
@@ -3586,6 +3675,7 @@ exports.en = {
|
|
|
3586
3675
|
'previewExitFullscreen': 'Exit fullscreen',
|
|
3587
3676
|
'allWorkspaces': 'All',
|
|
3588
3677
|
'switchWorkspace': 'Switch workspace',
|
|
3678
|
+
'pickWorkspaceNew': 'Pick a workspace to start',
|
|
3589
3679
|
'newSession': 'New session',
|
|
3590
3680
|
'noSessions': 'No sessions yet — tap + to start one',
|
|
3591
3681
|
'homeStatusOngoing': 'Running',
|
|
@@ -8113,6 +8203,20 @@ exports.LAYOUT_CSS = `/* ---------- mobile-only layout ---------- */
|
|
|
8113
8203
|
padding-top: var(--mnav-sat) !important;
|
|
8114
8204
|
}
|
|
8115
8205
|
|
|
8206
|
+
/* DSH 0.2.2-rc.1+: AppFrame pins centerCol to grid-area: 1/2 (column 2,
|
|
8207
|
+
which is 0px wide in our 1-column mobile grid) and inserts a
|
|
8208
|
+
[data-sidebar-mask] div before sidebarCol when viewport < 768. Pin
|
|
8209
|
+
centerCol to column 1 so it fills the 1fr track, hide the host mask
|
|
8210
|
+
(MobileNavOverlay renders its own backdrop on tablet, and MobileHome
|
|
8211
|
+
replaces the drawer on phone), and match sidebarCol by its stable class
|
|
8212
|
+
suffix alongside :first-child. */
|
|
8213
|
+
[data-mobile-nav="frame"] > [class$="_centerCol"] {
|
|
8214
|
+
grid-area: 1 / 1 !important;
|
|
8215
|
+
}
|
|
8216
|
+
[data-mobile-nav="frame"] > [data-sidebar-mask] {
|
|
8217
|
+
display: none !important;
|
|
8218
|
+
}
|
|
8219
|
+
|
|
8116
8220
|
/* The sidebar column (first grid child) becomes a left drawer. The drawer
|
|
8117
8221
|
hugs the sidebar content exactly (the wide sidebar carries an inline
|
|
8118
8222
|
width, ~280px): a fixed 92vw box would leave a white strip where the
|
|
@@ -8123,7 +8227,8 @@ exports.LAYOUT_CSS = `/* ---------- mobile-only layout ---------- */
|
|
|
8123
8227
|
before) left 14px of the drawer plus a long 32px-blur shadow gradient
|
|
8124
8228
|
visible along the left edge of the main UI. No box-shadow at all: the
|
|
8125
8229
|
dimmed backdrop already separates drawer from content. */
|
|
8126
|
-
[data-mobile-nav="frame"] > :first-child
|
|
8230
|
+
[data-mobile-nav="frame"] > :first-child,
|
|
8231
|
+
[data-mobile-nav="frame"] > [class$="_sidebarCol"] {
|
|
8127
8232
|
position: absolute !important;
|
|
8128
8233
|
inset: 0 auto 0 0 !important;
|
|
8129
8234
|
width: max-content !important;
|
|
@@ -8163,6 +8268,31 @@ exports.LAYOUT_CSS = `/* ---------- mobile-only layout ---------- */
|
|
|
8163
8268
|
padding-top: var(--mnav-sat) !important;
|
|
8164
8269
|
padding-bottom: var(--mnav-sab) !important;
|
|
8165
8270
|
}
|
|
8271
|
+
/* Right sidebar start/guide page (dsh-client-ui-sidebar-right GuideBody):
|
|
8272
|
+
the host sets .Dv5QdW_tabBody { height: 100%; overflow: hidden } and
|
|
8273
|
+
.F6Huca_guide { min-height: 100%; justify-content: center } with no
|
|
8274
|
+
overflow-y. When many tab entries are registered (e.g. better-sidebar +
|
|
8275
|
+
plugins), the centered flex column overflows equally out the top and
|
|
8276
|
+
bottom of the locked 100%-height box — clipping the leading entries
|
|
8277
|
+
("文件", "Git") above y=0 with no way to scroll. Make the guide itself
|
|
8278
|
+
the scroll container and replace justify-content:center with auto
|
|
8279
|
+
margins on the first item and ::after so short lists stay centered
|
|
8280
|
+
while long lists start cleanly at the top and scroll end-to-end. */
|
|
8281
|
+
[data-sidebar-right-guide] {
|
|
8282
|
+
flex: 1 1 0 !important;
|
|
8283
|
+
min-height: 0 !important;
|
|
8284
|
+
justify-content: flex-start !important;
|
|
8285
|
+
overflow-y: auto !important;
|
|
8286
|
+
-webkit-overflow-scrolling: touch;
|
|
8287
|
+
padding-top: 24px !important;
|
|
8288
|
+
padding-bottom: 24px !important;
|
|
8289
|
+
}
|
|
8290
|
+
[data-sidebar-right-guide] > :first-child {
|
|
8291
|
+
margin-top: auto !important;
|
|
8292
|
+
}
|
|
8293
|
+
[data-sidebar-right-guide]::after {
|
|
8294
|
+
margin-bottom: auto !important;
|
|
8295
|
+
}
|
|
8166
8296
|
|
|
8167
8297
|
/* Expanded state (frame without data-sidebar-collapsed) slides the drawer in.
|
|
8168
8298
|
The open state must be transform:none — NOT translateX(0): an identity
|
|
@@ -8173,7 +8303,8 @@ exports.LAYOUT_CSS = `/* ---------- mobile-only layout ---------- */
|
|
|
8173
8303
|
overflow:hidden drawer to scrollLeft=102, and every static child (plus the
|
|
8174
8304
|
fixed overlay) shifts 102px off-screen. With transform:none the overlay is
|
|
8175
8305
|
viewport-anchored: it dims the full screen and the sheet sits at left:8. */
|
|
8176
|
-
[data-mobile-nav="frame"]:not([data-sidebar-collapsed]) > :first-child
|
|
8306
|
+
[data-mobile-nav="frame"]:not([data-sidebar-collapsed]) > :first-child,
|
|
8307
|
+
[data-mobile-nav="frame"]:not([data-sidebar-collapsed]) > [class$="_sidebarCol"] {
|
|
8177
8308
|
transform: none !important;
|
|
8178
8309
|
}
|
|
8179
8310
|
|
|
@@ -9907,8 +10038,12 @@ exports.HOME_CSS = `/* ---------- phone app shell (< 768px) ---------- */
|
|
|
9907
10038
|
|
|
9908
10039
|
/* The official sidebar is no longer a drawer on a phone — the home screen
|
|
9909
10040
|
replaced it. Hidden outright (not translated off-screen) so it cannot
|
|
9910
|
-
capture taps or hold layout.
|
|
9911
|
-
|
|
10041
|
+
capture taps or hold layout. DSH 0.2.2-rc.1+ inserts [data-sidebar-mask]
|
|
10042
|
+
before [class$="_sidebarCol"] and mounts [data-shell-leading] (the host's
|
|
10043
|
+
own floating drawer opener) on < 768px; hide all three. */
|
|
10044
|
+
[data-mobile-nav="frame"] > :first-child,
|
|
10045
|
+
[data-mobile-nav="frame"] > [class$="_sidebarCol"],
|
|
10046
|
+
[data-mobile-nav="frame"] > [data-shell-leading] {
|
|
9912
10047
|
display: none !important;
|
|
9913
10048
|
}
|
|
9914
10049
|
/* The header's drawer toggle opened that sidebar; with it gone the button
|
|
@@ -10570,6 +10705,14 @@ exports.HEADER_CSS = `/* ---------- session header five-piece reflow (< 768px) -
|
|
|
10570
10705
|
instead of centered. Pinning the row makes it one 48px band again. */
|
|
10571
10706
|
[data-phase] header [class$="_titleRow"] {
|
|
10572
10707
|
position: relative;
|
|
10708
|
+
/* UX-2 (2026-11-04): the host gives this row container-type: inline-size,
|
|
10709
|
+
which implies layout containment — making the row the containing block
|
|
10710
|
+
for every descendant position:fixed. That shifted every viewport
|
|
10711
|
+
coordinate effects/native-trigger-overlay.ts writes by the safe-area
|
|
10712
|
+
top and the row's horizontal padding. normal hands fixed descendants
|
|
10713
|
+
back to the viewport; the phone block owns this layout, so nothing
|
|
10714
|
+
scoped container queries against the row here. */
|
|
10715
|
+
container-type: normal !important;
|
|
10573
10716
|
display: grid;
|
|
10574
10717
|
grid-template-columns: 92px 1fr 92px;
|
|
10575
10718
|
grid-template-rows: minmax(48px, auto);
|
|
@@ -10637,55 +10780,30 @@ exports.HEADER_CSS = `/* ---------- session header five-piece reflow (< 768px) -
|
|
|
10637
10780
|
min-width: 0;
|
|
10638
10781
|
overflow: hidden;
|
|
10639
10782
|
}
|
|
10640
|
-
/* Subagent sessions
|
|
10641
|
-
|
|
10642
|
-
|
|
10643
|
-
|
|
10644
|
-
|
|
10645
|
-
|
|
10646
|
-
|
|
10647
|
-
|
|
10648
|
-
|
|
10649
|
-
all — the centered single title is pixel-identical to before. */
|
|
10650
|
-
[data-phase] header [class$="_crumbSeg"]:not(:nth-last-child(-n+2)) {
|
|
10783
|
+
/* Subagent sessions hide every ancestor crumb (UX-2, 2026-11-04 —
|
|
10784
|
+
supersedes the 2026-08-18 "keep ONE ancestor" rule). DSH 0.2.1 turned
|
|
10785
|
+
the parent crumb into a session-TREE popup trigger
|
|
10786
|
+
(…_switcherTrigger[aria-haspopup="tree"]) instead of a direct
|
|
10787
|
+
switch-back tap, and that popup is desktop geometry on the phone; the
|
|
10788
|
+
back button and the home stack stay the way back. Main sessions render
|
|
10789
|
+
a single span with no segs at all, so :not(:last-child) never matches
|
|
10790
|
+
there. */
|
|
10791
|
+
[data-phase] header [class$="_crumbSeg"]:not(:last-child) {
|
|
10651
10792
|
display: none !important;
|
|
10652
10793
|
}
|
|
10653
|
-
|
|
10654
|
-
|
|
10655
|
-
|
|
10656
|
-
|
|
10657
|
-
|
|
10658
|
-
max-width: 38%;
|
|
10659
|
-
}
|
|
10660
|
-
/* Ancestors BEFORE the parent seg are hidden, which would leave the
|
|
10661
|
-
parent seg's own leading "/" dangling at the start of the title. */
|
|
10662
|
-
[data-phase] header [class$="_crumbSeg"]:nth-last-child(2) [class$="_crumbSep"] {
|
|
10794
|
+
/* With the parent seg gone, the current seg's own leading "/" would
|
|
10795
|
+
dangle before the title; the dissolved trigger container's stray
|
|
10796
|
+
separator is covered by this same rule (its slot rules below hide it
|
|
10797
|
+
too). One blanket rule instead of per-segment cases. */
|
|
10798
|
+
[data-phase] header [class$="_crumbSep"] {
|
|
10663
10799
|
display: none !important;
|
|
10664
10800
|
}
|
|
10665
|
-
/* Parent crumb: small tertiary text so the 19px current title stays the
|
|
10666
|
-
visual anchor; min-height keeps a finger-sized target inside the 48px
|
|
10667
|
-
row. Specificity (0,4,1) outranks the shared 19px _crumb rule below. */
|
|
10668
|
-
[data-phase] header [class$="_crumbSeg"]:nth-last-child(2) [class$="_crumb"] {
|
|
10669
|
-
font-size: 13px;
|
|
10670
|
-
font-weight: 400;
|
|
10671
|
-
color: var(--dsw-alias-label-tertiary, rgba(0, 0, 0, .45));
|
|
10672
|
-
min-height: 44px;
|
|
10673
|
-
display: inline-flex;
|
|
10674
|
-
align-items: center;
|
|
10675
|
-
padding: 0 2px 0 6px;
|
|
10676
|
-
touch-action: manipulation;
|
|
10677
|
-
-webkit-tap-highlight-color: transparent;
|
|
10678
|
-
}
|
|
10679
10801
|
[data-phase] header [class$="_crumbSeg"]:last-child {
|
|
10680
10802
|
display: flex;
|
|
10681
10803
|
align-items: center;
|
|
10682
10804
|
min-width: 0;
|
|
10683
10805
|
max-width: 100%;
|
|
10684
10806
|
}
|
|
10685
|
-
/* The last seg's own "/" is the divider between parent and current title —
|
|
10686
|
-
it only exists when there IS an ancestor (index > 0), so it can simply
|
|
10687
|
-
stay visible now that the parent seg shows. (It used to be hidden here:
|
|
10688
|
-
with the parent hidden it dangled before the title.) */
|
|
10689
10807
|
/* BOTH selectors are required: [class$=] matches the whole class
|
|
10690
10808
|
ATTRIBUTE's suffix, and the current-session title button's attribute is
|
|
10691
10809
|
"wSkVaW_crumb wSkVaW_crumbCurrent" — it ends in _crumbCurrent, so the _crumb
|
|
@@ -10795,12 +10913,17 @@ exports.HEADER_CSS = `/* ---------- session header five-piece reflow (< 768px) -
|
|
|
10795
10913
|
pointer-events on the trigger: that is exactly what would take the tap
|
|
10796
10914
|
target away again.
|
|
10797
10915
|
|
|
10798
|
-
CSS keeps only
|
|
10799
|
-
|
|
10800
|
-
|
|
10801
|
-
|
|
10802
|
-
|
|
10803
|
-
|
|
10916
|
+
CSS keeps only the resting dissolve: the root's box (and its stray
|
|
10917
|
+
separator, hidden below) stops occupying the breadcrumb until the
|
|
10918
|
+
overlay pins it. The measurement chore moved to the overlay (UX-2,
|
|
10919
|
+
2026-11-04): the 0.1.1+ popover portals to <body> as position:fixed
|
|
10920
|
+
and sizes itself from the OUTER anchor container (the official
|
|
10921
|
+
rootRef, the trigger's parent div) — a display:contents root has no
|
|
10922
|
+
box, so rootRef.getBoundingClientRect() was all zeros and the panel
|
|
10923
|
+
landed at 12,12 inside the status bar. effects/
|
|
10924
|
+
native-trigger-overlay.ts now pins container + trigger to the pill's
|
|
10925
|
+
rect together (its inline display beats this rule's !important, style
|
|
10926
|
+
attribute outranks selectors at author-important). */
|
|
10804
10927
|
[data-phase] header [data-slot="conversation.session.header.actions"] > *:has(> button[class$="_trigger"]),
|
|
10805
10928
|
[data-phase] header [data-slot="conversation.session.header.lineage"] > *:has(> button[class$="_trigger"]) {
|
|
10806
10929
|
display: contents !important;
|
|
@@ -10809,6 +10932,18 @@ exports.HEADER_CSS = `/* ---------- session header five-piece reflow (< 768px) -
|
|
|
10809
10932
|
[data-phase] header [data-slot="conversation.session.header.lineage"] [class$="_separator"] {
|
|
10810
10933
|
display: none !important;
|
|
10811
10934
|
}
|
|
10935
|
+
/* The popovers themselves portal to <body> as position:fixed layers
|
|
10936
|
+
(class suffix _menu, z-index 100) and place themselves from the pill's
|
|
10937
|
+
viewport rect with margin 12 — the overlay above is what makes that
|
|
10938
|
+
rect correct. This is only a floor: the panels' 336px width overflows
|
|
10939
|
+
320px-class phones, and a tall list must not run under the notch or
|
|
10940
|
+
the home indicator. [id$="-menu"] keeps the composer's model menu out
|
|
10941
|
+
of it — composer.css.ts owns that surface, and this file sits before
|
|
10942
|
+
it in styles/index.ts so its own rules still win the tie. */
|
|
10943
|
+
body > [class$="_menu"]:not([id$="-menu"]) {
|
|
10944
|
+
max-width: calc(100vw - 24px) !important;
|
|
10945
|
+
max-height: calc(100dvh - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px) - 24px) !important;
|
|
10946
|
+
}
|
|
10812
10947
|
/* The agent-preset mode badge specifically survives the blanket hide
|
|
10813
10948
|
above: layout.css.ts's shared <=1023px block targets it directly
|
|
10814
10949
|
(header [class$="_label"]:has(> svg) { display: block !important }),
|
|
@@ -10837,8 +10972,12 @@ exports.HEADER_CSS = `/* ---------- session header five-piece reflow (< 768px) -
|
|
|
10837
10972
|
display: inline-flex !important;
|
|
10838
10973
|
}
|
|
10839
10974
|
|
|
10840
|
-
/* Back button: 44pt touch target, leftmost column.
|
|
10975
|
+
/* Back button: 44pt touch target, leftmost column.
|
|
10976
|
+
order: -1 keeps it to the left of @deepseek-ai/dsh-experimental-client-ui-agent-team's
|
|
10977
|
+
TeamAction ([data-team-action], registered into header.actions at order -20 with
|
|
10978
|
+
default flex order 0). */
|
|
10841
10979
|
[data-mobile-nav="header-back"] {
|
|
10980
|
+
order: -1;
|
|
10842
10981
|
display: inline-flex;
|
|
10843
10982
|
align-items: center;
|
|
10844
10983
|
justify-content: center;
|
|
@@ -10855,6 +10994,13 @@ exports.HEADER_CSS = `/* ---------- session header five-piece reflow (< 768px) -
|
|
|
10855
10994
|
[data-mobile-nav="header-back"]:active {
|
|
10856
10995
|
opacity: .6;
|
|
10857
10996
|
}
|
|
10997
|
+
/* TeamAction's own stylesheet hides .XdyUwa_triggerLabel via
|
|
10998
|
+
@container (width<=480px) against _titleRow's container-type: inline-size.
|
|
10999
|
+
Shedding container-type on _titleRow above (UX-2) disabled that container
|
|
11000
|
+
query, so hide the label explicitly on phone and keep only the icon. */
|
|
11001
|
+
[data-phase] header [data-team-action] [class$="_triggerLabel"] {
|
|
11002
|
+
display: none !important;
|
|
11003
|
+
}
|
|
10858
11004
|
|
|
10859
11005
|
/* Info + workbench utility buttons, rightmost column. */
|
|
10860
11006
|
[data-mobile-nav="header-info"],
|
|
@@ -10945,6 +11091,7 @@ exports.HEADER_CSS = `/* ---------- session header five-piece reflow (< 768px) -
|
|
|
10945
11091
|
top:100% lands both on the same 28px line. z-index puts it above the
|
|
10946
11092
|
row it overlaps. */
|
|
10947
11093
|
[data-mobile-nav="header-activity"] {
|
|
11094
|
+
pointer-events: none;
|
|
10948
11095
|
position: absolute;
|
|
10949
11096
|
right: 0;
|
|
10950
11097
|
top: 100%;
|
|
@@ -11406,6 +11553,12 @@ exports.COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- */
|
|
|
11406
11553
|
…, open-in-app, deliverables, …), and none opens through this trigger
|
|
11407
11554
|
while no marked menu is up. */
|
|
11408
11555
|
body > div[role="menu"][data-zen-sheet="perm"],
|
|
11556
|
+
/* UX-3: the hero page's workspace menu, marked by the same effect through
|
|
11557
|
+
its own two triggers (the WorkspaceChip and the placeholder card) with
|
|
11558
|
+
the workspace value — a distinct marker so the pick that starts the
|
|
11559
|
+
workspace switch (effects/permission-sheet.ts) can key on THIS sheet
|
|
11560
|
+
without ever touching permission options. */
|
|
11561
|
+
body > div[role="menu"][data-zen-sheet="workspace"],
|
|
11409
11562
|
${MODEL_MENU} {
|
|
11410
11563
|
position: fixed !important;
|
|
11411
11564
|
left: 0 !important;
|
|
@@ -11438,6 +11591,7 @@ exports.COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- */
|
|
|
11438
11591
|
older hosts. */
|
|
11439
11592
|
${PERM} [role="menu"] [role="menuitem"],
|
|
11440
11593
|
body > div[role="menu"][data-zen-sheet="perm"] [role="menuitem"],
|
|
11594
|
+
body > div[role="menu"][data-zen-sheet="workspace"] [role="menuitem"],
|
|
11441
11595
|
${MODEL_MENU} [role="menuitem"],
|
|
11442
11596
|
${MODEL_MENU} [role="menuitemradio"],
|
|
11443
11597
|
${MODEL_MENU} [class$="_option"],
|
|
@@ -11586,11 +11740,28 @@ exports.COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- */
|
|
|
11586
11740
|
body:has(> [id$="-menu"][class$="_menu"]:is([role="menu"], [role="group"])) [data-chat-flow] + div,
|
|
11587
11741
|
/* T70: the portaled permission menu (marked by effects/permission-sheet.ts). */
|
|
11588
11742
|
body:has(> div[role="menu"][data-zen-sheet="perm"]) [data-chat-flow] + div,
|
|
11743
|
+
/* UX-3: the portaled hero workspace menu, same treatment. */
|
|
11744
|
+
body:has(> div[role="menu"][data-zen-sheet="workspace"]) [data-chat-flow] + div,
|
|
11589
11745
|
/* T-issue30: an inline menu a parked chip opened (escaped to a fixed
|
|
11590
11746
|
sheet by 4a). */
|
|
11591
11747
|
body:has([data-zen-sheet-extras] [role="menu"]) [data-chat-flow] + div {
|
|
11592
11748
|
display: none !important;
|
|
11593
11749
|
}
|
|
11750
|
+
/* --- 4c. UX-3: the workspace switch lock ---
|
|
11751
|
+
Between picking a workspace in the hero sheet and the new blank session
|
|
11752
|
+
landing (selectWorkspace → POST /api/session/create → replaceMain
|
|
11753
|
+
remounts the conversation), the composer still belongs to the OLD blank
|
|
11754
|
+
session: a message sent in that window lands there and is then wiped by
|
|
11755
|
+
the remount, bouncing the UI back to the "choose a workspace" state.
|
|
11756
|
+
effects/permission-sheet.ts stamps data-zen-ws-switching on <html> for
|
|
11757
|
+
exactly that window — the lock releases when the conversation root's
|
|
11758
|
+
data-conversation-session has moved and the placeholder card is gone,
|
|
11759
|
+
with a 1.5 s cap — and the dim (not just pointer-events) makes the
|
|
11760
|
+
freeze visible instead of reading as a dead input. */
|
|
11761
|
+
html[data-zen-ws-switching] [data-slot="conversation.composer.bar"] {
|
|
11762
|
+
pointer-events: none !important;
|
|
11763
|
+
opacity: .6 !important;
|
|
11764
|
+
}
|
|
11594
11765
|
/* --- 5. input box: two lines minimum, five lines maximum ---
|
|
11595
11766
|
The official autosizer drives the box off the hidden mirror's height, so
|
|
11596
11767
|
a min-height on the mirror IS the min-height of the field; the scroll cap
|
|
@@ -13244,12 +13415,17 @@ exports.CHIPS_CSS = `/* ---------- chips row + settings entry (S5) ---------- */
|
|
|
13244
13415
|
logoRow/newSession/regionArea at \`display: flex\`, which is only
|
|
13245
13416
|
hidden from view by being fully underneath the usage-stats panel's
|
|
13246
13417
|
opaque body, not by this rule. */
|
|
13247
|
-
[data-mobile-nav="frame"] > :first-child:has([aria-modal="true"], [data-usage-stats-panel], .dshst-overlay)
|
|
13418
|
+
[data-mobile-nav="frame"] > :first-child:has([aria-modal="true"], [data-usage-stats-panel], .dshst-overlay),
|
|
13419
|
+
[data-mobile-nav="frame"] > [class$="_sidebarCol"]:has([aria-modal="true"], [data-usage-stats-panel], .dshst-overlay) {
|
|
13248
13420
|
display: contents !important;
|
|
13421
|
+
visibility: visible !important;
|
|
13249
13422
|
}
|
|
13250
13423
|
[data-mobile-nav="frame"] > :first-child:has([aria-modal="true"], [data-usage-stats-panel], .dshst-overlay) [class$="_logoRow"],
|
|
13251
13424
|
[data-mobile-nav="frame"] > :first-child:has([aria-modal="true"], [data-usage-stats-panel], .dshst-overlay) [class$="_newSession"],
|
|
13252
|
-
[data-mobile-nav="frame"] > :first-child:has([aria-modal="true"], [data-usage-stats-panel], .dshst-overlay) [class$="_regionArea"]
|
|
13425
|
+
[data-mobile-nav="frame"] > :first-child:has([aria-modal="true"], [data-usage-stats-panel], .dshst-overlay) [class$="_regionArea"],
|
|
13426
|
+
[data-mobile-nav="frame"] > [class$="_sidebarCol"]:has([aria-modal="true"], [data-usage-stats-panel], .dshst-overlay) [class$="_logoRow"],
|
|
13427
|
+
[data-mobile-nav="frame"] > [class$="_sidebarCol"]:has([aria-modal="true"], [data-usage-stats-panel], .dshst-overlay) [class$="_newSession"],
|
|
13428
|
+
[data-mobile-nav="frame"] > [class$="_sidebarCol"]:has([aria-modal="true"], [data-usage-stats-panel], .dshst-overlay) [class$="_regionArea"] {
|
|
13253
13429
|
display: none !important;
|
|
13254
13430
|
}
|
|
13255
13431
|
|
|
@@ -15112,7 +15288,10 @@ __modules["client/effects/permission-sheet.js"] = function (require, module, exp
|
|
|
15112
15288
|
"use strict";
|
|
15113
15289
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15114
15290
|
exports.isUnmarkedPortalMenu = isUnmarkedPortalMenu;
|
|
15115
|
-
exports.
|
|
15291
|
+
exports.sheetKindForTrigger = sheetKindForTrigger;
|
|
15292
|
+
exports.pointerdownOnWorkspaceTrigger = pointerdownOnWorkspaceTrigger;
|
|
15293
|
+
exports.menuitemClickStartsWorkspaceSwitch = menuitemClickStartsWorkspaceSwitch;
|
|
15294
|
+
exports.workspaceSwitchSettled = workspaceSwitchSettled;
|
|
15116
15295
|
exports.installPermissionSheet = installPermissionSheet;
|
|
15117
15296
|
/** Phone breakpoint — the same query every phone-only effect in this plugin uses. */
|
|
15118
15297
|
const PHONE_QUERY = '(max-width: 767px)';
|
|
@@ -15123,7 +15302,18 @@ const PHONE_QUERY = '(max-width: 767px)';
|
|
|
15123
15302
|
* NO aria-controls on 0.2.0 (the Menu primitive portals its list to body
|
|
15124
15303
|
* and links nothing back), so the click is the only open signal there is.
|
|
15125
15304
|
*/
|
|
15126
|
-
const
|
|
15305
|
+
const PERM_TRIGGER_SELECTOR = '[data-slot="conversation.input.permission"] button[class$="_trigger"]';
|
|
15306
|
+
/**
|
|
15307
|
+
* UX-3: the HERO page's two workspace triggers (WorkspaceSheet slot impl,
|
|
15308
|
+
* verified in the dsh-client-ui-conversation bundle 2026-11-05):
|
|
15309
|
+
* - `WorkspaceChip` — a bare `button` whose hashed class ends in
|
|
15310
|
+
* `_workspace`, carrying `aria-haspopup="menu"` + `aria-expanded`;
|
|
15311
|
+
* - the placeholder card — when a blank hero session has no workspace yet
|
|
15312
|
+
* the whole `div[data-composer-card]` carries `cardWorkspaceTrigger` and
|
|
15313
|
+
* its click opens the same menu (`onRequestWorkspace`).
|
|
15314
|
+
* Both open a `body > div[role="menu"]` of `button[role="menuitem"]` items.
|
|
15315
|
+
*/
|
|
15316
|
+
const WORKSPACE_TRIGGER_SELECTOR = '[class$="_workspace"][aria-haspopup="menu"], [data-composer-card][class*="_cardWorkspaceTrigger"]';
|
|
15127
15317
|
/**
|
|
15128
15318
|
* What the Menu primitive portals on 0.2.0: a `role=menu` div DIRECTLY under
|
|
15129
15319
|
* body, carrying `data-menu-material` and a randomized `--dsh-menu-anchor`
|
|
@@ -15132,15 +15322,36 @@ const TRIGGER_SELECTOR = '[data-slot="conversation.input.permission"] button[cla
|
|
|
15132
15322
|
* host has MANY `portal: true` Menus (agent-preset, chat preference rows,
|
|
15133
15323
|
* the Enter-behavior row, the workspace sidebar's 「…」, open-in-app,
|
|
15134
15324
|
* deliverables, the copy button's confirm, …), and none of them may become
|
|
15135
|
-
* a bottom sheet. That is why this effect exists: a click on
|
|
15136
|
-
*
|
|
15137
|
-
*
|
|
15138
|
-
*
|
|
15325
|
+
* a bottom sheet. That is why this effect exists: a click on a sheet trigger
|
|
15326
|
+
* arms a short window and the next body-level menu to land is OURS to mark —
|
|
15327
|
+
* while a marked menu still on the page (the close click) keeps the window
|
|
15328
|
+
* shut.
|
|
15139
15329
|
*/
|
|
15140
15330
|
const PORTAL_MENU_SELECTOR = 'body > div[role="menu"]';
|
|
15141
15331
|
/** The marker composer.css.ts section 4 styles into the bottom sheet. */
|
|
15142
15332
|
const SHEET_MARKER = 'data-zen-sheet';
|
|
15143
|
-
const
|
|
15333
|
+
const SHEET_VALUE_PERM = 'perm';
|
|
15334
|
+
const SHEET_VALUE_WORKSPACE = 'workspace';
|
|
15335
|
+
/**
|
|
15336
|
+
* UX-3: the switch lock. Picking a workspace in the hero sheet runs
|
|
15337
|
+
* `selectWorkspace` → `POST /api/session/create` → `replaceMain` — the
|
|
15338
|
+
* conversation unmounts and remounts (`useMaybeIncarnation` epoch+1) and the
|
|
15339
|
+
* composer briefly still belongs to the OLD blank session. A message sent in
|
|
15340
|
+
* that window lands on the old session and is then wiped by the remount,
|
|
15341
|
+
* bouncing the UI back to the "choose a workspace" placeholder. While the
|
|
15342
|
+
* `data-zen-ws-switching` attribute sits on `<html>`, composer.css dead-zones
|
|
15343
|
+
* and dims the composer bar; the lock releases the moment the conversation
|
|
15344
|
+
* root's `data-conversation-session` has MOVED and the placeholder card is
|
|
15345
|
+
* gone — with a hard cap so a failed/no-op switch cannot freeze the input.
|
|
15346
|
+
*/
|
|
15347
|
+
const SWITCH_MARKER = 'data-zen-ws-switching';
|
|
15348
|
+
const SWITCH_LOCK_MS = 1_500;
|
|
15349
|
+
const SESSION_ATTR = 'data-conversation-session';
|
|
15350
|
+
const SESSION_SELECTOR = `[${SESSION_ATTR}]`;
|
|
15351
|
+
/** The hero's inert "choose a workspace" state: the whole input card IS the trigger. */
|
|
15352
|
+
const PLACEHOLDER_CARD_SELECTOR = '[data-composer-card][class*="_cardWorkspaceTrigger"]';
|
|
15353
|
+
/** Spelled literally (not interpolated) — the test pins this exact text as the perm/workspace discriminator. */
|
|
15354
|
+
const WS_MENUITEM_SELECTOR = 'body > div[role="menu"][data-zen-sheet="workspace"] button[role="menuitem"]';
|
|
15144
15355
|
/**
|
|
15145
15356
|
* Whether ONE body-level node is a portal menu that does not yet carry the
|
|
15146
15357
|
* sheet marker. Pure over the node's own methods: `matches` answers the
|
|
@@ -15149,17 +15360,52 @@ const SHEET_VALUE = 'perm';
|
|
|
15149
15360
|
function isUnmarkedPortalMenu(node) {
|
|
15150
15361
|
return node.matches(PORTAL_MENU_SELECTOR) && node.getAttribute(SHEET_MARKER) === null;
|
|
15151
15362
|
}
|
|
15363
|
+
const inSeat = (seat) => seat !== undefined && seat !== null;
|
|
15152
15364
|
/**
|
|
15153
|
-
*
|
|
15154
|
-
*
|
|
15155
|
-
*
|
|
15156
|
-
* the menus they open are
|
|
15365
|
+
* Which sheet ONE click event's target arms: the phone shell only. The
|
|
15366
|
+
* permission seat's trigger arms `perm` (T70); the hero page's two
|
|
15367
|
+
* workspace triggers arm `workspace` (UX-3). Any other button — the
|
|
15368
|
+
* sidebar's 「…」, a copy button — arms nothing, so the menus they open are
|
|
15369
|
+
* never marked.
|
|
15157
15370
|
*/
|
|
15158
|
-
function
|
|
15371
|
+
function sheetKindForTrigger(target, phoneMatches) {
|
|
15159
15372
|
if (!phoneMatches)
|
|
15373
|
+
return null;
|
|
15374
|
+
if (inSeat(target?.closest(PERM_TRIGGER_SELECTOR)))
|
|
15375
|
+
return 'perm';
|
|
15376
|
+
if (inSeat(target?.closest(WORKSPACE_TRIGGER_SELECTOR)))
|
|
15377
|
+
return 'workspace';
|
|
15378
|
+
return null;
|
|
15379
|
+
}
|
|
15380
|
+
/**
|
|
15381
|
+
* UX-3: whether a pointerdown landed inside a WORKSPACE trigger — the only
|
|
15382
|
+
* triggers whose open/close suffers the outside-pointerdown + toggle-click
|
|
15383
|
+
* race (the Menu closes the portaled sheet on the outside pointerdown, and
|
|
15384
|
+
* the trigger's own click would flip it straight back open). The permission
|
|
15385
|
+
* trigger toggles by click alone and is never suppressed.
|
|
15386
|
+
*/
|
|
15387
|
+
function pointerdownOnWorkspaceTrigger(target) {
|
|
15388
|
+
return inSeat(target?.closest(WORKSPACE_TRIGGER_SELECTOR));
|
|
15389
|
+
}
|
|
15390
|
+
/**
|
|
15391
|
+
* UX-3: whether a click started on a workspace item inside the MARKED
|
|
15392
|
+
* workspace sheet — the pick that starts the switch lock. Keyed on the
|
|
15393
|
+
* `workspace` marker value, so a permission option can never lock the
|
|
15394
|
+
* composer.
|
|
15395
|
+
*/
|
|
15396
|
+
function menuitemClickStartsWorkspaceSwitch(target) {
|
|
15397
|
+
return inSeat(target?.closest(WS_MENUITEM_SELECTOR));
|
|
15398
|
+
}
|
|
15399
|
+
/**
|
|
15400
|
+
* Whether the workspace switch has settled: the conversation root now names
|
|
15401
|
+
* a DIFFERENT session than the one the lock was taken on, and the no-workspace
|
|
15402
|
+
* placeholder card is gone (the optimistic `pendingWorkspaceId` display alone
|
|
15403
|
+
* is not enough — the remount resets it).
|
|
15404
|
+
*/
|
|
15405
|
+
function workspaceSwitchSettled(probe, lockedSessionValue) {
|
|
15406
|
+
if (probe.currentSessionValue() === lockedSessionValue)
|
|
15160
15407
|
return false;
|
|
15161
|
-
|
|
15162
|
-
return seat !== undefined && seat !== null;
|
|
15408
|
+
return !probe.placeholderOpen();
|
|
15163
15409
|
}
|
|
15164
15410
|
/**
|
|
15165
15411
|
* How long an OPEN click stays armed. The portal lands in the same React
|
|
@@ -15178,18 +15424,36 @@ const ARM_MS = 1_500;
|
|
|
15178
15424
|
* selectors stopped matching, and the phone got a tiny floating popup. The
|
|
15179
15425
|
* host offers no stable attribute to key the portal off — the anchor is a
|
|
15180
15426
|
* per-instance useId, there is no id and no aria-controls — so this effect
|
|
15181
|
-
* marks it: the trigger's click arms a window, the observer stamps the
|
|
15182
|
-
* body-level menu with `data-zen-sheet
|
|
15427
|
+
* marks it: the trigger's click arms a short window, the observer stamps the
|
|
15428
|
+
* next body-level menu with `data-zen-sheet`, and the stylesheet (which
|
|
15183
15429
|
* only the phone shell loads) turns THAT into the sheet. The portal menu
|
|
15184
15430
|
* unmounts on close, so the marker's lifetime is exactly the menu's; the
|
|
15185
15431
|
* 0.1.7 inline structure keeps working through the untouched descendant
|
|
15186
15432
|
* selectors, and the desktop shell never loads this stylesheet at all.
|
|
15433
|
+
*
|
|
15434
|
+
* UX-3 extends the same machinery to the hero page's workspace menu:
|
|
15435
|
+
* - the two workspace triggers arm a `data-zen-sheet="workspace"` sheet;
|
|
15436
|
+
* - a pointerdown on a workspace trigger while a marked sheet is up
|
|
15437
|
+
* suppresses the trigger's own click, so one tap reliably CLOSES (the
|
|
15438
|
+
* host's outside-pointerdown close stands instead of being undone by the
|
|
15439
|
+
* toggle click);
|
|
15440
|
+
* - a pick inside the workspace sheet locks the composer for the length of
|
|
15441
|
+
* the `selectWorkspace` remount (see SWITCH_MARKER above).
|
|
15187
15442
|
*/
|
|
15188
15443
|
function installPermissionSheet(ctx) {
|
|
15189
15444
|
ctx.effect(() => {
|
|
15190
15445
|
const phone = window.matchMedia(PHONE_QUERY);
|
|
15191
15446
|
let armed = false;
|
|
15447
|
+
let armedKind = SHEET_VALUE_PERM;
|
|
15192
15448
|
let armTimer;
|
|
15449
|
+
// UX-3 toggle debounce: recomputed on EVERY pointerdown, consumed by the
|
|
15450
|
+
// next click — a pointerdown whose click never comes (drag cancel) cannot
|
|
15451
|
+
// leave a stale swallow behind.
|
|
15452
|
+
let suppressClick = false;
|
|
15453
|
+
// Set when a swallowed click found the marked menu STILL on the page
|
|
15454
|
+
// (the pointerdown close did not land): the next tap passes through to
|
|
15455
|
+
// the host's own toggle instead of being swallowed forever.
|
|
15456
|
+
let allowToggle = false;
|
|
15193
15457
|
const disarm = () => {
|
|
15194
15458
|
armed = false;
|
|
15195
15459
|
if (armTimer !== undefined) {
|
|
@@ -15197,9 +15461,75 @@ function installPermissionSheet(ctx) {
|
|
|
15197
15461
|
armTimer = undefined;
|
|
15198
15462
|
}
|
|
15199
15463
|
};
|
|
15464
|
+
const markedSheet = () => document.querySelector(`${PORTAL_MENU_SELECTOR}[${SHEET_MARKER}]`);
|
|
15465
|
+
const readSessionValue = () => document.querySelector(SESSION_SELECTOR)?.getAttribute(SESSION_ATTR) ?? null;
|
|
15466
|
+
const placeholderOpen = () => document.querySelector(PLACEHOLDER_CARD_SELECTOR) !== null;
|
|
15467
|
+
// -- UX-3 switch lock -------------------------------------------------
|
|
15468
|
+
let lockedSessionValue = null;
|
|
15469
|
+
let lockTimer;
|
|
15470
|
+
let lockObserver;
|
|
15471
|
+
const releaseLock = () => {
|
|
15472
|
+
if (lockTimer !== undefined) {
|
|
15473
|
+
clearTimeout(lockTimer);
|
|
15474
|
+
lockTimer = undefined;
|
|
15475
|
+
}
|
|
15476
|
+
lockObserver?.disconnect();
|
|
15477
|
+
lockObserver = undefined;
|
|
15478
|
+
lockedSessionValue = null;
|
|
15479
|
+
document.documentElement.removeAttribute(SWITCH_MARKER);
|
|
15480
|
+
};
|
|
15481
|
+
const checkLock = () => {
|
|
15482
|
+
if (lockedSessionValue === null)
|
|
15483
|
+
return;
|
|
15484
|
+
if (workspaceSwitchSettled({ currentSessionValue: readSessionValue, placeholderOpen }, lockedSessionValue)) {
|
|
15485
|
+
releaseLock();
|
|
15486
|
+
}
|
|
15487
|
+
};
|
|
15488
|
+
const startSwitchLock = () => {
|
|
15489
|
+
if (lockedSessionValue === null) {
|
|
15490
|
+
lockedSessionValue = readSessionValue();
|
|
15491
|
+
document.documentElement.setAttribute(SWITCH_MARKER, '1');
|
|
15492
|
+
lockObserver = new MutationObserver(checkLock);
|
|
15493
|
+
// The session id moves on the conversation root (attribute) and the
|
|
15494
|
+
// placeholder card mounts/unmounts (childList) — plus class flips on
|
|
15495
|
+
// the card are attribute mutations too. One bounded subtree watch
|
|
15496
|
+
// covers all of it.
|
|
15497
|
+
lockObserver.observe(document.documentElement, { childList: true, subtree: true, attributes: true });
|
|
15498
|
+
}
|
|
15499
|
+
if (lockTimer !== undefined)
|
|
15500
|
+
clearTimeout(lockTimer);
|
|
15501
|
+
lockTimer = setTimeout(releaseLock, SWITCH_LOCK_MS);
|
|
15502
|
+
};
|
|
15503
|
+
// ---------------------------------------------------------------------
|
|
15504
|
+
const onCapturePointerDown = (event) => {
|
|
15505
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
15506
|
+
const onTrigger = pointerdownOnWorkspaceTrigger(target);
|
|
15507
|
+
suppressClick = onTrigger && markedSheet() !== null && !allowToggle;
|
|
15508
|
+
if (!suppressClick)
|
|
15509
|
+
allowToggle = false;
|
|
15510
|
+
};
|
|
15200
15511
|
const onCaptureClick = (event) => {
|
|
15512
|
+
// UX-3: let the outside-pointerdown close stand — stopPropagation
|
|
15513
|
+
// keeps the click from React's root listeners, so the trigger cannot
|
|
15514
|
+
// toggle the sheet straight back open.
|
|
15515
|
+
if (suppressClick) {
|
|
15516
|
+
suppressClick = false;
|
|
15517
|
+
if (markedSheet() !== null)
|
|
15518
|
+
allowToggle = true;
|
|
15519
|
+
event.preventDefault();
|
|
15520
|
+
event.stopPropagation();
|
|
15521
|
+
return;
|
|
15522
|
+
}
|
|
15201
15523
|
const target = event.target instanceof Element ? event.target : null;
|
|
15202
|
-
|
|
15524
|
+
// UX-3: a pick inside the workspace sheet starts the switch lock — the
|
|
15525
|
+
// composer must not accept input on the old blank session while
|
|
15526
|
+
// selectWorkspace remounts the conversation underneath it.
|
|
15527
|
+
if (phone.matches && menuitemClickStartsWorkspaceSwitch(target)) {
|
|
15528
|
+
startSwitchLock();
|
|
15529
|
+
return;
|
|
15530
|
+
}
|
|
15531
|
+
const kind = sheetKindForTrigger(target, phone.matches);
|
|
15532
|
+
if (kind === null)
|
|
15203
15533
|
return;
|
|
15204
15534
|
// T70-fix: the trigger TOGGLES. A click that CLOSES the menu lands
|
|
15205
15535
|
// while the marked menu is still in the DOM (capture runs before the
|
|
@@ -15208,9 +15538,10 @@ function installPermissionSheet(ctx) {
|
|
|
15208
15538
|
// body-level menu opening inside that window would be mis-marked as
|
|
15209
15539
|
// ours. A marked menu present at click time means this click can only
|
|
15210
15540
|
// be the close (or a no-op re-open), never an opening we must catch.
|
|
15211
|
-
if (
|
|
15541
|
+
if (markedSheet() !== null)
|
|
15212
15542
|
return;
|
|
15213
15543
|
armed = true;
|
|
15544
|
+
armedKind = kind === 'workspace' ? SHEET_VALUE_WORKSPACE : SHEET_VALUE_PERM;
|
|
15214
15545
|
if (armTimer !== undefined)
|
|
15215
15546
|
clearTimeout(armTimer);
|
|
15216
15547
|
armTimer = setTimeout(disarm, ARM_MS);
|
|
@@ -15220,11 +15551,12 @@ function installPermissionSheet(ctx) {
|
|
|
15220
15551
|
if (!armed)
|
|
15221
15552
|
return;
|
|
15222
15553
|
disarm();
|
|
15223
|
-
root.setAttribute(SHEET_MARKER,
|
|
15554
|
+
root.setAttribute(SHEET_MARKER, armedKind);
|
|
15224
15555
|
};
|
|
15225
15556
|
// Capture phase: the host's own handlers may stop the click's bubble
|
|
15226
15557
|
// (the trigger toggles `open` itself), so listen on the document in
|
|
15227
15558
|
// capture and never depend on propagation reaching us.
|
|
15559
|
+
document.addEventListener('pointerdown', onCapturePointerDown, true);
|
|
15228
15560
|
document.addEventListener('click', onCaptureClick, true);
|
|
15229
15561
|
const observer = new MutationObserver((records) => {
|
|
15230
15562
|
if (!armed)
|
|
@@ -15238,9 +15570,11 @@ function installPermissionSheet(ctx) {
|
|
|
15238
15570
|
});
|
|
15239
15571
|
observer.observe(document.body, { childList: true });
|
|
15240
15572
|
return () => {
|
|
15573
|
+
document.removeEventListener('pointerdown', onCapturePointerDown, true);
|
|
15241
15574
|
document.removeEventListener('click', onCaptureClick, true);
|
|
15242
15575
|
observer.disconnect();
|
|
15243
15576
|
disarm();
|
|
15577
|
+
releaseLock();
|
|
15244
15578
|
// A still-open marked menu outliving this effect keeps its marker only
|
|
15245
15579
|
// until the user closes it (the element unmounts); scrubbing here
|
|
15246
15580
|
// would fight a React commit mid-flight for no visual gain.
|
|
@@ -15276,36 +15610,75 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
15276
15610
|
exports.installNativeTriggerOverlay = installNativeTriggerOverlay;
|
|
15277
15611
|
const PHONE = '(max-width: 767px)';
|
|
15278
15612
|
const HEADER = '[data-phase] header';
|
|
15279
|
-
/**
|
|
15613
|
+
/**
|
|
15614
|
+
* Pill → the official trigger it fronts. Same ARIA split the pills use,
|
|
15615
|
+
* narrowed by the action-button class tail (UX-2, 2026-11-04): the bare
|
|
15616
|
+
* `button[aria-haspopup="tree"]` matched the breadcrumb's parent-session
|
|
15617
|
+
* switcher first (`…_switcherTrigger` — also a tree popup, earlier in
|
|
15618
|
+
* document order), so the pill got the wrong button laid over it. The
|
|
15619
|
+
* suffix matcher is case-sensitive and `_switcherTrigger` does not end in
|
|
15620
|
+
* `_trigger`, so the catalog trigger is the only match left.
|
|
15621
|
+
*/
|
|
15280
15622
|
const PAIRS = [
|
|
15281
|
-
{ pill: '[data-activity-kind="subagent"]', trigger: `${HEADER} button[aria-haspopup="tree"]` },
|
|
15623
|
+
{ pill: '[data-activity-kind="subagent"]', trigger: `${HEADER} button[class$="_trigger"][aria-haspopup="tree"]` },
|
|
15282
15624
|
{ pill: '[data-activity-kind="job"]', trigger: `${HEADER} button[class$="_trigger"]:not([aria-haspopup])` },
|
|
15283
15625
|
];
|
|
15284
|
-
/** Marks a trigger we have taken over, so cleanup can find
|
|
15626
|
+
/** Marks a trigger (and its anchor container) we have taken over, so cleanup can find them again. */
|
|
15285
15627
|
const TAKEN = 'data-zen-overlay';
|
|
15628
|
+
/** The shared fixed-viewport box: exactly what the portalled popover measures against. */
|
|
15629
|
+
function pin(el, r, display) {
|
|
15630
|
+
el.style.setProperty('position', 'fixed', 'important');
|
|
15631
|
+
el.style.setProperty('left', `${r.left}px`, 'important');
|
|
15632
|
+
el.style.setProperty('top', `${r.top}px`, 'important');
|
|
15633
|
+
el.style.setProperty('width', `${r.width}px`, 'important');
|
|
15634
|
+
el.style.setProperty('height', `${r.height}px`, 'important');
|
|
15635
|
+
el.style.setProperty('margin', '0', 'important');
|
|
15636
|
+
el.style.setProperty('padding', '0', 'important');
|
|
15637
|
+
el.style.setProperty('border', '0', 'important');
|
|
15638
|
+
if (display !== undefined)
|
|
15639
|
+
el.style.setProperty('display', display, 'important');
|
|
15640
|
+
}
|
|
15286
15641
|
function place(trigger, pill) {
|
|
15287
15642
|
const r = pill.getBoundingClientRect();
|
|
15288
15643
|
if (r.width === 0 || r.height === 0)
|
|
15289
15644
|
return;
|
|
15290
15645
|
trigger.setAttribute(TAKEN, '');
|
|
15646
|
+
pin(trigger, r);
|
|
15291
15647
|
// Fixed, not absolute: the header's containing block is not the pill's, and
|
|
15292
15648
|
// fixed rects are exactly what the portalled popover measures against.
|
|
15293
|
-
trigger.style.setProperty('position', 'fixed', 'important');
|
|
15294
|
-
trigger.style.setProperty('left', `${r.left}px`, 'important');
|
|
15295
|
-
trigger.style.setProperty('top', `${r.top}px`, 'important');
|
|
15296
|
-
trigger.style.setProperty('width', `${r.width}px`, 'important');
|
|
15297
|
-
trigger.style.setProperty('height', `${r.height}px`, 'important');
|
|
15298
|
-
trigger.style.setProperty('margin', '0', 'important');
|
|
15299
|
-
trigger.style.setProperty('padding', '0', 'important');
|
|
15300
|
-
trigger.style.setProperty('border', '0', 'important');
|
|
15301
15649
|
trigger.style.setProperty('opacity', '0', 'important');
|
|
15302
15650
|
trigger.style.setProperty('overflow', 'hidden', 'important');
|
|
15303
15651
|
trigger.style.setProperty('pointer-events', 'auto', 'important');
|
|
15304
15652
|
trigger.style.setProperty('z-index', '3', 'important');
|
|
15653
|
+
// The official popover (useAnchoredPosition) sizes itself from the OUTER
|
|
15654
|
+
// anchor container — the trigger's parent div, the official rootRef — not
|
|
15655
|
+
// the button. header.css.ts dissolves that container with
|
|
15656
|
+
// display:contents, so its rect was all zeros and the panel landed at
|
|
15657
|
+
// 12,12 inside the status bar (UX-2). Give the anchor the pill's exact
|
|
15658
|
+
// box: the inline display beats the stylesheet's display:contents
|
|
15659
|
+
// !important (the style attribute outranks selectors at author-important),
|
|
15660
|
+
// so rootRef.getBoundingClientRect() finally sees the pill. pointer-events
|
|
15661
|
+
// none keeps the invisible box from ever stealing a tap — the trigger
|
|
15662
|
+
// child above it is the tappable one. z-index 3 is required on the anchor
|
|
15663
|
+
// too: position:fixed unconditionally creates a stacking context (even at
|
|
15664
|
+
// z-index:auto, stack level 0), which otherwise traps the trigger's own
|
|
15665
|
+
// z-index:3 underneath [data-mobile-nav="header-activity"] (z-index:1).
|
|
15666
|
+
const anchor = trigger.parentElement;
|
|
15667
|
+
if (anchor !== null && anchor.closest(HEADER) !== null) {
|
|
15668
|
+
anchor.setAttribute(TAKEN, '');
|
|
15669
|
+
pin(anchor, r, 'block');
|
|
15670
|
+
anchor.style.setProperty('pointer-events', 'none', 'important');
|
|
15671
|
+
anchor.style.setProperty('z-index', '3', 'important');
|
|
15672
|
+
}
|
|
15305
15673
|
}
|
|
15306
15674
|
function release(trigger) {
|
|
15307
15675
|
trigger.removeAttribute(TAKEN);
|
|
15308
15676
|
trigger.style.cssText = '';
|
|
15677
|
+
const anchor = trigger.parentElement;
|
|
15678
|
+
if (anchor !== null && anchor.hasAttribute(TAKEN)) {
|
|
15679
|
+
anchor.removeAttribute(TAKEN);
|
|
15680
|
+
anchor.style.cssText = '';
|
|
15681
|
+
}
|
|
15309
15682
|
}
|
|
15310
15683
|
function installNativeTriggerOverlay(ctx) {
|
|
15311
15684
|
ctx.effect(() => {
|
|
@@ -17593,6 +17966,11 @@ function SettingsSectionPage({ config, shares, t }) {
|
|
|
17593
17966
|
setClaimBusy(true);
|
|
17594
17967
|
setClaimFail(null);
|
|
17595
17968
|
setClaimWriteFailed(false);
|
|
17969
|
+
// UX-1: captured BEFORE the pairing — whether the page ran as client, and
|
|
17970
|
+
// whether the backend's stream interceptor was already active per the
|
|
17971
|
+
// last client/status diagnostics.
|
|
17972
|
+
const wasEffectiveClient = clientRole;
|
|
17973
|
+
const streamInterceptAlreadyActive = clientView?.intercept?.installed === true;
|
|
17596
17974
|
// T60: one name for both halves of the round-trip — trimmed and legal
|
|
17597
17975
|
// ({@link claimDeviceNameOf}), sent to the server AND written into the
|
|
17598
17976
|
// row when the claim lands.
|
|
@@ -17625,6 +18003,20 @@ function SettingsSectionPage({ config, shares, t }) {
|
|
|
17625
18003
|
// could not store falls back to the name we actually sent.
|
|
17626
18004
|
const echoed = typeof body.deviceName === 'string' && body.deviceName !== '' ? (0, settings_form_ts_1.claimDeviceNameOf)(body.deviceName, claimName) : claimName;
|
|
17627
18005
|
const landed = await config.writeClientPairing(urlToWrite, body.token, echoed);
|
|
18006
|
+
if (landed && (0, settings_form_ts_1.shouldReloadAfterClientActivation)({
|
|
18007
|
+
wasEffectiveClient,
|
|
18008
|
+
nowClientRole: true,
|
|
18009
|
+
savedOrPaired: true,
|
|
18010
|
+
streamInterceptAlreadyActive,
|
|
18011
|
+
})) {
|
|
18012
|
+
// UX-1: FIRST pairing on a page whose resident global streams were
|
|
18013
|
+
// opened unwrapped (or with the interceptor inactive) — wait out the
|
|
18014
|
+
// backend's row-reload window, then reload so the streams reopen
|
|
18015
|
+
// wrapped and the server's sessions enter the sidebar.
|
|
18016
|
+
setClaimBusy(false);
|
|
18017
|
+
void (0, settings_form_ts_1.awaitClientStatusReady)().then(() => window.location.reload());
|
|
18018
|
+
return;
|
|
18019
|
+
}
|
|
17628
18020
|
if (landed) {
|
|
17629
18021
|
// The draft did its job — it now equals the displayed value, and
|
|
17630
18022
|
// keeping it would be a phantom edit (T60).
|
|
@@ -17753,8 +18145,29 @@ function SettingsSectionPage({ config, shares, t }) {
|
|
|
17753
18145
|
? t('settings.statusUnknown')
|
|
17754
18146
|
: t('settings.statusUnavailable', { code: load.status }) })), !clientRole && load.state === 'stale' && ((0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", "data-invalid": "true", children: t('settings.refreshFailed') })), clientRole && clientLoad.state === 'error' && ((0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", "data-invalid": "true", children: t('settings.statusUnknown') })), clientRole && clientLoad.state === 'stale' && ((0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", "data-invalid": "true", children: t('settings.refreshFailed') })), data?.viaGateway === true && (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-notice", children: t('settings.remoteNotice') }), (0, jsx_runtime_1.jsx)("div", { className: "zr-settings-card", children: (0, jsx_runtime_1.jsxs)(dsh_client_ui_primitives_1.SettingsForm, { labels: labels, state: form, onSave: () => {
|
|
17755
18147
|
const reloadsRow = saveReloadsRow();
|
|
17756
|
-
|
|
17757
|
-
|
|
18148
|
+
// UX-1: captured BEFORE the save — whether the page ran as client,
|
|
18149
|
+
// and whether the backend's stream interceptor was already active
|
|
18150
|
+
// per the last client/status diagnostics.
|
|
18151
|
+
const wasEffectiveClient = clientRole;
|
|
18152
|
+
const streamInterceptAlreadyActive = clientView?.intercept?.installed === true;
|
|
18153
|
+
void config.save().then((landed) => {
|
|
18154
|
+
if (!landed)
|
|
18155
|
+
return;
|
|
18156
|
+
if ((0, settings_form_ts_1.shouldReloadAfterClientActivation)({
|
|
18157
|
+
wasEffectiveClient,
|
|
18158
|
+
nowClientRole: config.savedRoleIsClient(),
|
|
18159
|
+
savedOrPaired: true,
|
|
18160
|
+
streamInterceptAlreadyActive,
|
|
18161
|
+
})) {
|
|
18162
|
+
// UX-1: the save first-activated the client interceptor while
|
|
18163
|
+
// this page's resident global streams were opened unwrapped —
|
|
18164
|
+
// wait out the row-reload window, then reload so the streams
|
|
18165
|
+
// reopen wrapped and the server's sessions enter the sidebar.
|
|
18166
|
+
void (0, settings_form_ts_1.awaitClientStatusReady)().then(() => window.location.reload());
|
|
18167
|
+
return;
|
|
18168
|
+
}
|
|
18169
|
+
saveLanded(reloadsRow);
|
|
18170
|
+
});
|
|
17758
18171
|
}, onDiscard: () => { config.discard(); }, children: [(0, jsx_runtime_1.jsx)("h3", { className: "zr-settings-card-title", children: t('settings.roleTitle') }), select(form.role, 'role', t('settings.role'), t('settings.roleHint'), ['host', 'client'], (option) => (option === 'host' ? t('settings.roleHost') : t('settings.roleClient'))), text(form.serverName, 'serverName', t('settings.fieldServerName'), t(clientRole ? 'settings.fieldServerNameHintClient' : 'settings.fieldServerNameHint'), t('settings.invalidName')), reloadPending && (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-status-line", children: t('settings.reloadNote') }), !clientRole && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("h3", { className: "zr-settings-card-title", children: t('settings.gatewayTitle') }), text(form.host, 'host', t('settings.fieldHost'), t('settings.fieldHostHint'), '', false, t('settings.fieldHostNote')), text(form.port, 'port', t('settings.fieldPort'), t('settings.fieldPortHint'), t('settings.invalidPort'), true), text(form.trustedProxies, 'trustedProxies', t('settings.fieldTrustedProxies'), t('settings.fieldTrustedProxiesHint'), ''), text(form.rateLimit, 'rateLimit', t('settings.fieldRateLimit'), t('settings.fieldRateLimitHint'), t('settings.invalidPositiveInt'), true), text(form.vapidSubject, 'vapidSubject', t('settings.fieldVapidSubject'), t('settings.fieldVapidSubjectHint'), ''), (0, jsx_runtime_1.jsx)("h3", { className: "zr-settings-card-title", children: t('settings.pushTitle') }), bool(form.pushSummary, 'pushSummary', t('settings.fieldPushSummary'), t('settings.fieldPushSummaryHint')), bool(form.pushTurnEnd, 'pushTurnEnd', t('settings.fieldPushTurnEnd'), t('settings.fieldPushTurnEndHint')), bool(form.pushTool, 'pushTool', t('settings.fieldPushTool'), t('settings.fieldPushToolHint')), text(form.pushDebounceMs, 'pushDebounceMs', t('settings.fieldPushDebounceMs'), t('settings.fieldPushDebounceMsHint'), t('settings.invalidNonNegativeInt'), true), select(form.lang, 'lang', t('settings.fieldLang'), t('settings.fieldLangHint'), ['auto', 'zh', 'en'], (option) => (option === 'auto' ? t('settings.langAuto') : option === 'zh' ? t('settings.langZh') : t('settings.langEn'))), (0, jsx_runtime_1.jsx)("h3", { className: "zr-settings-card-title", children: t('settings.shareTitle') }), text(form.idleHours, 'idleHours', t('settings.fieldIdleHours'), t('settings.fieldIdleHoursHint'), t('settings.invalidHours'), true), bool(form.autoShareNewSessions, 'autoShareNewSessions', t('settings.fieldAutoShare'), t('settings.fieldAutoShareHint')), (0, jsx_runtime_1.jsx)("h3", { className: "zr-settings-card-title", children: t('settings.bridge.title') }), bridgeState === 'loading' && (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", children: t('settings.bridge.loading') }), bridgeState === 'error' && (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", "data-invalid": "true", children: t('settings.bridge.loadFailed') }), bridgeState === 'empty' && (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", children: t('settings.bridge.empty') }), bridgeLoad.rows.map((row) => {
|
|
17759
18172
|
let current = {};
|
|
17760
18173
|
try {
|
|
@@ -19620,6 +20033,12 @@ function GatewayWizard(props) {
|
|
|
19620
20033
|
const runClientClaim = async () => {
|
|
19621
20034
|
setFormError(null);
|
|
19622
20035
|
setClientClaimFail(null);
|
|
20036
|
+
// UX-1: the facts that decide the post-pairing reload — whether the page
|
|
20037
|
+
// opened as an already-effective client, and whether the backend's
|
|
20038
|
+
// stream interceptor was already active (from the last client/status
|
|
20039
|
+
// diagnostics). Captured BEFORE the role switch and the claim below.
|
|
20040
|
+
const wasEffectiveClient = effectiveClient;
|
|
20041
|
+
const streamInterceptAlreadyActive = clientView?.intercept?.installed === true;
|
|
19623
20042
|
const trimmedUrl = clientServerUrl.trim();
|
|
19624
20043
|
const normalizedCode = (0, settings_form_ts_1.normalizePairingCode)(clientPairCode);
|
|
19625
20044
|
if (trimmedUrl === '' || !/^https?:\/\//i.test(trimmedUrl)) {
|
|
@@ -19661,7 +20080,21 @@ function GatewayWizard(props) {
|
|
|
19661
20080
|
if (body?.ok === true && typeof body.token === 'string' && body.token !== '') {
|
|
19662
20081
|
const urlToWrite = typeof body.serverUrl === 'string' && body.serverUrl !== '' ? body.serverUrl : trimmedUrl;
|
|
19663
20082
|
const echoed = typeof body.deviceName === 'string' && body.deviceName.trim() !== '' ? body.deviceName.trim() : deviceName;
|
|
19664
|
-
await config.writeClientPairing(urlToWrite, body.token, echoed);
|
|
20083
|
+
const landed = await config.writeClientPairing(urlToWrite, body.token, echoed);
|
|
20084
|
+
if (landed && (0, settings_form_ts_1.shouldReloadAfterClientActivation)({
|
|
20085
|
+
wasEffectiveClient,
|
|
20086
|
+
nowClientRole: true,
|
|
20087
|
+
savedOrPaired: true,
|
|
20088
|
+
streamInterceptAlreadyActive,
|
|
20089
|
+
})) {
|
|
20090
|
+
// UX-1: this was the FIRST client activation while the page's three
|
|
20091
|
+
// resident global streams were opened unwrapped — wait out the
|
|
20092
|
+
// backend's row-reload window, then reload so the streams reopen
|
|
20093
|
+
// wrapped and the server's shared sessions enter the sidebar.
|
|
20094
|
+
await (0, settings_form_ts_1.awaitClientStatusReady)();
|
|
20095
|
+
window.location.reload();
|
|
20096
|
+
return;
|
|
20097
|
+
}
|
|
19665
20098
|
setClientPairCode('');
|
|
19666
20099
|
await loadClientStatus();
|
|
19667
20100
|
setStep('verify');
|