dsh-zen-remote 2.3.5 → 2.3.7

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 (100) hide show
  1. package/AGENTS.md +15 -5
  2. package/CHANGELOG.md +71 -0
  3. package/README.md +2 -2
  4. package/README.zh-CN.md +2 -2
  5. package/docs/interface.md +12 -6
  6. package/docs/plugin-bridge.md +2 -2
  7. package/docs/remote-access.en.md +9 -3
  8. package/docs/remote-access.md +9 -3
  9. package/lan-gate.mjs +7 -0
  10. package/lib/adapters/dsh-better-sidebar/index.js +31 -2
  11. package/lib/adapters/dsh-better-sidebar/index.js.map +1 -1
  12. package/lib/client-pairing.js +10 -0
  13. package/lib/client-pairing.js.map +1 -1
  14. package/lib/client-routes.js +183 -94
  15. package/lib/client-routes.js.map +1 -1
  16. package/lib/client.js +589 -97
  17. package/lib/debug-redact.js +253 -0
  18. package/lib/debug-redact.js.map +1 -0
  19. package/lib/debug-report.js +60 -0
  20. package/lib/debug-report.js.map +1 -0
  21. package/lib/fetch-route-intercept.js +23 -0
  22. package/lib/fetch-route-intercept.js.map +1 -1
  23. package/lib/index.js +306 -45
  24. package/lib/index.js.map +1 -1
  25. package/lib/intercept.js +43 -1
  26. package/lib/intercept.js.map +1 -1
  27. package/lib/log-ring.js +76 -0
  28. package/lib/log-ring.js.map +1 -0
  29. package/lib/plugin-adapter-core.js +30 -1
  30. package/lib/plugin-adapter-core.js.map +1 -1
  31. package/lib/plugin-route-intercept.js +506 -11
  32. package/lib/plugin-route-intercept.js.map +1 -1
  33. package/lib/plugin-tunnel.js +210 -18
  34. package/lib/plugin-tunnel.js.map +1 -1
  35. package/lib/types/adapters/dsh-better-sidebar/index.d.ts +5 -2
  36. package/lib/types/adapters/dsh-better-sidebar/index.d.ts.map +1 -1
  37. package/lib/types/client/MobileSessionInfo.d.ts.map +1 -1
  38. package/lib/types/client/effects/remote-drop.d.ts +20 -8
  39. package/lib/types/client/effects/remote-drop.d.ts.map +1 -1
  40. package/lib/types/client/locales.d.ts +10 -1
  41. package/lib/types/client/locales.d.ts.map +1 -1
  42. package/lib/types/client/settings/SettingsSection.d.ts.map +1 -1
  43. package/lib/types/client/styles/content.css.d.ts +1 -1
  44. package/lib/types/client/styles/content.css.d.ts.map +1 -1
  45. package/lib/types/client/styles/layout.css.d.ts +1 -1
  46. package/lib/types/client/styles/layout.css.d.ts.map +1 -1
  47. package/lib/types/client/styles/misc.css.d.ts +1 -1
  48. package/lib/types/client/styles/misc.css.d.ts.map +1 -1
  49. package/lib/types/client-data/remote-drop.d.ts +64 -0
  50. package/lib/types/client-data/remote-drop.d.ts.map +1 -1
  51. package/lib/types/client-data/settings-form.d.ts +3 -0
  52. package/lib/types/client-data/settings-form.d.ts.map +1 -1
  53. package/lib/types/client-pairing.d.ts +8 -0
  54. package/lib/types/client-pairing.d.ts.map +1 -1
  55. package/lib/types/client-routes.d.ts +23 -0
  56. package/lib/types/client-routes.d.ts.map +1 -1
  57. package/lib/types/debug-redact.d.ts +68 -0
  58. package/lib/types/debug-redact.d.ts.map +1 -0
  59. package/lib/types/debug-report.d.ts +58 -0
  60. package/lib/types/debug-report.d.ts.map +1 -0
  61. package/lib/types/fetch-route-intercept.d.ts +26 -0
  62. package/lib/types/fetch-route-intercept.d.ts.map +1 -1
  63. package/lib/types/index.d.ts +7 -1
  64. package/lib/types/index.d.ts.map +1 -1
  65. package/lib/types/intercept.d.ts +6 -0
  66. package/lib/types/intercept.d.ts.map +1 -1
  67. package/lib/types/log-ring.d.ts +57 -0
  68. package/lib/types/log-ring.d.ts.map +1 -0
  69. package/lib/types/plugin-adapter-core.d.ts.map +1 -1
  70. package/lib/types/plugin-route-intercept.d.ts +8 -0
  71. package/lib/types/plugin-route-intercept.d.ts.map +1 -1
  72. package/lib/types/plugin-tunnel.d.ts +37 -2
  73. package/lib/types/plugin-tunnel.d.ts.map +1 -1
  74. package/package.json +3 -2
  75. package/pwa/app.css +13 -2
  76. package/scripts/check-debug-report.mjs +277 -0
  77. package/scripts/check-info-sheet-stacking.mjs +85 -0
  78. package/scripts/check-remote-drop.mjs +130 -0
  79. package/scripts/check-tablet-safe-area.mjs +49 -0
  80. package/src/adapters/dsh-better-sidebar/index.ts +31 -2
  81. package/src/client/MobileSessionInfo.tsx +29 -2
  82. package/src/client/effects/remote-drop.ts +271 -24
  83. package/src/client/locales.ts +24 -4
  84. package/src/client/settings/SettingsSection.tsx +120 -3
  85. package/src/client/styles/content.css.ts +44 -14
  86. package/src/client/styles/layout.css.ts +9 -3
  87. package/src/client/styles/misc.css.ts +38 -0
  88. package/src/client-data/remote-drop.ts +103 -0
  89. package/src/client-data/settings-form.ts +3 -0
  90. package/src/client-pairing.ts +11 -0
  91. package/src/client-routes.ts +196 -90
  92. package/src/debug-redact.ts +290 -0
  93. package/src/debug-report.ts +93 -0
  94. package/src/fetch-route-intercept.ts +29 -0
  95. package/src/index.ts +295 -43
  96. package/src/intercept.ts +46 -0
  97. package/src/log-ring.ts +101 -0
  98. package/src/plugin-adapter-core.ts +29 -2
  99. package/src/plugin-route-intercept.ts +516 -12
  100. package/src/plugin-tunnel.ts +230 -20
package/src/intercept.ts CHANGED
@@ -671,6 +671,12 @@ export interface InterceptHandle {
671
671
  * re-judges NOW — the mergers dedupe no-change calls — instead of
672
672
  * waiting for the next frame or the 1s fallback poll. */
673
673
  currentSessionChanged(): void
674
+ /** PB-26a: trigger a summary-sync check for virtual session ids discovered
675
+ * OUTSIDE workspace/follow (a sidechat childId the plugin bridge just
676
+ * minted in a response rewrite, for instance). Empty and non-virtual
677
+ * entries are dropped; a batch with nothing left is a no-op. Reaches every
678
+ * live merged `$events` stream, and the pending slot when none is open. */
679
+ nudgeSummarySync(ids: readonly string[]): void
674
680
  }
675
681
 
676
682
  function isPlainObject(value: unknown): value is Record<string, unknown> {
@@ -2013,6 +2019,15 @@ async function* mergedEventsStream(deps: MergedEventsStreamDeps): AsyncGenerator
2013
2019
  /** The ids the in-flight round asked (its triggering batches) — marked at
2014
2020
  * completion, joined by nothing (mid-flight batches queue instead). */
2015
2021
  let askingNow: string[] = []
2022
+ /** PB-26a: the newest REAL-TIME running flag this stream delivered per
2023
+ * VIRTUAL session id (the leg's forwarded `api-session/status` frames
2024
+ * only — never the synthesized status-sync frames). Read when a row is
2025
+ * first announced: a status frame that reached the UI BEFORE its row
2026
+ * existed was dropped by the host (handleSessionStatus only updates
2027
+ * sessions already in its summaries), and a flip during the list pull is
2028
+ * newer than the snapshot's `running` — either way the announce must be
2029
+ * followed by the live value. Cleared with a fresh serving period. */
2030
+ const latestLiveStatus = new Map<string, boolean>()
2016
2031
  const scheduleRetry = (): void => {
2017
2032
  if (retryTimer !== undefined || retryAt.size === 0) return
2018
2033
  const earliest = Math.min(...retryAt.values())
@@ -2072,6 +2087,15 @@ async function* mergedEventsStream(deps: MergedEventsStreamDeps): AsyncGenerator
2072
2087
  if (announced.has(rowId)) continue
2073
2088
  announced.add(rowId)
2074
2089
  channel.push(frame)
2090
+ // PB-26a: the row is born in the UI carrying the snapshot's
2091
+ // `running`; when this stream delivered a live status for it the
2092
+ // host could not apply (no row yet) or that postdates the
2093
+ // snapshot, realign right behind the added frame — only when the
2094
+ // two disagree, a matching flag needs no second frame.
2095
+ const liveRunning = latestLiveStatus.get(rowId)
2096
+ if (liveRunning !== undefined && liveRunning !== (isPlainObject(row) && row.running === true)) {
2097
+ channel.push({ type: 'emit', event: 'api-session/status', args: [rowId, liveRunning] })
2098
+ }
2075
2099
  }
2076
2100
  // T65-fix ask bookkeeping: a returned id is announced and done; an
2077
2101
  // id the server did NOT return either exhausts its attempts or
@@ -2206,6 +2230,10 @@ async function* mergedEventsStream(deps: MergedEventsStreamDeps): AsyncGenerator
2206
2230
  askedForGood.clear()
2207
2231
  askAttempts.clear()
2208
2232
  retryAt.clear()
2233
+ // PB-26a: the live flags belong to the serving period that delivered
2234
+ // them — the status re-sync below re-reads every row from a fresh
2235
+ // snapshot.
2236
+ latestLiveStatus.clear()
2209
2237
  if (retryTimer !== undefined) {
2210
2238
  clock.clearTimeout(retryTimer)
2211
2239
  retryTimer = undefined
@@ -2370,6 +2398,20 @@ async function* mergedEventsStream(deps: MergedEventsStreamDeps): AsyncGenerator
2370
2398
  if (isPlainObject(frame) && frame.type === 'emit' && frame.event === 'api-session/status') {
2371
2399
  const rowId = Array.isArray(frame.args) ? frame.args[0] : undefined
2372
2400
  if (typeof rowId === 'string' && rowId !== '') liveStatusSeq.set(rowId, ++statusSeq)
2401
+ // PB-26a: the rewritten frame is the proved shape (`args` =
2402
+ // [virtualId, running]). Remember the live flag under the
2403
+ // VIRTUAL id, and — when this stream has not announced the row
2404
+ // — pull the list now: a sidechat / subagent child rides no
2405
+ // workspace/follow frame, so this status is the client's first
2406
+ // (and, until the next turn, only) notice that the row exists.
2407
+ // The frame itself still crosses at once, below.
2408
+ const args = isPlainObject(rewritten) && Array.isArray(rewritten.args) ? rewritten.args : undefined
2409
+ const virtualId = args?.[0]
2410
+ const running = args?.[1]
2411
+ if (typeof virtualId === 'string' && typeof running === 'boolean') {
2412
+ latestLiveStatus.set(virtualId, running)
2413
+ if (!announced.has(virtualId)) handleForwardedBatch([virtualId])
2414
+ }
2373
2415
  }
2374
2416
  // The orphan-close bookkeeping rides the two id-carrying frame
2375
2417
  // types: waterfalls enter the record (once per leg — a repeat
@@ -3498,6 +3540,10 @@ export function installIntercept(options: InstallInterceptOptions): InterceptHan
3498
3540
  currentSessionChanged() {
3499
3541
  for (const listener of [...currentSessionListeners]) listener()
3500
3542
  },
3543
+ nudgeSummarySync(ids: readonly string[]) {
3544
+ const virtualIds = ids.filter((id) => typeof id === 'string' && id !== '' && isVirtual(id))
3545
+ if (virtualIds.length > 0) summarySync.noteForwardedSessionIds(virtualIds)
3546
+ },
3501
3547
  }
3502
3548
  }
3503
3549
 
@@ -0,0 +1,101 @@
1
+ /**
2
+ * CU-1: the plugin's in-process diagnostic log ring (SPEC §方案 C). A bounded
3
+ * FIFO of the plugin row's own log lines, kept in memory only — the desktop
4
+ * app writes no log file, so this is the one artifact the debug report (CU-2)
5
+ * can read on either role. Deliberately NOT a file, and deliberately only
6
+ * this plugin's lines: DSH's own log stream stays untouched.
7
+ *
8
+ * Pure module: no DSH imports, no I/O, no clocks beyond the timestamp it
9
+ * stamps entries with. The wiring lives in src/index.ts, which funnels its
10
+ * `logWarn` / `logInfo` through {@link createPluginLog} so existing call
11
+ * sites enter the ring unchanged (double write: logger first, ring second),
12
+ * and pushes the handful of lifecycle events (row apply/reload, gateway
13
+ * spawn/exit, relay link flips, fetch-route installs) by hand.
14
+ *
15
+ * One discipline the debug report leans on: ring lines carry event codes,
16
+ * id shapes and counters — never payloads. The redactor (debug-redact.ts)
17
+ * is the second layer, not the license to log secrets here.
18
+ */
19
+
20
+ import { format } from 'node:util'
21
+
22
+ /** Severity of one ring entry. The ring only distinguishes what the report
23
+ * renders: info lines and warnings. */
24
+ export type LogLevel = 'info' | 'warn'
25
+
26
+ /** One ring entry: ISO timestamp, level, fully interpolated message. */
27
+ export interface LogRingEntry {
28
+ t: string
29
+ level: LogLevel
30
+ msg: string
31
+ }
32
+
33
+ /** Longest message kept, in characters (UTF-16 code units). Debug lines are
34
+ * codes and shapes, not dumps — anything longer was the wrong thing to log. */
35
+ const MAX_MSG_CHARS = 512
36
+
37
+ /** The ring's face. `push` never throws and never returns anything; callers
38
+ * log through it exactly like they would through the context logger. */
39
+ export interface LogRing {
40
+ push(level: LogLevel, formatText: string, ...args: unknown[]): void
41
+ /** Oldest-last copy of the ring. Mutating the returned array or its
42
+ * entries never reaches the ring's own state. */
43
+ snapshot(): LogRingEntry[]
44
+ }
45
+
46
+ /**
47
+ * Create one bounded log ring. `cap` defaults to 300 entries — a full debug
48
+ * session of lifecycle lines plus a healthy share of warnings, while keeping
49
+ * the report a fixed small size. A cap below 1 is clamped to 1.
50
+ */
51
+ export function createLogRing(cap = 300): LogRing {
52
+ const capacity = Number.isFinite(cap) && cap >= 1 ? Math.floor(cap) : 1
53
+ const entries: LogRingEntry[] = []
54
+ return {
55
+ push(level, formatText, ...args) {
56
+ let msg: string
57
+ try {
58
+ msg = args.length === 0 ? String(formatText) : format(formatText, ...args)
59
+ } catch {
60
+ // A malformed format string must never break the caller's flow —
61
+ // keep the raw template as the message.
62
+ msg = String(formatText)
63
+ }
64
+ if (msg.length > MAX_MSG_CHARS) msg = msg.slice(0, MAX_MSG_CHARS)
65
+ entries.push({ t: new Date().toISOString(), level, msg })
66
+ if (entries.length > capacity) entries.splice(0, entries.length - capacity)
67
+ },
68
+ snapshot() {
69
+ return entries.map((entry) => ({ ...entry }))
70
+ },
71
+ }
72
+ }
73
+
74
+ /** The logger face this module needs: the cordis context logger's two
75
+ * methods, each optional (test fakes carry none). */
76
+ export interface PluginLogLike {
77
+ info?(format: string, ...args: unknown[]): void
78
+ warn?(format: string, ...args: unknown[]): void
79
+ }
80
+
81
+ /** The double-writer the apply() wiring funnels its logging through: every
82
+ * line reaches the context logger (unchanged behavior, unchanged call
83
+ * sites) AND the ring (the debug report's source). A ring failure is
84
+ * swallowed; a logger failure propagates exactly as it did before this
85
+ * module existed. */
86
+ export function createPluginLog(ring: LogRing, logger: PluginLogLike | undefined): { info(format: string, ...args: unknown[]): void, warn(format: string, ...args: unknown[]): void } {
87
+ return {
88
+ info(formatText, ...args) {
89
+ logger?.info?.(formatText, ...args)
90
+ try {
91
+ ring.push('info', formatText, ...args)
92
+ } catch { /* the ring must never break logging */ }
93
+ },
94
+ warn(formatText, ...args) {
95
+ logger?.warn?.(formatText, ...args)
96
+ try {
97
+ ring.push('warn', formatText, ...args)
98
+ } catch { /* the ring must never break logging */ }
99
+ },
100
+ }
101
+ }
@@ -25,7 +25,7 @@
25
25
  import { createHash } from 'node:crypto'
26
26
  import { isVirtual, fromVirtual } from './virtual-id.js'
27
27
  import { MAX_UPLOAD_BYTES } from './upload-limits.js'
28
- import type { NormalizedBridgeRoute, NormalizedDeclaration } from './plugin-bridge.js'
28
+ import type { BridgeLocator, NormalizedBridgeRoute, NormalizedDeclaration } from './plugin-bridge.js'
29
29
  import { BRIDGE_METHODS, DEFAULT_ROUTE_BODY_BYTES } from './plugin-bridge.js'
30
30
 
31
31
  export { fromVirtual }
@@ -327,6 +327,15 @@ export function normalizeAdapter(input: unknown, installedVersion: string, optio
327
327
  methods: route.methods.length === 1 && route.methods[0] === 'GET' ? undefined : [...route.methods],
328
328
  maxBody: route.maxBody === DEFAULT_ROUTE_BODY_BYTES ? undefined : route.maxBody,
329
329
  upgrade: route.upgrade ? true : undefined,
330
+ // PB-26b: an upgrade route whose ids do NOT ride the query keeps its
331
+ // locator on the registry entry — the tunnel's validation and the
332
+ // client's upgrade surrogate read where the ids actually arrive (the
333
+ // sidechat ws watch carries them in the post-handshake JSON frames).
334
+ // A query route's declaration stays EXACTLY the 2.1 shape — no
335
+ // locator key — so every older consumer and digest is untouched
336
+ // (the adapter digest always covered route.locator and never
337
+ // covered the declaration routes' shape).
338
+ ...(route.upgrade && route.locator.in !== 'query' ? { locator: route.locator as BridgeLocator } : {}),
330
339
  })),
331
340
  digest,
332
341
  }
@@ -483,6 +492,16 @@ export function locateAdapterIds(route: NormalizedAdapterRoute, pathname: string
483
492
  for (const field of locator.fields) {
484
493
  const value = body[field]
485
494
  if (typeof value === 'string' && value !== '') hits.push({ kind: 'json-body', key: field, value })
495
+ // PB-26b: a field may carry a LIST of ids — better-sidebar's sidechat
496
+ // ws watch frame sends `{"op":"watch","threadIds":[...]}`. Every
497
+ // non-empty string element is one hit under the same field key, so
498
+ // classify/rewrite treat the batch exactly like repeated query
499
+ // values: every id virtual → remote, one mixed in → refuse.
500
+ else if (Array.isArray(value)) {
501
+ for (const item of value) {
502
+ if (typeof item === 'string' && item !== '') hits.push({ kind: 'json-body', key: field, value: item })
503
+ }
504
+ }
486
505
  }
487
506
  }
488
507
  }
@@ -549,7 +568,15 @@ export function rewriteAdapterRequest(
549
568
  } else {
550
569
  if (bodyCopy === undefined && isRecord(body)) bodyCopy = { ...body }
551
570
  if (bodyCopy !== undefined) {
552
- bodyCopy[hit.key!] = real
571
+ const current = bodyCopy[hit.key!]
572
+ // PB-26b: an array field rewrites IN PLACE, element for element —
573
+ // only the entries equal to this hit's located value swap for their
574
+ // real halves, so a mixed batch of ids keeps its shape and order.
575
+ if (Array.isArray(current)) {
576
+ bodyCopy[hit.key!] = current.map((item) => (item === hit.value ? real : item))
577
+ } else {
578
+ bodyCopy[hit.key!] = real
579
+ }
553
580
  bodyText = JSON.stringify(bodyCopy)
554
581
  }
555
582
  }