dsh-zen-remote 2.3.8 → 2.3.9
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 +29 -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 +470 -81
- 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/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/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/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 +44 -7
- 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/composer.css.ts +24 -0
- package/src/client/styles/header.css.ts +58 -46
- package/src/client/styles/layout.css.ts +25 -0
- 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
|
@@ -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)';
|
|
@@ -1600,12 +1613,29 @@ function MobileHome({ useSessions, useWorkspaces, useSessionPendingInteraction,
|
|
|
1600
1613
|
const status = statusLine(row, dot, t);
|
|
1601
1614
|
const initial = row.displayTitle.trim().charAt(0).toUpperCase();
|
|
1602
1615
|
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: () =>
|
|
1616
|
+
}), 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: () => {
|
|
1617
|
+
// UX-3: never enter a dangling no-workspace session. With "all"
|
|
1618
|
+
// selected (the common case — the host's own sessions belong to no
|
|
1619
|
+
// workspace) the decision either auto-starts the only workspace or
|
|
1620
|
+
// opens the workspace drawer first; the session then starts with a
|
|
1621
|
+
// workspace attached from the first frame.
|
|
1622
|
+
const action = (0, new_session_ts_1.decideNewSessionAction)(selectedWorkspace?.workspaceId, workspaces.items.map((item) => item.workspaceId));
|
|
1623
|
+
if (action.kind === 'start')
|
|
1624
|
+
enter(() => startSession(action.workspaceId));
|
|
1625
|
+
else
|
|
1626
|
+
setSheet('new-session');
|
|
1627
|
+
}, 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
1628
|
actions.filter('all');
|
|
1605
1629
|
setSheet(null);
|
|
1606
1630
|
}, 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
1631
|
actions.filter(item.workspaceId);
|
|
1608
1632
|
setSheet(null);
|
|
1633
|
+
}, 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: () => {
|
|
1634
|
+
// Pin the list to the picked workspace (so backing out
|
|
1635
|
+
// of the blank session lands on ITS list) and start the
|
|
1636
|
+
// session with that workspace from the first frame.
|
|
1637
|
+
actions.filter(item.workspaceId);
|
|
1638
|
+
enter(() => startSession(item.workspaceId));
|
|
1609
1639
|
}, children: item.title }, item.workspaceId)))] })), sheet === 'chips' && (0, jsx_runtime_1.jsx)(MobileHomeChips_tsx_1.MobileHomeChipsSheetBody, { t: t })] })] }))] })] }));
|
|
1610
1640
|
}
|
|
1611
1641
|
};
|
|
@@ -1665,7 +1695,13 @@ function IconTaskGlyph({ size = 14 }) {
|
|
|
1665
1695
|
* the root through `:has()` instead of naming it.
|
|
1666
1696
|
*/
|
|
1667
1697
|
const HEADER = '[data-phase] header';
|
|
1668
|
-
|
|
1698
|
+
/* UX-2 (2026-11-04): narrowed by the action-button class tail. The bare
|
|
1699
|
+
`button[aria-haspopup="tree"]` matched the breadcrumb's parent-session
|
|
1700
|
+
switcher first (`…_switcherTrigger` — also a tree popup), so both the
|
|
1701
|
+
overlay and this fallback check resolved to the wrong button. The suffix
|
|
1702
|
+
matcher is case-sensitive and `_switcherTrigger` does not end in
|
|
1703
|
+
`_trigger`. Must stay in lockstep with effects/native-trigger-overlay.ts. */
|
|
1704
|
+
const SUBAGENT_TRIGGER = `${HEADER} button[class$="_trigger"][aria-haspopup="tree"]`;
|
|
1669
1705
|
const JOBS_TRIGGER = `${HEADER} button[class$="_trigger"]:not([aria-haspopup])`;
|
|
1670
1706
|
/** One glanceable group: icon + count + state dot; opens the official popover. */
|
|
1671
1707
|
function ActivityPill({ kind, count, state, label, trigger, onFallback, Icon }) {
|
|
@@ -2072,6 +2108,8 @@ exports.deriveClientStatusView = deriveClientStatusView;
|
|
|
2072
2108
|
exports.clientStatusLineOf = clientStatusLineOf;
|
|
2073
2109
|
exports.normalizePairingCode = normalizePairingCode;
|
|
2074
2110
|
exports.shouldFollowServerDeviceName = shouldFollowServerDeviceName;
|
|
2111
|
+
exports.shouldReloadAfterClientActivation = shouldReloadAfterClientActivation;
|
|
2112
|
+
exports.awaitClientStatusReady = awaitClientStatusReady;
|
|
2075
2113
|
exports.createLatestGate = createLatestGate;
|
|
2076
2114
|
exports.savedRowRole = savedRowRole;
|
|
2077
2115
|
exports.settingsRoleOf = settingsRoleOf;
|
|
@@ -2273,6 +2311,37 @@ function normalizePairingCode(input) {
|
|
|
2273
2311
|
function shouldFollowServerDeviceName(deviceName, rowName, hasDraft) {
|
|
2274
2312
|
return deviceName !== '' && deviceName !== rowName && !hasDraft;
|
|
2275
2313
|
}
|
|
2314
|
+
function shouldReloadAfterClientActivation(input) {
|
|
2315
|
+
if (!input.savedOrPaired || !input.nowClientRole)
|
|
2316
|
+
return false;
|
|
2317
|
+
if (!input.wasEffectiveClient)
|
|
2318
|
+
return true;
|
|
2319
|
+
if (input.streamInterceptAlreadyActive === false)
|
|
2320
|
+
return true;
|
|
2321
|
+
return false;
|
|
2322
|
+
}
|
|
2323
|
+
/**
|
|
2324
|
+
* Poll {@link CLIENT_STATUS_ROUTE} until the backend answers ok — the bounded
|
|
2325
|
+
* wait before the post-activation reload, covering the restart-watcher's row
|
|
2326
|
+
* reload window (the route 404s / fails while the row restarts). Resolves
|
|
2327
|
+
* after `maxMs` regardless, so the reload always fires. Injectable fetch and
|
|
2328
|
+
* timings keep the check script fast; defaults are the page's 3 s / 250 ms.
|
|
2329
|
+
*/
|
|
2330
|
+
async function awaitClientStatusReady(fetchFn = fetch, maxMs = 3000, intervalMs = 250) {
|
|
2331
|
+
const deadline = Date.now() + maxMs;
|
|
2332
|
+
while (true) {
|
|
2333
|
+
try {
|
|
2334
|
+
if ((await fetchFn(exports.CLIENT_STATUS_ROUTE)).ok)
|
|
2335
|
+
return;
|
|
2336
|
+
}
|
|
2337
|
+
catch {
|
|
2338
|
+
// the row is mid-reload; keep polling
|
|
2339
|
+
}
|
|
2340
|
+
if (Date.now() >= deadline)
|
|
2341
|
+
return;
|
|
2342
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2276
2345
|
function createLatestGate() {
|
|
2277
2346
|
let current = 0;
|
|
2278
2347
|
return {
|
|
@@ -3063,6 +3132,7 @@ exports.zh = {
|
|
|
3063
3132
|
'previewExitFullscreen': '退出全屏',
|
|
3064
3133
|
'allWorkspaces': '全部',
|
|
3065
3134
|
'switchWorkspace': '切换工作区',
|
|
3135
|
+
'pickWorkspaceNew': '选择工作区开始',
|
|
3066
3136
|
'newSession': '新建会话',
|
|
3067
3137
|
'noSessions': '暂无会话,点击右下角加号新建',
|
|
3068
3138
|
'homeStatusOngoing': '运行中',
|
|
@@ -3586,6 +3656,7 @@ exports.en = {
|
|
|
3586
3656
|
'previewExitFullscreen': 'Exit fullscreen',
|
|
3587
3657
|
'allWorkspaces': 'All',
|
|
3588
3658
|
'switchWorkspace': 'Switch workspace',
|
|
3659
|
+
'pickWorkspaceNew': 'Pick a workspace to start',
|
|
3589
3660
|
'newSession': 'New session',
|
|
3590
3661
|
'noSessions': 'No sessions yet — tap + to start one',
|
|
3591
3662
|
'homeStatusOngoing': 'Running',
|
|
@@ -8163,6 +8234,31 @@ exports.LAYOUT_CSS = `/* ---------- mobile-only layout ---------- */
|
|
|
8163
8234
|
padding-top: var(--mnav-sat) !important;
|
|
8164
8235
|
padding-bottom: var(--mnav-sab) !important;
|
|
8165
8236
|
}
|
|
8237
|
+
/* Right sidebar start/guide page (dsh-client-ui-sidebar-right GuideBody):
|
|
8238
|
+
the host sets .Dv5QdW_tabBody { height: 100%; overflow: hidden } and
|
|
8239
|
+
.F6Huca_guide { min-height: 100%; justify-content: center } with no
|
|
8240
|
+
overflow-y. When many tab entries are registered (e.g. better-sidebar +
|
|
8241
|
+
plugins), the centered flex column overflows equally out the top and
|
|
8242
|
+
bottom of the locked 100%-height box — clipping the leading entries
|
|
8243
|
+
("文件", "Git") above y=0 with no way to scroll. Make the guide itself
|
|
8244
|
+
the scroll container and replace justify-content:center with auto
|
|
8245
|
+
margins on the first item and ::after so short lists stay centered
|
|
8246
|
+
while long lists start cleanly at the top and scroll end-to-end. */
|
|
8247
|
+
[data-sidebar-right-guide] {
|
|
8248
|
+
flex: 1 1 0 !important;
|
|
8249
|
+
min-height: 0 !important;
|
|
8250
|
+
justify-content: flex-start !important;
|
|
8251
|
+
overflow-y: auto !important;
|
|
8252
|
+
-webkit-overflow-scrolling: touch;
|
|
8253
|
+
padding-top: 24px !important;
|
|
8254
|
+
padding-bottom: 24px !important;
|
|
8255
|
+
}
|
|
8256
|
+
[data-sidebar-right-guide] > :first-child {
|
|
8257
|
+
margin-top: auto !important;
|
|
8258
|
+
}
|
|
8259
|
+
[data-sidebar-right-guide]::after {
|
|
8260
|
+
margin-bottom: auto !important;
|
|
8261
|
+
}
|
|
8166
8262
|
|
|
8167
8263
|
/* Expanded state (frame without data-sidebar-collapsed) slides the drawer in.
|
|
8168
8264
|
The open state must be transform:none — NOT translateX(0): an identity
|
|
@@ -10570,6 +10666,14 @@ exports.HEADER_CSS = `/* ---------- session header five-piece reflow (< 768px) -
|
|
|
10570
10666
|
instead of centered. Pinning the row makes it one 48px band again. */
|
|
10571
10667
|
[data-phase] header [class$="_titleRow"] {
|
|
10572
10668
|
position: relative;
|
|
10669
|
+
/* UX-2 (2026-11-04): the host gives this row container-type: inline-size,
|
|
10670
|
+
which implies layout containment — making the row the containing block
|
|
10671
|
+
for every descendant position:fixed. That shifted every viewport
|
|
10672
|
+
coordinate effects/native-trigger-overlay.ts writes by the safe-area
|
|
10673
|
+
top and the row's horizontal padding. normal hands fixed descendants
|
|
10674
|
+
back to the viewport; the phone block owns this layout, so nothing
|
|
10675
|
+
scoped container queries against the row here. */
|
|
10676
|
+
container-type: normal !important;
|
|
10573
10677
|
display: grid;
|
|
10574
10678
|
grid-template-columns: 92px 1fr 92px;
|
|
10575
10679
|
grid-template-rows: minmax(48px, auto);
|
|
@@ -10637,55 +10741,30 @@ exports.HEADER_CSS = `/* ---------- session header five-piece reflow (< 768px) -
|
|
|
10637
10741
|
min-width: 0;
|
|
10638
10742
|
overflow: hidden;
|
|
10639
10743
|
}
|
|
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)) {
|
|
10744
|
+
/* Subagent sessions hide every ancestor crumb (UX-2, 2026-11-04 —
|
|
10745
|
+
supersedes the 2026-08-18 "keep ONE ancestor" rule). DSH 0.2.1 turned
|
|
10746
|
+
the parent crumb into a session-TREE popup trigger
|
|
10747
|
+
(…_switcherTrigger[aria-haspopup="tree"]) instead of a direct
|
|
10748
|
+
switch-back tap, and that popup is desktop geometry on the phone; the
|
|
10749
|
+
back button and the home stack stay the way back. Main sessions render
|
|
10750
|
+
a single span with no segs at all, so :not(:last-child) never matches
|
|
10751
|
+
there. */
|
|
10752
|
+
[data-phase] header [class$="_crumbSeg"]:not(:last-child) {
|
|
10651
10753
|
display: none !important;
|
|
10652
10754
|
}
|
|
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"] {
|
|
10755
|
+
/* With the parent seg gone, the current seg's own leading "/" would
|
|
10756
|
+
dangle before the title; the dissolved trigger container's stray
|
|
10757
|
+
separator is covered by this same rule (its slot rules below hide it
|
|
10758
|
+
too). One blanket rule instead of per-segment cases. */
|
|
10759
|
+
[data-phase] header [class$="_crumbSep"] {
|
|
10663
10760
|
display: none !important;
|
|
10664
10761
|
}
|
|
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
10762
|
[data-phase] header [class$="_crumbSeg"]:last-child {
|
|
10680
10763
|
display: flex;
|
|
10681
10764
|
align-items: center;
|
|
10682
10765
|
min-width: 0;
|
|
10683
10766
|
max-width: 100%;
|
|
10684
10767
|
}
|
|
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
10768
|
/* BOTH selectors are required: [class$=] matches the whole class
|
|
10690
10769
|
ATTRIBUTE's suffix, and the current-session title button's attribute is
|
|
10691
10770
|
"wSkVaW_crumb wSkVaW_crumbCurrent" — it ends in _crumbCurrent, so the _crumb
|
|
@@ -10795,12 +10874,17 @@ exports.HEADER_CSS = `/* ---------- session header five-piece reflow (< 768px) -
|
|
|
10795
10874
|
pointer-events on the trigger: that is exactly what would take the tap
|
|
10796
10875
|
target away again.
|
|
10797
10876
|
|
|
10798
|
-
CSS keeps only
|
|
10799
|
-
|
|
10800
|
-
|
|
10801
|
-
|
|
10802
|
-
|
|
10803
|
-
|
|
10877
|
+
CSS keeps only the resting dissolve: the root's box (and its stray
|
|
10878
|
+
separator, hidden below) stops occupying the breadcrumb until the
|
|
10879
|
+
overlay pins it. The measurement chore moved to the overlay (UX-2,
|
|
10880
|
+
2026-11-04): the 0.1.1+ popover portals to <body> as position:fixed
|
|
10881
|
+
and sizes itself from the OUTER anchor container (the official
|
|
10882
|
+
rootRef, the trigger's parent div) — a display:contents root has no
|
|
10883
|
+
box, so rootRef.getBoundingClientRect() was all zeros and the panel
|
|
10884
|
+
landed at 12,12 inside the status bar. effects/
|
|
10885
|
+
native-trigger-overlay.ts now pins container + trigger to the pill's
|
|
10886
|
+
rect together (its inline display beats this rule's !important, style
|
|
10887
|
+
attribute outranks selectors at author-important). */
|
|
10804
10888
|
[data-phase] header [data-slot="conversation.session.header.actions"] > *:has(> button[class$="_trigger"]),
|
|
10805
10889
|
[data-phase] header [data-slot="conversation.session.header.lineage"] > *:has(> button[class$="_trigger"]) {
|
|
10806
10890
|
display: contents !important;
|
|
@@ -10809,6 +10893,18 @@ exports.HEADER_CSS = `/* ---------- session header five-piece reflow (< 768px) -
|
|
|
10809
10893
|
[data-phase] header [data-slot="conversation.session.header.lineage"] [class$="_separator"] {
|
|
10810
10894
|
display: none !important;
|
|
10811
10895
|
}
|
|
10896
|
+
/* The popovers themselves portal to <body> as position:fixed layers
|
|
10897
|
+
(class suffix _menu, z-index 100) and place themselves from the pill's
|
|
10898
|
+
viewport rect with margin 12 — the overlay above is what makes that
|
|
10899
|
+
rect correct. This is only a floor: the panels' 336px width overflows
|
|
10900
|
+
320px-class phones, and a tall list must not run under the notch or
|
|
10901
|
+
the home indicator. [id$="-menu"] keeps the composer's model menu out
|
|
10902
|
+
of it — composer.css.ts owns that surface, and this file sits before
|
|
10903
|
+
it in styles/index.ts so its own rules still win the tie. */
|
|
10904
|
+
body > [class$="_menu"]:not([id$="-menu"]) {
|
|
10905
|
+
max-width: calc(100vw - 24px) !important;
|
|
10906
|
+
max-height: calc(100dvh - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px) - 24px) !important;
|
|
10907
|
+
}
|
|
10812
10908
|
/* The agent-preset mode badge specifically survives the blanket hide
|
|
10813
10909
|
above: layout.css.ts's shared <=1023px block targets it directly
|
|
10814
10910
|
(header [class$="_label"]:has(> svg) { display: block !important }),
|
|
@@ -10837,8 +10933,12 @@ exports.HEADER_CSS = `/* ---------- session header five-piece reflow (< 768px) -
|
|
|
10837
10933
|
display: inline-flex !important;
|
|
10838
10934
|
}
|
|
10839
10935
|
|
|
10840
|
-
/* Back button: 44pt touch target, leftmost column.
|
|
10936
|
+
/* Back button: 44pt touch target, leftmost column.
|
|
10937
|
+
order: -1 keeps it to the left of @deepseek-ai/dsh-experimental-client-ui-agent-team's
|
|
10938
|
+
TeamAction ([data-team-action], registered into header.actions at order -20 with
|
|
10939
|
+
default flex order 0). */
|
|
10841
10940
|
[data-mobile-nav="header-back"] {
|
|
10941
|
+
order: -1;
|
|
10842
10942
|
display: inline-flex;
|
|
10843
10943
|
align-items: center;
|
|
10844
10944
|
justify-content: center;
|
|
@@ -10855,6 +10955,13 @@ exports.HEADER_CSS = `/* ---------- session header five-piece reflow (< 768px) -
|
|
|
10855
10955
|
[data-mobile-nav="header-back"]:active {
|
|
10856
10956
|
opacity: .6;
|
|
10857
10957
|
}
|
|
10958
|
+
/* TeamAction's own stylesheet hides .XdyUwa_triggerLabel via
|
|
10959
|
+
@container (width<=480px) against _titleRow's container-type: inline-size.
|
|
10960
|
+
Shedding container-type on _titleRow above (UX-2) disabled that container
|
|
10961
|
+
query, so hide the label explicitly on phone and keep only the icon. */
|
|
10962
|
+
[data-phase] header [data-team-action] [class$="_triggerLabel"] {
|
|
10963
|
+
display: none !important;
|
|
10964
|
+
}
|
|
10858
10965
|
|
|
10859
10966
|
/* Info + workbench utility buttons, rightmost column. */
|
|
10860
10967
|
[data-mobile-nav="header-info"],
|
|
@@ -10945,6 +11052,7 @@ exports.HEADER_CSS = `/* ---------- session header five-piece reflow (< 768px) -
|
|
|
10945
11052
|
top:100% lands both on the same 28px line. z-index puts it above the
|
|
10946
11053
|
row it overlaps. */
|
|
10947
11054
|
[data-mobile-nav="header-activity"] {
|
|
11055
|
+
pointer-events: none;
|
|
10948
11056
|
position: absolute;
|
|
10949
11057
|
right: 0;
|
|
10950
11058
|
top: 100%;
|
|
@@ -11406,6 +11514,12 @@ exports.COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- */
|
|
|
11406
11514
|
…, open-in-app, deliverables, …), and none opens through this trigger
|
|
11407
11515
|
while no marked menu is up. */
|
|
11408
11516
|
body > div[role="menu"][data-zen-sheet="perm"],
|
|
11517
|
+
/* UX-3: the hero page's workspace menu, marked by the same effect through
|
|
11518
|
+
its own two triggers (the WorkspaceChip and the placeholder card) with
|
|
11519
|
+
the workspace value — a distinct marker so the pick that starts the
|
|
11520
|
+
workspace switch (effects/permission-sheet.ts) can key on THIS sheet
|
|
11521
|
+
without ever touching permission options. */
|
|
11522
|
+
body > div[role="menu"][data-zen-sheet="workspace"],
|
|
11409
11523
|
${MODEL_MENU} {
|
|
11410
11524
|
position: fixed !important;
|
|
11411
11525
|
left: 0 !important;
|
|
@@ -11438,6 +11552,7 @@ exports.COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- */
|
|
|
11438
11552
|
older hosts. */
|
|
11439
11553
|
${PERM} [role="menu"] [role="menuitem"],
|
|
11440
11554
|
body > div[role="menu"][data-zen-sheet="perm"] [role="menuitem"],
|
|
11555
|
+
body > div[role="menu"][data-zen-sheet="workspace"] [role="menuitem"],
|
|
11441
11556
|
${MODEL_MENU} [role="menuitem"],
|
|
11442
11557
|
${MODEL_MENU} [role="menuitemradio"],
|
|
11443
11558
|
${MODEL_MENU} [class$="_option"],
|
|
@@ -11586,11 +11701,28 @@ exports.COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- */
|
|
|
11586
11701
|
body:has(> [id$="-menu"][class$="_menu"]:is([role="menu"], [role="group"])) [data-chat-flow] + div,
|
|
11587
11702
|
/* T70: the portaled permission menu (marked by effects/permission-sheet.ts). */
|
|
11588
11703
|
body:has(> div[role="menu"][data-zen-sheet="perm"]) [data-chat-flow] + div,
|
|
11704
|
+
/* UX-3: the portaled hero workspace menu, same treatment. */
|
|
11705
|
+
body:has(> div[role="menu"][data-zen-sheet="workspace"]) [data-chat-flow] + div,
|
|
11589
11706
|
/* T-issue30: an inline menu a parked chip opened (escaped to a fixed
|
|
11590
11707
|
sheet by 4a). */
|
|
11591
11708
|
body:has([data-zen-sheet-extras] [role="menu"]) [data-chat-flow] + div {
|
|
11592
11709
|
display: none !important;
|
|
11593
11710
|
}
|
|
11711
|
+
/* --- 4c. UX-3: the workspace switch lock ---
|
|
11712
|
+
Between picking a workspace in the hero sheet and the new blank session
|
|
11713
|
+
landing (selectWorkspace → POST /api/session/create → replaceMain
|
|
11714
|
+
remounts the conversation), the composer still belongs to the OLD blank
|
|
11715
|
+
session: a message sent in that window lands there and is then wiped by
|
|
11716
|
+
the remount, bouncing the UI back to the "choose a workspace" state.
|
|
11717
|
+
effects/permission-sheet.ts stamps data-zen-ws-switching on <html> for
|
|
11718
|
+
exactly that window — the lock releases when the conversation root's
|
|
11719
|
+
data-conversation-session has moved and the placeholder card is gone,
|
|
11720
|
+
with a 1.5 s cap — and the dim (not just pointer-events) makes the
|
|
11721
|
+
freeze visible instead of reading as a dead input. */
|
|
11722
|
+
html[data-zen-ws-switching] [data-slot="conversation.composer.bar"] {
|
|
11723
|
+
pointer-events: none !important;
|
|
11724
|
+
opacity: .6 !important;
|
|
11725
|
+
}
|
|
11594
11726
|
/* --- 5. input box: two lines minimum, five lines maximum ---
|
|
11595
11727
|
The official autosizer drives the box off the hidden mirror's height, so
|
|
11596
11728
|
a min-height on the mirror IS the min-height of the field; the scroll cap
|
|
@@ -15112,7 +15244,10 @@ __modules["client/effects/permission-sheet.js"] = function (require, module, exp
|
|
|
15112
15244
|
"use strict";
|
|
15113
15245
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15114
15246
|
exports.isUnmarkedPortalMenu = isUnmarkedPortalMenu;
|
|
15115
|
-
exports.
|
|
15247
|
+
exports.sheetKindForTrigger = sheetKindForTrigger;
|
|
15248
|
+
exports.pointerdownOnWorkspaceTrigger = pointerdownOnWorkspaceTrigger;
|
|
15249
|
+
exports.menuitemClickStartsWorkspaceSwitch = menuitemClickStartsWorkspaceSwitch;
|
|
15250
|
+
exports.workspaceSwitchSettled = workspaceSwitchSettled;
|
|
15116
15251
|
exports.installPermissionSheet = installPermissionSheet;
|
|
15117
15252
|
/** Phone breakpoint — the same query every phone-only effect in this plugin uses. */
|
|
15118
15253
|
const PHONE_QUERY = '(max-width: 767px)';
|
|
@@ -15123,7 +15258,18 @@ const PHONE_QUERY = '(max-width: 767px)';
|
|
|
15123
15258
|
* NO aria-controls on 0.2.0 (the Menu primitive portals its list to body
|
|
15124
15259
|
* and links nothing back), so the click is the only open signal there is.
|
|
15125
15260
|
*/
|
|
15126
|
-
const
|
|
15261
|
+
const PERM_TRIGGER_SELECTOR = '[data-slot="conversation.input.permission"] button[class$="_trigger"]';
|
|
15262
|
+
/**
|
|
15263
|
+
* UX-3: the HERO page's two workspace triggers (WorkspaceSheet slot impl,
|
|
15264
|
+
* verified in the dsh-client-ui-conversation bundle 2026-11-05):
|
|
15265
|
+
* - `WorkspaceChip` — a bare `button` whose hashed class ends in
|
|
15266
|
+
* `_workspace`, carrying `aria-haspopup="menu"` + `aria-expanded`;
|
|
15267
|
+
* - the placeholder card — when a blank hero session has no workspace yet
|
|
15268
|
+
* the whole `div[data-composer-card]` carries `cardWorkspaceTrigger` and
|
|
15269
|
+
* its click opens the same menu (`onRequestWorkspace`).
|
|
15270
|
+
* Both open a `body > div[role="menu"]` of `button[role="menuitem"]` items.
|
|
15271
|
+
*/
|
|
15272
|
+
const WORKSPACE_TRIGGER_SELECTOR = '[class$="_workspace"][aria-haspopup="menu"], [data-composer-card][class*="_cardWorkspaceTrigger"]';
|
|
15127
15273
|
/**
|
|
15128
15274
|
* What the Menu primitive portals on 0.2.0: a `role=menu` div DIRECTLY under
|
|
15129
15275
|
* body, carrying `data-menu-material` and a randomized `--dsh-menu-anchor`
|
|
@@ -15132,15 +15278,36 @@ const TRIGGER_SELECTOR = '[data-slot="conversation.input.permission"] button[cla
|
|
|
15132
15278
|
* host has MANY `portal: true` Menus (agent-preset, chat preference rows,
|
|
15133
15279
|
* the Enter-behavior row, the workspace sidebar's 「…」, open-in-app,
|
|
15134
15280
|
* 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
|
-
*
|
|
15281
|
+
* a bottom sheet. That is why this effect exists: a click on a sheet trigger
|
|
15282
|
+
* arms a short window and the next body-level menu to land is OURS to mark —
|
|
15283
|
+
* while a marked menu still on the page (the close click) keeps the window
|
|
15284
|
+
* shut.
|
|
15139
15285
|
*/
|
|
15140
15286
|
const PORTAL_MENU_SELECTOR = 'body > div[role="menu"]';
|
|
15141
15287
|
/** The marker composer.css.ts section 4 styles into the bottom sheet. */
|
|
15142
15288
|
const SHEET_MARKER = 'data-zen-sheet';
|
|
15143
|
-
const
|
|
15289
|
+
const SHEET_VALUE_PERM = 'perm';
|
|
15290
|
+
const SHEET_VALUE_WORKSPACE = 'workspace';
|
|
15291
|
+
/**
|
|
15292
|
+
* UX-3: the switch lock. Picking a workspace in the hero sheet runs
|
|
15293
|
+
* `selectWorkspace` → `POST /api/session/create` → `replaceMain` — the
|
|
15294
|
+
* conversation unmounts and remounts (`useMaybeIncarnation` epoch+1) and the
|
|
15295
|
+
* composer briefly still belongs to the OLD blank session. A message sent in
|
|
15296
|
+
* that window lands on the old session and is then wiped by the remount,
|
|
15297
|
+
* bouncing the UI back to the "choose a workspace" placeholder. While the
|
|
15298
|
+
* `data-zen-ws-switching` attribute sits on `<html>`, composer.css dead-zones
|
|
15299
|
+
* and dims the composer bar; the lock releases the moment the conversation
|
|
15300
|
+
* root's `data-conversation-session` has MOVED and the placeholder card is
|
|
15301
|
+
* gone — with a hard cap so a failed/no-op switch cannot freeze the input.
|
|
15302
|
+
*/
|
|
15303
|
+
const SWITCH_MARKER = 'data-zen-ws-switching';
|
|
15304
|
+
const SWITCH_LOCK_MS = 1_500;
|
|
15305
|
+
const SESSION_ATTR = 'data-conversation-session';
|
|
15306
|
+
const SESSION_SELECTOR = `[${SESSION_ATTR}]`;
|
|
15307
|
+
/** The hero's inert "choose a workspace" state: the whole input card IS the trigger. */
|
|
15308
|
+
const PLACEHOLDER_CARD_SELECTOR = '[data-composer-card][class*="_cardWorkspaceTrigger"]';
|
|
15309
|
+
/** Spelled literally (not interpolated) — the test pins this exact text as the perm/workspace discriminator. */
|
|
15310
|
+
const WS_MENUITEM_SELECTOR = 'body > div[role="menu"][data-zen-sheet="workspace"] button[role="menuitem"]';
|
|
15144
15311
|
/**
|
|
15145
15312
|
* Whether ONE body-level node is a portal menu that does not yet carry the
|
|
15146
15313
|
* sheet marker. Pure over the node's own methods: `matches` answers the
|
|
@@ -15149,17 +15316,52 @@ const SHEET_VALUE = 'perm';
|
|
|
15149
15316
|
function isUnmarkedPortalMenu(node) {
|
|
15150
15317
|
return node.matches(PORTAL_MENU_SELECTOR) && node.getAttribute(SHEET_MARKER) === null;
|
|
15151
15318
|
}
|
|
15319
|
+
const inSeat = (seat) => seat !== undefined && seat !== null;
|
|
15152
15320
|
/**
|
|
15153
|
-
*
|
|
15154
|
-
*
|
|
15155
|
-
*
|
|
15156
|
-
* the menus they open are
|
|
15321
|
+
* Which sheet ONE click event's target arms: the phone shell only. The
|
|
15322
|
+
* permission seat's trigger arms `perm` (T70); the hero page's two
|
|
15323
|
+
* workspace triggers arm `workspace` (UX-3). Any other button — the
|
|
15324
|
+
* sidebar's 「…」, a copy button — arms nothing, so the menus they open are
|
|
15325
|
+
* never marked.
|
|
15157
15326
|
*/
|
|
15158
|
-
function
|
|
15327
|
+
function sheetKindForTrigger(target, phoneMatches) {
|
|
15159
15328
|
if (!phoneMatches)
|
|
15329
|
+
return null;
|
|
15330
|
+
if (inSeat(target?.closest(PERM_TRIGGER_SELECTOR)))
|
|
15331
|
+
return 'perm';
|
|
15332
|
+
if (inSeat(target?.closest(WORKSPACE_TRIGGER_SELECTOR)))
|
|
15333
|
+
return 'workspace';
|
|
15334
|
+
return null;
|
|
15335
|
+
}
|
|
15336
|
+
/**
|
|
15337
|
+
* UX-3: whether a pointerdown landed inside a WORKSPACE trigger — the only
|
|
15338
|
+
* triggers whose open/close suffers the outside-pointerdown + toggle-click
|
|
15339
|
+
* race (the Menu closes the portaled sheet on the outside pointerdown, and
|
|
15340
|
+
* the trigger's own click would flip it straight back open). The permission
|
|
15341
|
+
* trigger toggles by click alone and is never suppressed.
|
|
15342
|
+
*/
|
|
15343
|
+
function pointerdownOnWorkspaceTrigger(target) {
|
|
15344
|
+
return inSeat(target?.closest(WORKSPACE_TRIGGER_SELECTOR));
|
|
15345
|
+
}
|
|
15346
|
+
/**
|
|
15347
|
+
* UX-3: whether a click started on a workspace item inside the MARKED
|
|
15348
|
+
* workspace sheet — the pick that starts the switch lock. Keyed on the
|
|
15349
|
+
* `workspace` marker value, so a permission option can never lock the
|
|
15350
|
+
* composer.
|
|
15351
|
+
*/
|
|
15352
|
+
function menuitemClickStartsWorkspaceSwitch(target) {
|
|
15353
|
+
return inSeat(target?.closest(WS_MENUITEM_SELECTOR));
|
|
15354
|
+
}
|
|
15355
|
+
/**
|
|
15356
|
+
* Whether the workspace switch has settled: the conversation root now names
|
|
15357
|
+
* a DIFFERENT session than the one the lock was taken on, and the no-workspace
|
|
15358
|
+
* placeholder card is gone (the optimistic `pendingWorkspaceId` display alone
|
|
15359
|
+
* is not enough — the remount resets it).
|
|
15360
|
+
*/
|
|
15361
|
+
function workspaceSwitchSettled(probe, lockedSessionValue) {
|
|
15362
|
+
if (probe.currentSessionValue() === lockedSessionValue)
|
|
15160
15363
|
return false;
|
|
15161
|
-
|
|
15162
|
-
return seat !== undefined && seat !== null;
|
|
15364
|
+
return !probe.placeholderOpen();
|
|
15163
15365
|
}
|
|
15164
15366
|
/**
|
|
15165
15367
|
* How long an OPEN click stays armed. The portal lands in the same React
|
|
@@ -15178,18 +15380,36 @@ const ARM_MS = 1_500;
|
|
|
15178
15380
|
* selectors stopped matching, and the phone got a tiny floating popup. The
|
|
15179
15381
|
* host offers no stable attribute to key the portal off — the anchor is a
|
|
15180
15382
|
* 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
|
|
15383
|
+
* marks it: the trigger's click arms a short window, the observer stamps the
|
|
15384
|
+
* next body-level menu with `data-zen-sheet`, and the stylesheet (which
|
|
15183
15385
|
* only the phone shell loads) turns THAT into the sheet. The portal menu
|
|
15184
15386
|
* unmounts on close, so the marker's lifetime is exactly the menu's; the
|
|
15185
15387
|
* 0.1.7 inline structure keeps working through the untouched descendant
|
|
15186
15388
|
* selectors, and the desktop shell never loads this stylesheet at all.
|
|
15389
|
+
*
|
|
15390
|
+
* UX-3 extends the same machinery to the hero page's workspace menu:
|
|
15391
|
+
* - the two workspace triggers arm a `data-zen-sheet="workspace"` sheet;
|
|
15392
|
+
* - a pointerdown on a workspace trigger while a marked sheet is up
|
|
15393
|
+
* suppresses the trigger's own click, so one tap reliably CLOSES (the
|
|
15394
|
+
* host's outside-pointerdown close stands instead of being undone by the
|
|
15395
|
+
* toggle click);
|
|
15396
|
+
* - a pick inside the workspace sheet locks the composer for the length of
|
|
15397
|
+
* the `selectWorkspace` remount (see SWITCH_MARKER above).
|
|
15187
15398
|
*/
|
|
15188
15399
|
function installPermissionSheet(ctx) {
|
|
15189
15400
|
ctx.effect(() => {
|
|
15190
15401
|
const phone = window.matchMedia(PHONE_QUERY);
|
|
15191
15402
|
let armed = false;
|
|
15403
|
+
let armedKind = SHEET_VALUE_PERM;
|
|
15192
15404
|
let armTimer;
|
|
15405
|
+
// UX-3 toggle debounce: recomputed on EVERY pointerdown, consumed by the
|
|
15406
|
+
// next click — a pointerdown whose click never comes (drag cancel) cannot
|
|
15407
|
+
// leave a stale swallow behind.
|
|
15408
|
+
let suppressClick = false;
|
|
15409
|
+
// Set when a swallowed click found the marked menu STILL on the page
|
|
15410
|
+
// (the pointerdown close did not land): the next tap passes through to
|
|
15411
|
+
// the host's own toggle instead of being swallowed forever.
|
|
15412
|
+
let allowToggle = false;
|
|
15193
15413
|
const disarm = () => {
|
|
15194
15414
|
armed = false;
|
|
15195
15415
|
if (armTimer !== undefined) {
|
|
@@ -15197,9 +15417,75 @@ function installPermissionSheet(ctx) {
|
|
|
15197
15417
|
armTimer = undefined;
|
|
15198
15418
|
}
|
|
15199
15419
|
};
|
|
15420
|
+
const markedSheet = () => document.querySelector(`${PORTAL_MENU_SELECTOR}[${SHEET_MARKER}]`);
|
|
15421
|
+
const readSessionValue = () => document.querySelector(SESSION_SELECTOR)?.getAttribute(SESSION_ATTR) ?? null;
|
|
15422
|
+
const placeholderOpen = () => document.querySelector(PLACEHOLDER_CARD_SELECTOR) !== null;
|
|
15423
|
+
// -- UX-3 switch lock -------------------------------------------------
|
|
15424
|
+
let lockedSessionValue = null;
|
|
15425
|
+
let lockTimer;
|
|
15426
|
+
let lockObserver;
|
|
15427
|
+
const releaseLock = () => {
|
|
15428
|
+
if (lockTimer !== undefined) {
|
|
15429
|
+
clearTimeout(lockTimer);
|
|
15430
|
+
lockTimer = undefined;
|
|
15431
|
+
}
|
|
15432
|
+
lockObserver?.disconnect();
|
|
15433
|
+
lockObserver = undefined;
|
|
15434
|
+
lockedSessionValue = null;
|
|
15435
|
+
document.documentElement.removeAttribute(SWITCH_MARKER);
|
|
15436
|
+
};
|
|
15437
|
+
const checkLock = () => {
|
|
15438
|
+
if (lockedSessionValue === null)
|
|
15439
|
+
return;
|
|
15440
|
+
if (workspaceSwitchSettled({ currentSessionValue: readSessionValue, placeholderOpen }, lockedSessionValue)) {
|
|
15441
|
+
releaseLock();
|
|
15442
|
+
}
|
|
15443
|
+
};
|
|
15444
|
+
const startSwitchLock = () => {
|
|
15445
|
+
if (lockedSessionValue === null) {
|
|
15446
|
+
lockedSessionValue = readSessionValue();
|
|
15447
|
+
document.documentElement.setAttribute(SWITCH_MARKER, '1');
|
|
15448
|
+
lockObserver = new MutationObserver(checkLock);
|
|
15449
|
+
// The session id moves on the conversation root (attribute) and the
|
|
15450
|
+
// placeholder card mounts/unmounts (childList) — plus class flips on
|
|
15451
|
+
// the card are attribute mutations too. One bounded subtree watch
|
|
15452
|
+
// covers all of it.
|
|
15453
|
+
lockObserver.observe(document.documentElement, { childList: true, subtree: true, attributes: true });
|
|
15454
|
+
}
|
|
15455
|
+
if (lockTimer !== undefined)
|
|
15456
|
+
clearTimeout(lockTimer);
|
|
15457
|
+
lockTimer = setTimeout(releaseLock, SWITCH_LOCK_MS);
|
|
15458
|
+
};
|
|
15459
|
+
// ---------------------------------------------------------------------
|
|
15460
|
+
const onCapturePointerDown = (event) => {
|
|
15461
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
15462
|
+
const onTrigger = pointerdownOnWorkspaceTrigger(target);
|
|
15463
|
+
suppressClick = onTrigger && markedSheet() !== null && !allowToggle;
|
|
15464
|
+
if (!suppressClick)
|
|
15465
|
+
allowToggle = false;
|
|
15466
|
+
};
|
|
15200
15467
|
const onCaptureClick = (event) => {
|
|
15468
|
+
// UX-3: let the outside-pointerdown close stand — stopPropagation
|
|
15469
|
+
// keeps the click from React's root listeners, so the trigger cannot
|
|
15470
|
+
// toggle the sheet straight back open.
|
|
15471
|
+
if (suppressClick) {
|
|
15472
|
+
suppressClick = false;
|
|
15473
|
+
if (markedSheet() !== null)
|
|
15474
|
+
allowToggle = true;
|
|
15475
|
+
event.preventDefault();
|
|
15476
|
+
event.stopPropagation();
|
|
15477
|
+
return;
|
|
15478
|
+
}
|
|
15201
15479
|
const target = event.target instanceof Element ? event.target : null;
|
|
15202
|
-
|
|
15480
|
+
// UX-3: a pick inside the workspace sheet starts the switch lock — the
|
|
15481
|
+
// composer must not accept input on the old blank session while
|
|
15482
|
+
// selectWorkspace remounts the conversation underneath it.
|
|
15483
|
+
if (phone.matches && menuitemClickStartsWorkspaceSwitch(target)) {
|
|
15484
|
+
startSwitchLock();
|
|
15485
|
+
return;
|
|
15486
|
+
}
|
|
15487
|
+
const kind = sheetKindForTrigger(target, phone.matches);
|
|
15488
|
+
if (kind === null)
|
|
15203
15489
|
return;
|
|
15204
15490
|
// T70-fix: the trigger TOGGLES. A click that CLOSES the menu lands
|
|
15205
15491
|
// while the marked menu is still in the DOM (capture runs before the
|
|
@@ -15208,9 +15494,10 @@ function installPermissionSheet(ctx) {
|
|
|
15208
15494
|
// body-level menu opening inside that window would be mis-marked as
|
|
15209
15495
|
// ours. A marked menu present at click time means this click can only
|
|
15210
15496
|
// be the close (or a no-op re-open), never an opening we must catch.
|
|
15211
|
-
if (
|
|
15497
|
+
if (markedSheet() !== null)
|
|
15212
15498
|
return;
|
|
15213
15499
|
armed = true;
|
|
15500
|
+
armedKind = kind === 'workspace' ? SHEET_VALUE_WORKSPACE : SHEET_VALUE_PERM;
|
|
15214
15501
|
if (armTimer !== undefined)
|
|
15215
15502
|
clearTimeout(armTimer);
|
|
15216
15503
|
armTimer = setTimeout(disarm, ARM_MS);
|
|
@@ -15220,11 +15507,12 @@ function installPermissionSheet(ctx) {
|
|
|
15220
15507
|
if (!armed)
|
|
15221
15508
|
return;
|
|
15222
15509
|
disarm();
|
|
15223
|
-
root.setAttribute(SHEET_MARKER,
|
|
15510
|
+
root.setAttribute(SHEET_MARKER, armedKind);
|
|
15224
15511
|
};
|
|
15225
15512
|
// Capture phase: the host's own handlers may stop the click's bubble
|
|
15226
15513
|
// (the trigger toggles `open` itself), so listen on the document in
|
|
15227
15514
|
// capture and never depend on propagation reaching us.
|
|
15515
|
+
document.addEventListener('pointerdown', onCapturePointerDown, true);
|
|
15228
15516
|
document.addEventListener('click', onCaptureClick, true);
|
|
15229
15517
|
const observer = new MutationObserver((records) => {
|
|
15230
15518
|
if (!armed)
|
|
@@ -15238,9 +15526,11 @@ function installPermissionSheet(ctx) {
|
|
|
15238
15526
|
});
|
|
15239
15527
|
observer.observe(document.body, { childList: true });
|
|
15240
15528
|
return () => {
|
|
15529
|
+
document.removeEventListener('pointerdown', onCapturePointerDown, true);
|
|
15241
15530
|
document.removeEventListener('click', onCaptureClick, true);
|
|
15242
15531
|
observer.disconnect();
|
|
15243
15532
|
disarm();
|
|
15533
|
+
releaseLock();
|
|
15244
15534
|
// A still-open marked menu outliving this effect keeps its marker only
|
|
15245
15535
|
// until the user closes it (the element unmounts); scrubbing here
|
|
15246
15536
|
// would fight a React commit mid-flight for no visual gain.
|
|
@@ -15276,36 +15566,75 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
15276
15566
|
exports.installNativeTriggerOverlay = installNativeTriggerOverlay;
|
|
15277
15567
|
const PHONE = '(max-width: 767px)';
|
|
15278
15568
|
const HEADER = '[data-phase] header';
|
|
15279
|
-
/**
|
|
15569
|
+
/**
|
|
15570
|
+
* Pill → the official trigger it fronts. Same ARIA split the pills use,
|
|
15571
|
+
* narrowed by the action-button class tail (UX-2, 2026-11-04): the bare
|
|
15572
|
+
* `button[aria-haspopup="tree"]` matched the breadcrumb's parent-session
|
|
15573
|
+
* switcher first (`…_switcherTrigger` — also a tree popup, earlier in
|
|
15574
|
+
* document order), so the pill got the wrong button laid over it. The
|
|
15575
|
+
* suffix matcher is case-sensitive and `_switcherTrigger` does not end in
|
|
15576
|
+
* `_trigger`, so the catalog trigger is the only match left.
|
|
15577
|
+
*/
|
|
15280
15578
|
const PAIRS = [
|
|
15281
|
-
{ pill: '[data-activity-kind="subagent"]', trigger: `${HEADER} button[aria-haspopup="tree"]` },
|
|
15579
|
+
{ pill: '[data-activity-kind="subagent"]', trigger: `${HEADER} button[class$="_trigger"][aria-haspopup="tree"]` },
|
|
15282
15580
|
{ pill: '[data-activity-kind="job"]', trigger: `${HEADER} button[class$="_trigger"]:not([aria-haspopup])` },
|
|
15283
15581
|
];
|
|
15284
|
-
/** Marks a trigger we have taken over, so cleanup can find
|
|
15582
|
+
/** Marks a trigger (and its anchor container) we have taken over, so cleanup can find them again. */
|
|
15285
15583
|
const TAKEN = 'data-zen-overlay';
|
|
15584
|
+
/** The shared fixed-viewport box: exactly what the portalled popover measures against. */
|
|
15585
|
+
function pin(el, r, display) {
|
|
15586
|
+
el.style.setProperty('position', 'fixed', 'important');
|
|
15587
|
+
el.style.setProperty('left', `${r.left}px`, 'important');
|
|
15588
|
+
el.style.setProperty('top', `${r.top}px`, 'important');
|
|
15589
|
+
el.style.setProperty('width', `${r.width}px`, 'important');
|
|
15590
|
+
el.style.setProperty('height', `${r.height}px`, 'important');
|
|
15591
|
+
el.style.setProperty('margin', '0', 'important');
|
|
15592
|
+
el.style.setProperty('padding', '0', 'important');
|
|
15593
|
+
el.style.setProperty('border', '0', 'important');
|
|
15594
|
+
if (display !== undefined)
|
|
15595
|
+
el.style.setProperty('display', display, 'important');
|
|
15596
|
+
}
|
|
15286
15597
|
function place(trigger, pill) {
|
|
15287
15598
|
const r = pill.getBoundingClientRect();
|
|
15288
15599
|
if (r.width === 0 || r.height === 0)
|
|
15289
15600
|
return;
|
|
15290
15601
|
trigger.setAttribute(TAKEN, '');
|
|
15602
|
+
pin(trigger, r);
|
|
15291
15603
|
// Fixed, not absolute: the header's containing block is not the pill's, and
|
|
15292
15604
|
// 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
15605
|
trigger.style.setProperty('opacity', '0', 'important');
|
|
15302
15606
|
trigger.style.setProperty('overflow', 'hidden', 'important');
|
|
15303
15607
|
trigger.style.setProperty('pointer-events', 'auto', 'important');
|
|
15304
15608
|
trigger.style.setProperty('z-index', '3', 'important');
|
|
15609
|
+
// The official popover (useAnchoredPosition) sizes itself from the OUTER
|
|
15610
|
+
// anchor container — the trigger's parent div, the official rootRef — not
|
|
15611
|
+
// the button. header.css.ts dissolves that container with
|
|
15612
|
+
// display:contents, so its rect was all zeros and the panel landed at
|
|
15613
|
+
// 12,12 inside the status bar (UX-2). Give the anchor the pill's exact
|
|
15614
|
+
// box: the inline display beats the stylesheet's display:contents
|
|
15615
|
+
// !important (the style attribute outranks selectors at author-important),
|
|
15616
|
+
// so rootRef.getBoundingClientRect() finally sees the pill. pointer-events
|
|
15617
|
+
// none keeps the invisible box from ever stealing a tap — the trigger
|
|
15618
|
+
// child above it is the tappable one. z-index 3 is required on the anchor
|
|
15619
|
+
// too: position:fixed unconditionally creates a stacking context (even at
|
|
15620
|
+
// z-index:auto, stack level 0), which otherwise traps the trigger's own
|
|
15621
|
+
// z-index:3 underneath [data-mobile-nav="header-activity"] (z-index:1).
|
|
15622
|
+
const anchor = trigger.parentElement;
|
|
15623
|
+
if (anchor !== null && anchor.closest(HEADER) !== null) {
|
|
15624
|
+
anchor.setAttribute(TAKEN, '');
|
|
15625
|
+
pin(anchor, r, 'block');
|
|
15626
|
+
anchor.style.setProperty('pointer-events', 'none', 'important');
|
|
15627
|
+
anchor.style.setProperty('z-index', '3', 'important');
|
|
15628
|
+
}
|
|
15305
15629
|
}
|
|
15306
15630
|
function release(trigger) {
|
|
15307
15631
|
trigger.removeAttribute(TAKEN);
|
|
15308
15632
|
trigger.style.cssText = '';
|
|
15633
|
+
const anchor = trigger.parentElement;
|
|
15634
|
+
if (anchor !== null && anchor.hasAttribute(TAKEN)) {
|
|
15635
|
+
anchor.removeAttribute(TAKEN);
|
|
15636
|
+
anchor.style.cssText = '';
|
|
15637
|
+
}
|
|
15309
15638
|
}
|
|
15310
15639
|
function installNativeTriggerOverlay(ctx) {
|
|
15311
15640
|
ctx.effect(() => {
|
|
@@ -17593,6 +17922,11 @@ function SettingsSectionPage({ config, shares, t }) {
|
|
|
17593
17922
|
setClaimBusy(true);
|
|
17594
17923
|
setClaimFail(null);
|
|
17595
17924
|
setClaimWriteFailed(false);
|
|
17925
|
+
// UX-1: captured BEFORE the pairing — whether the page ran as client, and
|
|
17926
|
+
// whether the backend's stream interceptor was already active per the
|
|
17927
|
+
// last client/status diagnostics.
|
|
17928
|
+
const wasEffectiveClient = clientRole;
|
|
17929
|
+
const streamInterceptAlreadyActive = clientView?.intercept?.installed === true;
|
|
17596
17930
|
// T60: one name for both halves of the round-trip — trimmed and legal
|
|
17597
17931
|
// ({@link claimDeviceNameOf}), sent to the server AND written into the
|
|
17598
17932
|
// row when the claim lands.
|
|
@@ -17625,6 +17959,20 @@ function SettingsSectionPage({ config, shares, t }) {
|
|
|
17625
17959
|
// could not store falls back to the name we actually sent.
|
|
17626
17960
|
const echoed = typeof body.deviceName === 'string' && body.deviceName !== '' ? (0, settings_form_ts_1.claimDeviceNameOf)(body.deviceName, claimName) : claimName;
|
|
17627
17961
|
const landed = await config.writeClientPairing(urlToWrite, body.token, echoed);
|
|
17962
|
+
if (landed && (0, settings_form_ts_1.shouldReloadAfterClientActivation)({
|
|
17963
|
+
wasEffectiveClient,
|
|
17964
|
+
nowClientRole: true,
|
|
17965
|
+
savedOrPaired: true,
|
|
17966
|
+
streamInterceptAlreadyActive,
|
|
17967
|
+
})) {
|
|
17968
|
+
// UX-1: FIRST pairing on a page whose resident global streams were
|
|
17969
|
+
// opened unwrapped (or with the interceptor inactive) — wait out the
|
|
17970
|
+
// backend's row-reload window, then reload so the streams reopen
|
|
17971
|
+
// wrapped and the server's sessions enter the sidebar.
|
|
17972
|
+
setClaimBusy(false);
|
|
17973
|
+
void (0, settings_form_ts_1.awaitClientStatusReady)().then(() => window.location.reload());
|
|
17974
|
+
return;
|
|
17975
|
+
}
|
|
17628
17976
|
if (landed) {
|
|
17629
17977
|
// The draft did its job — it now equals the displayed value, and
|
|
17630
17978
|
// keeping it would be a phantom edit (T60).
|
|
@@ -17753,8 +18101,29 @@ function SettingsSectionPage({ config, shares, t }) {
|
|
|
17753
18101
|
? t('settings.statusUnknown')
|
|
17754
18102
|
: 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
18103
|
const reloadsRow = saveReloadsRow();
|
|
17756
|
-
|
|
17757
|
-
|
|
18104
|
+
// UX-1: captured BEFORE the save — whether the page ran as client,
|
|
18105
|
+
// and whether the backend's stream interceptor was already active
|
|
18106
|
+
// per the last client/status diagnostics.
|
|
18107
|
+
const wasEffectiveClient = clientRole;
|
|
18108
|
+
const streamInterceptAlreadyActive = clientView?.intercept?.installed === true;
|
|
18109
|
+
void config.save().then((landed) => {
|
|
18110
|
+
if (!landed)
|
|
18111
|
+
return;
|
|
18112
|
+
if ((0, settings_form_ts_1.shouldReloadAfterClientActivation)({
|
|
18113
|
+
wasEffectiveClient,
|
|
18114
|
+
nowClientRole: config.savedRoleIsClient(),
|
|
18115
|
+
savedOrPaired: true,
|
|
18116
|
+
streamInterceptAlreadyActive,
|
|
18117
|
+
})) {
|
|
18118
|
+
// UX-1: the save first-activated the client interceptor while
|
|
18119
|
+
// this page's resident global streams were opened unwrapped —
|
|
18120
|
+
// wait out the row-reload window, then reload so the streams
|
|
18121
|
+
// reopen wrapped and the server's sessions enter the sidebar.
|
|
18122
|
+
void (0, settings_form_ts_1.awaitClientStatusReady)().then(() => window.location.reload());
|
|
18123
|
+
return;
|
|
18124
|
+
}
|
|
18125
|
+
saveLanded(reloadsRow);
|
|
18126
|
+
});
|
|
17758
18127
|
}, 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
18128
|
let current = {};
|
|
17760
18129
|
try {
|
|
@@ -19620,6 +19989,12 @@ function GatewayWizard(props) {
|
|
|
19620
19989
|
const runClientClaim = async () => {
|
|
19621
19990
|
setFormError(null);
|
|
19622
19991
|
setClientClaimFail(null);
|
|
19992
|
+
// UX-1: the facts that decide the post-pairing reload — whether the page
|
|
19993
|
+
// opened as an already-effective client, and whether the backend's
|
|
19994
|
+
// stream interceptor was already active (from the last client/status
|
|
19995
|
+
// diagnostics). Captured BEFORE the role switch and the claim below.
|
|
19996
|
+
const wasEffectiveClient = effectiveClient;
|
|
19997
|
+
const streamInterceptAlreadyActive = clientView?.intercept?.installed === true;
|
|
19623
19998
|
const trimmedUrl = clientServerUrl.trim();
|
|
19624
19999
|
const normalizedCode = (0, settings_form_ts_1.normalizePairingCode)(clientPairCode);
|
|
19625
20000
|
if (trimmedUrl === '' || !/^https?:\/\//i.test(trimmedUrl)) {
|
|
@@ -19661,7 +20036,21 @@ function GatewayWizard(props) {
|
|
|
19661
20036
|
if (body?.ok === true && typeof body.token === 'string' && body.token !== '') {
|
|
19662
20037
|
const urlToWrite = typeof body.serverUrl === 'string' && body.serverUrl !== '' ? body.serverUrl : trimmedUrl;
|
|
19663
20038
|
const echoed = typeof body.deviceName === 'string' && body.deviceName.trim() !== '' ? body.deviceName.trim() : deviceName;
|
|
19664
|
-
await config.writeClientPairing(urlToWrite, body.token, echoed);
|
|
20039
|
+
const landed = await config.writeClientPairing(urlToWrite, body.token, echoed);
|
|
20040
|
+
if (landed && (0, settings_form_ts_1.shouldReloadAfterClientActivation)({
|
|
20041
|
+
wasEffectiveClient,
|
|
20042
|
+
nowClientRole: true,
|
|
20043
|
+
savedOrPaired: true,
|
|
20044
|
+
streamInterceptAlreadyActive,
|
|
20045
|
+
})) {
|
|
20046
|
+
// UX-1: this was the FIRST client activation while the page's three
|
|
20047
|
+
// resident global streams were opened unwrapped — wait out the
|
|
20048
|
+
// backend's row-reload window, then reload so the streams reopen
|
|
20049
|
+
// wrapped and the server's shared sessions enter the sidebar.
|
|
20050
|
+
await (0, settings_form_ts_1.awaitClientStatusReady)();
|
|
20051
|
+
window.location.reload();
|
|
20052
|
+
return;
|
|
20053
|
+
}
|
|
19665
20054
|
setClientPairCode('');
|
|
19666
20055
|
await loadClientStatus();
|
|
19667
20056
|
setStep('verify');
|