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.
Files changed (53) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +2 -2
  3. package/README.zh-CN.md +2 -2
  4. package/docs/interface.md +1 -1
  5. package/docs/remote-access.en.md +1 -1
  6. package/docs/remote-access.md +1 -1
  7. package/lib/adapters/dsh-better-sidebar/index.js +21 -3
  8. package/lib/adapters/dsh-better-sidebar/index.js.map +1 -1
  9. package/lib/client.js +522 -89
  10. package/lib/types/adapters/dsh-better-sidebar/index.d.ts +1 -1
  11. package/lib/types/adapters/dsh-better-sidebar/index.d.ts.map +1 -1
  12. package/lib/types/client/MobileHome.d.ts.map +1 -1
  13. package/lib/types/client/MobileNavOverlay.d.ts.map +1 -1
  14. package/lib/types/client/MobileSessionHeader.d.ts.map +1 -1
  15. package/lib/types/client/effects/native-trigger-overlay.d.ts.map +1 -1
  16. package/lib/types/client/effects/permission-sheet.d.ts +51 -10
  17. package/lib/types/client/effects/permission-sheet.d.ts.map +1 -1
  18. package/lib/types/client/locales.d.ts +1 -0
  19. package/lib/types/client/locales.d.ts.map +1 -1
  20. package/lib/types/client/settings/SettingsSection.d.ts.map +1 -1
  21. package/lib/types/client/styles/chips.css.d.ts +1 -1
  22. package/lib/types/client/styles/chips.css.d.ts.map +1 -1
  23. package/lib/types/client/styles/composer.css.d.ts +1 -1
  24. package/lib/types/client/styles/composer.css.d.ts.map +1 -1
  25. package/lib/types/client/styles/header.css.d.ts +1 -1
  26. package/lib/types/client/styles/header.css.d.ts.map +1 -1
  27. package/lib/types/client/styles/home.css.d.ts +1 -1
  28. package/lib/types/client/styles/home.css.d.ts.map +1 -1
  29. package/lib/types/client/styles/layout.css.d.ts +1 -1
  30. package/lib/types/client/styles/layout.css.d.ts.map +1 -1
  31. package/lib/types/client/wizard/GatewayWizard.d.ts.map +1 -1
  32. package/lib/types/client-data/new-session.d.ts +26 -0
  33. package/lib/types/client-data/new-session.d.ts.map +1 -0
  34. package/lib/types/client-data/settings-form.d.ts +36 -0
  35. package/lib/types/client-data/settings-form.d.ts.map +1 -1
  36. package/package.json +1 -1
  37. package/scripts/check-settings-form.mjs +63 -0
  38. package/src/adapters/dsh-better-sidebar/index.ts +23 -3
  39. package/src/client/MobileHome.tsx +63 -8
  40. package/src/client/MobileNavOverlay.tsx +3 -1
  41. package/src/client/MobileSessionHeader.tsx +7 -1
  42. package/src/client/effects/native-trigger-overlay.ts +50 -11
  43. package/src/client/effects/permission-sheet.ts +199 -22
  44. package/src/client/locales.ts +2 -0
  45. package/src/client/settings/SettingsSection.tsx +43 -1
  46. package/src/client/styles/chips.css.ts +7 -2
  47. package/src/client/styles/composer.css.ts +24 -0
  48. package/src/client/styles/header.css.ts +58 -46
  49. package/src/client/styles/home.css.ts +6 -2
  50. package/src/client/styles/layout.css.ts +43 -2
  51. package/src/client/wizard/GatewayWizard.tsx +23 -1
  52. package/src/client-data/new-session.ts +30 -0
  53. 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