dsh-zen-remote 2.3.4 → 2.3.6

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/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/docs/interface.md +2 -2
  4. package/docs/plugin-bridge.md +2 -2
  5. package/docs/remote-access.en.md +3 -3
  6. package/docs/remote-access.md +3 -3
  7. package/lib/adapters/dsh-better-sidebar/index.js +31 -2
  8. package/lib/adapters/dsh-better-sidebar/index.js.map +1 -1
  9. package/lib/client.js +185 -48
  10. package/lib/index.js +13 -0
  11. package/lib/index.js.map +1 -1
  12. package/lib/intercept.js +43 -1
  13. package/lib/intercept.js.map +1 -1
  14. package/lib/plugin-adapter-core.js +30 -1
  15. package/lib/plugin-adapter-core.js.map +1 -1
  16. package/lib/plugin-route-intercept.js +506 -11
  17. package/lib/plugin-route-intercept.js.map +1 -1
  18. package/lib/plugin-tunnel.js +210 -18
  19. package/lib/plugin-tunnel.js.map +1 -1
  20. package/lib/types/adapters/dsh-better-sidebar/index.d.ts +5 -2
  21. package/lib/types/adapters/dsh-better-sidebar/index.d.ts.map +1 -1
  22. package/lib/types/client/MobileSessionInfo.d.ts.map +1 -1
  23. package/lib/types/client/effects/turn-fold.d.ts +42 -0
  24. package/lib/types/client/effects/turn-fold.d.ts.map +1 -1
  25. package/lib/types/client/locales.d.ts +2 -0
  26. package/lib/types/client/locales.d.ts.map +1 -1
  27. package/lib/types/client/styles/content.css.d.ts +1 -1
  28. package/lib/types/client/styles/content.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/styles/turn-fold.css.d.ts.map +1 -1
  32. package/lib/types/index.d.ts.map +1 -1
  33. package/lib/types/intercept.d.ts +6 -0
  34. package/lib/types/intercept.d.ts.map +1 -1
  35. package/lib/types/plugin-adapter-core.d.ts.map +1 -1
  36. package/lib/types/plugin-route-intercept.d.ts +8 -0
  37. package/lib/types/plugin-route-intercept.d.ts.map +1 -1
  38. package/lib/types/plugin-tunnel.d.ts +37 -2
  39. package/lib/types/plugin-tunnel.d.ts.map +1 -1
  40. package/package.json +37 -37
  41. package/scripts/check-info-sheet-stacking.mjs +85 -0
  42. package/src/adapters/dsh-better-sidebar/index.ts +31 -2
  43. package/src/client/MobileSessionInfo.tsx +29 -2
  44. package/src/client/effects/turn-fold.ts +97 -26
  45. package/src/client/locales.ts +4 -0
  46. package/src/client/styles/content.css.ts +44 -14
  47. package/src/client/styles/layout.css.ts +9 -3
  48. package/src/client/styles/turn-fold.css.ts +9 -0
  49. package/src/index.ts +13 -0
  50. package/src/intercept.ts +46 -0
  51. package/src/plugin-adapter-core.ts +29 -2
  52. package/src/plugin-route-intercept.ts +516 -12
  53. 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
 
@@ -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
  }