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
|
@@ -42,6 +42,20 @@ export const LAYOUT_CSS = `/* ---------- mobile-only layout ---------- */
|
|
|
42
42
|
padding-top: var(--mnav-sat) !important;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/* DSH 0.2.2-rc.1+: AppFrame pins centerCol to grid-area: 1/2 (column 2,
|
|
46
|
+
which is 0px wide in our 1-column mobile grid) and inserts a
|
|
47
|
+
[data-sidebar-mask] div before sidebarCol when viewport < 768. Pin
|
|
48
|
+
centerCol to column 1 so it fills the 1fr track, hide the host mask
|
|
49
|
+
(MobileNavOverlay renders its own backdrop on tablet, and MobileHome
|
|
50
|
+
replaces the drawer on phone), and match sidebarCol by its stable class
|
|
51
|
+
suffix alongside :first-child. */
|
|
52
|
+
[data-mobile-nav="frame"] > [class$="_centerCol"] {
|
|
53
|
+
grid-area: 1 / 1 !important;
|
|
54
|
+
}
|
|
55
|
+
[data-mobile-nav="frame"] > [data-sidebar-mask] {
|
|
56
|
+
display: none !important;
|
|
57
|
+
}
|
|
58
|
+
|
|
45
59
|
/* The sidebar column (first grid child) becomes a left drawer. The drawer
|
|
46
60
|
hugs the sidebar content exactly (the wide sidebar carries an inline
|
|
47
61
|
width, ~280px): a fixed 92vw box would leave a white strip where the
|
|
@@ -52,7 +66,8 @@ export const LAYOUT_CSS = `/* ---------- mobile-only layout ---------- */
|
|
|
52
66
|
before) left 14px of the drawer plus a long 32px-blur shadow gradient
|
|
53
67
|
visible along the left edge of the main UI. No box-shadow at all: the
|
|
54
68
|
dimmed backdrop already separates drawer from content. */
|
|
55
|
-
[data-mobile-nav="frame"] > :first-child
|
|
69
|
+
[data-mobile-nav="frame"] > :first-child,
|
|
70
|
+
[data-mobile-nav="frame"] > [class$="_sidebarCol"] {
|
|
56
71
|
position: absolute !important;
|
|
57
72
|
inset: 0 auto 0 0 !important;
|
|
58
73
|
width: max-content !important;
|
|
@@ -92,6 +107,31 @@ export const LAYOUT_CSS = `/* ---------- mobile-only layout ---------- */
|
|
|
92
107
|
padding-top: var(--mnav-sat) !important;
|
|
93
108
|
padding-bottom: var(--mnav-sab) !important;
|
|
94
109
|
}
|
|
110
|
+
/* Right sidebar start/guide page (dsh-client-ui-sidebar-right GuideBody):
|
|
111
|
+
the host sets .Dv5QdW_tabBody { height: 100%; overflow: hidden } and
|
|
112
|
+
.F6Huca_guide { min-height: 100%; justify-content: center } with no
|
|
113
|
+
overflow-y. When many tab entries are registered (e.g. better-sidebar +
|
|
114
|
+
plugins), the centered flex column overflows equally out the top and
|
|
115
|
+
bottom of the locked 100%-height box — clipping the leading entries
|
|
116
|
+
("文件", "Git") above y=0 with no way to scroll. Make the guide itself
|
|
117
|
+
the scroll container and replace justify-content:center with auto
|
|
118
|
+
margins on the first item and ::after so short lists stay centered
|
|
119
|
+
while long lists start cleanly at the top and scroll end-to-end. */
|
|
120
|
+
[data-sidebar-right-guide] {
|
|
121
|
+
flex: 1 1 0 !important;
|
|
122
|
+
min-height: 0 !important;
|
|
123
|
+
justify-content: flex-start !important;
|
|
124
|
+
overflow-y: auto !important;
|
|
125
|
+
-webkit-overflow-scrolling: touch;
|
|
126
|
+
padding-top: 24px !important;
|
|
127
|
+
padding-bottom: 24px !important;
|
|
128
|
+
}
|
|
129
|
+
[data-sidebar-right-guide] > :first-child {
|
|
130
|
+
margin-top: auto !important;
|
|
131
|
+
}
|
|
132
|
+
[data-sidebar-right-guide]::after {
|
|
133
|
+
margin-bottom: auto !important;
|
|
134
|
+
}
|
|
95
135
|
|
|
96
136
|
/* Expanded state (frame without data-sidebar-collapsed) slides the drawer in.
|
|
97
137
|
The open state must be transform:none — NOT translateX(0): an identity
|
|
@@ -102,7 +142,8 @@ export const LAYOUT_CSS = `/* ---------- mobile-only layout ---------- */
|
|
|
102
142
|
overflow:hidden drawer to scrollLeft=102, and every static child (plus the
|
|
103
143
|
fixed overlay) shifts 102px off-screen. With transform:none the overlay is
|
|
104
144
|
viewport-anchored: it dims the full screen and the sheet sits at left:8. */
|
|
105
|
-
[data-mobile-nav="frame"]:not([data-sidebar-collapsed]) > :first-child
|
|
145
|
+
[data-mobile-nav="frame"]:not([data-sidebar-collapsed]) > :first-child,
|
|
146
|
+
[data-mobile-nav="frame"]:not([data-sidebar-collapsed]) > [class$="_sidebarCol"] {
|
|
106
147
|
transform: none !important;
|
|
107
148
|
}
|
|
108
149
|
|
|
@@ -33,11 +33,13 @@ import {
|
|
|
33
33
|
CLIENT_CONFIG_ROUTE,
|
|
34
34
|
CLIENT_RECONNECT_ROUTE,
|
|
35
35
|
CLIENT_STATUS_ROUTE,
|
|
36
|
+
awaitClientStatusReady,
|
|
36
37
|
claimDeviceNameOf,
|
|
37
38
|
clientStatusLineOf,
|
|
38
39
|
deriveClientStatusView,
|
|
39
40
|
deriveSettingsView,
|
|
40
41
|
normalizePairingCode,
|
|
42
|
+
shouldReloadAfterClientActivation,
|
|
41
43
|
} from '../../client-data/settings-form.ts'
|
|
42
44
|
import { qrMatrix } from '../../client-data/qr.ts'
|
|
43
45
|
import {
|
|
@@ -761,6 +763,12 @@ export function GatewayWizard(props: GatewayWizardProps): ReactNode {
|
|
|
761
763
|
const runClientClaim = async (): Promise<void> => {
|
|
762
764
|
setFormError(null)
|
|
763
765
|
setClientClaimFail(null)
|
|
766
|
+
// UX-1: the facts that decide the post-pairing reload — whether the page
|
|
767
|
+
// opened as an already-effective client, and whether the backend's
|
|
768
|
+
// stream interceptor was already active (from the last client/status
|
|
769
|
+
// diagnostics). Captured BEFORE the role switch and the claim below.
|
|
770
|
+
const wasEffectiveClient = effectiveClient
|
|
771
|
+
const streamInterceptAlreadyActive = clientView?.intercept?.installed === true
|
|
764
772
|
const trimmedUrl = clientServerUrl.trim()
|
|
765
773
|
const normalizedCode = normalizePairingCode(clientPairCode)
|
|
766
774
|
if (trimmedUrl === '' || !/^https?:\/\//i.test(trimmedUrl)) {
|
|
@@ -801,7 +809,21 @@ export function GatewayWizard(props: GatewayWizardProps): ReactNode {
|
|
|
801
809
|
if (body?.ok === true && typeof body.token === 'string' && body.token !== '') {
|
|
802
810
|
const urlToWrite = typeof body.serverUrl === 'string' && body.serverUrl !== '' ? body.serverUrl : trimmedUrl
|
|
803
811
|
const echoed = typeof body.deviceName === 'string' && body.deviceName.trim() !== '' ? body.deviceName.trim() : deviceName
|
|
804
|
-
await config.writeClientPairing(urlToWrite, body.token, echoed)
|
|
812
|
+
const landed = await config.writeClientPairing(urlToWrite, body.token, echoed)
|
|
813
|
+
if (landed && shouldReloadAfterClientActivation({
|
|
814
|
+
wasEffectiveClient,
|
|
815
|
+
nowClientRole: true,
|
|
816
|
+
savedOrPaired: true,
|
|
817
|
+
streamInterceptAlreadyActive,
|
|
818
|
+
})) {
|
|
819
|
+
// UX-1: this was the FIRST client activation while the page's three
|
|
820
|
+
// resident global streams were opened unwrapped — wait out the
|
|
821
|
+
// backend's row-reload window, then reload so the streams reopen
|
|
822
|
+
// wrapped and the server's shared sessions enter the sidebar.
|
|
823
|
+
await awaitClientStatusReady()
|
|
824
|
+
window.location.reload()
|
|
825
|
+
return
|
|
826
|
+
}
|
|
805
827
|
setClientPairCode('')
|
|
806
828
|
await loadClientStatus()
|
|
807
829
|
setStep('verify')
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UX-3 — what tapping the phone home screen's "+ 新建会话" FAB should do.
|
|
3
|
+
*
|
|
4
|
+
* Entering a blank session with NO workspace leaves the phone dangling in
|
|
5
|
+
* the hero's inert "choose a workspace" state (the host's `marshal-main`
|
|
6
|
+
* belongs to no workspace, so the "all" filter often resolves to
|
|
7
|
+
* `undefined`). Decide BEFORE entering:
|
|
8
|
+
*
|
|
9
|
+
* - a workspace is already selected → start there;
|
|
10
|
+
* - exactly one workspace exists (or none at all) → start with it
|
|
11
|
+
* (`undefined` keeps a no-workspace host usable);
|
|
12
|
+
* - several workspaces and none selected → the FAB opens the same
|
|
13
|
+
* bottom-sheet workspace drawer the title bar uses, and the session only
|
|
14
|
+
* starts once one is picked.
|
|
15
|
+
*
|
|
16
|
+
* Pure: no DOM, no imports — MobileHome calls it inside the FAB's onClick
|
|
17
|
+
* and test/permission-sheet.test.cjs drives it directly.
|
|
18
|
+
*/
|
|
19
|
+
export type NewSessionAction<W extends string> =
|
|
20
|
+
| { kind: 'start'; workspaceId: W | undefined }
|
|
21
|
+
| { kind: 'pick-workspace' }
|
|
22
|
+
|
|
23
|
+
export function decideNewSessionAction<W extends string>(
|
|
24
|
+
selectedWorkspaceId: W | undefined,
|
|
25
|
+
workspaceIds: readonly W[],
|
|
26
|
+
): NewSessionAction<W> {
|
|
27
|
+
if (selectedWorkspaceId !== undefined) return { kind: 'start', workspaceId: selectedWorkspaceId }
|
|
28
|
+
if (workspaceIds.length <= 1) return { kind: 'start', workspaceId: workspaceIds[0] }
|
|
29
|
+
return { kind: 'pick-workspace' }
|
|
30
|
+
}
|
|
@@ -433,6 +433,63 @@ export function shouldFollowServerDeviceName(deviceName: string, rowName: string
|
|
|
433
433
|
return deviceName !== '' && deviceName !== rowName && !hasDraft
|
|
434
434
|
}
|
|
435
435
|
|
|
436
|
+
// --- UX-1: reload after the FIRST client activation ---------------------------
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Whether one successful client-side activation (a staged-save role switch, a
|
|
440
|
+
* first pairing) needs a page reload: the backend's restart-watcher only
|
|
441
|
+
* installs the stream interceptor when the row reloads into client mode, and
|
|
442
|
+
* the page's three resident global streams were opened under whatever gateway
|
|
443
|
+
* object was current AT LOAD TIME. Without a reload they stay unwrapped and
|
|
444
|
+
* the server's remote sessions never enter the merged sidebar lists.
|
|
445
|
+
*
|
|
446
|
+
* - A write that did not land, or a save that is not heading to `client`,
|
|
447
|
+
* never reloads.
|
|
448
|
+
* - A host→client switch always reloads: the streams predate the interceptor.
|
|
449
|
+
* - An already-client page reloads only when the diagnostics say the
|
|
450
|
+
* interceptor was NOT yet active (`streamInterceptAlreadyActive === false`).
|
|
451
|
+
* `undefined` — an older server that answers no diagnostics — is UNKNOWN and
|
|
452
|
+
* stays on the safe no-reload side (a re-pairing must not needlessly reload).
|
|
453
|
+
*/
|
|
454
|
+
export interface ClientActivationReloadInput {
|
|
455
|
+
/** Whether the page was ALREADY an effective client before this operation. */
|
|
456
|
+
wasEffectiveClient: boolean
|
|
457
|
+
/** Whether the role after the operation is (or is switching to) client. */
|
|
458
|
+
nowClientRole: boolean
|
|
459
|
+
/** Whether this operation actually landed (the save / pairing write). */
|
|
460
|
+
savedOrPaired: boolean
|
|
461
|
+
/** The client/status diagnostics' interceptor flag before the operation
|
|
462
|
+
* (derived by the caller); `undefined` when unknown. */
|
|
463
|
+
streamInterceptAlreadyActive?: boolean
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export function shouldReloadAfterClientActivation(input: ClientActivationReloadInput): boolean {
|
|
467
|
+
if (!input.savedOrPaired || !input.nowClientRole) return false
|
|
468
|
+
if (!input.wasEffectiveClient) return true
|
|
469
|
+
if (input.streamInterceptAlreadyActive === false) return true
|
|
470
|
+
return false
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Poll {@link CLIENT_STATUS_ROUTE} until the backend answers ok — the bounded
|
|
475
|
+
* wait before the post-activation reload, covering the restart-watcher's row
|
|
476
|
+
* reload window (the route 404s / fails while the row restarts). Resolves
|
|
477
|
+
* after `maxMs` regardless, so the reload always fires. Injectable fetch and
|
|
478
|
+
* timings keep the check script fast; defaults are the page's 3 s / 250 ms.
|
|
479
|
+
*/
|
|
480
|
+
export async function awaitClientStatusReady(fetchFn: typeof fetch = fetch, maxMs = 3000, intervalMs = 250): Promise<void> {
|
|
481
|
+
const deadline = Date.now() + maxMs
|
|
482
|
+
while (true) {
|
|
483
|
+
try {
|
|
484
|
+
if ((await fetchFn(CLIENT_STATUS_ROUTE)).ok) return
|
|
485
|
+
} catch {
|
|
486
|
+
// the row is mid-reload; keep polling
|
|
487
|
+
}
|
|
488
|
+
if (Date.now() >= deadline) return
|
|
489
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs))
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
436
493
|
/**
|
|
437
494
|
* Latest-wins sequencing for the status loads: every request takes a ticket,
|
|
438
495
|
* and only the newest ticket may still apply its result. An earlier request
|