@linxin666/dsh-remote-web-ui 0.1.3 → 0.1.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 (52) hide show
  1. package/README.md +14 -5
  2. package/lib/client.js +70 -70
  3. package/lib/mobile.js +160 -32
  4. package/lib/mobile.js.map +1 -1
  5. package/lib/types/mobile/mux.d.ts +66 -2
  6. package/lib/types/mobile/mux.d.ts.map +1 -1
  7. package/lib/types/mobile/mux.js +155 -8
  8. package/lib/types/mobile/mux.test.d.ts +2 -0
  9. package/lib/types/mobile/mux.test.d.ts.map +1 -0
  10. package/lib/types/mobile/mux.test.js +141 -0
  11. package/lib/types/mobile/views/App.d.ts.map +1 -1
  12. package/lib/types/mobile/views/App.js +6 -0
  13. package/lib/types/mobile/views/ChatView.d.ts.map +1 -1
  14. package/lib/types/mobile/views/ChatView.js +20 -6
  15. package/lib/types/mobile/views/ChatView.test.js +92 -2
  16. package/package.json +1 -1
  17. package/src/mobile/mux.test.ts +172 -0
  18. package/src/mobile/mux.ts +170 -8
  19. package/src/mobile/views/App.tsx +7 -0
  20. package/src/mobile/views/ChatView.test.tsx +98 -3
  21. package/src/mobile/views/ChatView.tsx +20 -6
  22. package/lib/types/client/PairFailedNotice.js.map +0 -1
  23. package/lib/types/client/PhoneIcon.js.map +0 -1
  24. package/lib/types/client/PluginSettingsCard.js.map +0 -1
  25. package/lib/types/client/RemoteEntry.js.map +0 -1
  26. package/lib/types/client/RemotePanel.js.map +0 -1
  27. package/lib/types/client/RemoteSettingsCard.js.map +0 -1
  28. package/lib/types/client/deep-link.js.map +0 -1
  29. package/lib/types/client/index.js.map +0 -1
  30. package/lib/types/client/locales.js.map +0 -1
  31. package/lib/types/client/pair-api.js.map +0 -1
  32. package/lib/types/client/settings-form.js.map +0 -1
  33. package/lib/types/gate.js.map +0 -1
  34. package/lib/types/index.js.map +0 -1
  35. package/lib/types/invariant.js.map +0 -1
  36. package/lib/types/lan.js.map +0 -1
  37. package/lib/types/mobile/api.js.map +0 -1
  38. package/lib/types/mobile/index.js.map +0 -1
  39. package/lib/types/mobile/messages.js.map +0 -1
  40. package/lib/types/mobile/messages.test.js.map +0 -1
  41. package/lib/types/mobile/mobile-styles.js.map +0 -1
  42. package/lib/types/mobile/mux.js.map +0 -1
  43. package/lib/types/mobile/rpc.js.map +0 -1
  44. package/lib/types/mobile/views/App.js.map +0 -1
  45. package/lib/types/mobile/views/ChatView.js.map +0 -1
  46. package/lib/types/mobile/views/SessionListView.js.map +0 -1
  47. package/lib/types/mobile/views/WorkspaceView.js.map +0 -1
  48. package/lib/types/mobile-api.js.map +0 -1
  49. package/lib/types/mobile-routes.js.map +0 -1
  50. package/lib/types/pairing.js.map +0 -1
  51. package/lib/types/routes.js.map +0 -1
  52. package/lib/types/tunnel.js.map +0 -1
package/lib/mobile.js CHANGED
@@ -7625,7 +7625,7 @@ async function selectModel(sessionId, selection) {
7625
7625
  });
7626
7626
  }
7627
7627
  //#endregion
7628
- //#region ../../../../.dsh/source/staging-20260812T235349Z/node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/core.js
7628
+ //#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/core.js
7629
7629
  var _a$1;
7630
7630
  function $constructor(name, initializer, params) {
7631
7631
  function init(inst, def) {
@@ -7684,7 +7684,7 @@ function config(newConfig) {
7684
7684
  return globalConfig;
7685
7685
  }
7686
7686
  //#endregion
7687
- //#region ../../../../.dsh/source/staging-20260812T235349Z/node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/util.js
7687
+ //#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/util.js
7688
7688
  function getEnumValues(entries) {
7689
7689
  const numericValues = Object.values(entries).filter((v) => typeof v === "number");
7690
7690
  return Object.entries(entries).filter(([k, _]) => numericValues.indexOf(+k) === -1).map(([_, v]) => v);
@@ -7999,7 +7999,7 @@ function issue(...args) {
7999
7999
  return { ...iss };
8000
8000
  }
8001
8001
  //#endregion
8002
- //#region ../../../../.dsh/source/staging-20260812T235349Z/node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/errors.js
8002
+ //#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/errors.js
8003
8003
  const initializer$1 = (inst, def) => {
8004
8004
  inst.name = "$ZodError";
8005
8005
  Object.defineProperty(inst, "_zod", {
@@ -8059,7 +8059,7 @@ function formatError(error, mapper = (issue) => issue.message) {
8059
8059
  return fieldErrors;
8060
8060
  }
8061
8061
  //#endregion
8062
- //#region ../../../../.dsh/source/staging-20260812T235349Z/node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/parse.js
8062
+ //#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/parse.js
8063
8063
  const _parse = (_Err) => (schema, value, _ctx, _params) => {
8064
8064
  const ctx = _ctx ? {
8065
8065
  ..._ctx,
@@ -8173,7 +8173,7 @@ const _safeDecodeAsync = (_Err) => async (schema, value, _ctx) => {
8173
8173
  return _safeParseAsync(_Err)(schema, value, _ctx);
8174
8174
  };
8175
8175
  //#endregion
8176
- //#region ../../../../.dsh/source/staging-20260812T235349Z/node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/regexes.js
8176
+ //#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/regexes.js
8177
8177
  /**
8178
8178
  * @deprecated CUID v1 is deprecated by its authors due to information leakage
8179
8179
  * (timestamps embedded in the id). Use {@link cuid2} instead.
@@ -8237,7 +8237,7 @@ const boolean$1 = /^(?:true|false)$/i;
8237
8237
  const lowercase = /^[^A-Z]*$/;
8238
8238
  const uppercase = /^[^a-z]*$/;
8239
8239
  //#endregion
8240
- //#region ../../../../.dsh/source/staging-20260812T235349Z/node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/checks.js
8240
+ //#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/checks.js
8241
8241
  const $ZodCheck = /*@__PURE__*/ $constructor("$ZodCheck", (inst, def) => {
8242
8242
  var _a;
8243
8243
  inst._zod ?? (inst._zod = {});
@@ -8595,7 +8595,7 @@ const $ZodCheckOverwrite = /*@__PURE__*/ $constructor("$ZodCheckOverwrite", (ins
8595
8595
  };
8596
8596
  });
8597
8597
  //#endregion
8598
- //#region ../../../../.dsh/source/staging-20260812T235349Z/node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/doc.js
8598
+ //#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/doc.js
8599
8599
  var Doc = class {
8600
8600
  constructor(args = []) {
8601
8601
  this.content = [];
@@ -8626,14 +8626,14 @@ var Doc = class {
8626
8626
  }
8627
8627
  };
8628
8628
  //#endregion
8629
- //#region ../../../../.dsh/source/staging-20260812T235349Z/node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/versions.js
8629
+ //#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/versions.js
8630
8630
  const version = {
8631
8631
  major: 4,
8632
8632
  minor: 4,
8633
8633
  patch: 3
8634
8634
  };
8635
8635
  //#endregion
8636
- //#region ../../../../.dsh/source/staging-20260812T235349Z/node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/schemas.js
8636
+ //#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/schemas.js
8637
8637
  const $ZodType = /*@__PURE__*/ $constructor("$ZodType", (inst, def) => {
8638
8638
  var _a;
8639
8639
  inst ?? (inst = {});
@@ -9934,7 +9934,7 @@ function handleRefineResult(result, payload, input, inst) {
9934
9934
  }
9935
9935
  }
9936
9936
  //#endregion
9937
- //#region ../../../../.dsh/source/staging-20260812T235349Z/node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/registries.js
9937
+ //#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/registries.js
9938
9938
  var _a;
9939
9939
  var $ZodRegistry = class {
9940
9940
  constructor() {
@@ -9981,7 +9981,7 @@ function registry() {
9981
9981
  (_a = globalThis).__zod_globalRegistry ?? (_a.__zod_globalRegistry = registry());
9982
9982
  const globalRegistry = globalThis.__zod_globalRegistry;
9983
9983
  //#endregion
9984
- //#region ../../../../.dsh/source/staging-20260812T235349Z/node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/api.js
9984
+ //#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/api.js
9985
9985
  // @__NO_SIDE_EFFECTS__
9986
9986
  function _string(Class, params) {
9987
9987
  return new Class({
@@ -10497,7 +10497,7 @@ function _check(fn, params) {
10497
10497
  return ch;
10498
10498
  }
10499
10499
  //#endregion
10500
- //#region ../../../../.dsh/source/staging-20260812T235349Z/node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/to-json-schema.js
10500
+ //#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/to-json-schema.js
10501
10501
  function initializeContext(params) {
10502
10502
  let target = params?.target ?? "draft-2020-12";
10503
10503
  if (target === "draft-4") target = "draft-04";
@@ -10790,7 +10790,7 @@ const createStandardJSONSchemaMethod = (schema, io, processors = {}) => (params)
10790
10790
  return finalize(ctx, schema);
10791
10791
  };
10792
10792
  //#endregion
10793
- //#region ../../../../.dsh/source/staging-20260812T235349Z/node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/json-schema-processors.js
10793
+ //#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/json-schema-processors.js
10794
10794
  const formatMap = {
10795
10795
  guid: "uuid",
10796
10796
  url: "uri",
@@ -11058,7 +11058,7 @@ const lazyProcessor = (schema, ctx, _json, params) => {
11058
11058
  seen.ref = innerType;
11059
11059
  };
11060
11060
  //#endregion
11061
- //#region ../../../../.dsh/source/staging-20260812T235349Z/node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/iso.js
11061
+ //#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/iso.js
11062
11062
  const ZodISODateTime = /*@__PURE__*/ $constructor("ZodISODateTime", (inst, def) => {
11063
11063
  $ZodISODateTime.init(inst, def);
11064
11064
  ZodStringFormat.init(inst, def);
@@ -11088,7 +11088,7 @@ function duration(params) {
11088
11088
  return /* @__PURE__ */ _isoDuration(ZodISODuration, params);
11089
11089
  }
11090
11090
  //#endregion
11091
- //#region ../../../../.dsh/source/staging-20260812T235349Z/node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/errors.js
11091
+ //#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/errors.js
11092
11092
  const initializer = (inst, issues) => {
11093
11093
  $ZodError.init(inst, issues);
11094
11094
  inst.name = "ZodError";
@@ -11110,7 +11110,7 @@ const initializer = (inst, issues) => {
11110
11110
  };
11111
11111
  const ZodRealError = /*@__PURE__*/ $constructor("ZodError", initializer, { Parent: Error });
11112
11112
  //#endregion
11113
- //#region ../../../../.dsh/source/staging-20260812T235349Z/node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/parse.js
11113
+ //#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/parse.js
11114
11114
  const parse = /* @__PURE__ */ _parse(ZodRealError);
11115
11115
  const parseAsync = /* @__PURE__ */ _parseAsync(ZodRealError);
11116
11116
  const safeParse = /* @__PURE__ */ _safeParse(ZodRealError);
@@ -11124,7 +11124,7 @@ const safeDecode = /* @__PURE__ */ _safeDecode(ZodRealError);
11124
11124
  const safeEncodeAsync = /* @__PURE__ */ _safeEncodeAsync(ZodRealError);
11125
11125
  const safeDecodeAsync = /* @__PURE__ */ _safeDecodeAsync(ZodRealError);
11126
11126
  //#endregion
11127
- //#region ../../../../.dsh/source/staging-20260812T235349Z/node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/schemas.js
11127
+ //#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/schemas.js
11128
11128
  const _installedGroups = /* @__PURE__ */ new WeakMap();
11129
11129
  function _installLazyMethods(inst, group, methods) {
11130
11130
  const proto = Object.getPrototypeOf(inst);
@@ -11954,7 +11954,7 @@ function superRefine(fn, params) {
11954
11954
  return /* @__PURE__ */ _superRefine(fn, params);
11955
11955
  }
11956
11956
  //#endregion
11957
- //#region ../../../../.dsh/source/staging-20260812T235349Z/packages/host/apiproxy/lib/types/api/rpc.schema.js
11957
+ //#region ../../node_modules/.pnpm/@deepseek-ai+dsh-host-apiproxy@0.1.0-rc.6_8335eca05d5647f26dcc0c8fd5a39ec0/node_modules/@deepseek-ai/dsh-host-apiproxy/lib/types/api/rpc.schema.js
11958
11958
  /**
11959
11959
  * Message-layer zod schemas: the four wire full forms + error body +
11960
11960
  * carrier receipt. The payload slot is unknown in the full-form schemas — business payloads
@@ -12268,7 +12268,7 @@ union([object({ accepted: literal(true) }), object({
12268
12268
  reason: union([literal("not-pending"), literal("bad-response")])
12269
12269
  })]);
12270
12270
  //#endregion
12271
- //#region ../../../../.dsh/source/staging-20260812T235349Z/packages/host/apiproxy/lib/types/api/session-search.js
12271
+ //#region ../../node_modules/.pnpm/@deepseek-ai+dsh-host-apiproxy@0.1.0-rc.6_8335eca05d5647f26dcc0c8fd5a39ec0/node_modules/@deepseek-ai/dsh-host-apiproxy/lib/types/api/session-search.js
12272
12272
  /**
12273
12273
  * Return the longest prefix containing at most `maximum` Unicode code points.
12274
12274
  * @param value - text to bound.
@@ -12286,7 +12286,7 @@ function truncateUnicodeCodePoints(value, maximum) {
12286
12286
  return value;
12287
12287
  }
12288
12288
  //#endregion
12289
- //#region ../../../../.dsh/source/staging-20260812T235349Z/packages/host/apiproxy/lib/types/api/sessions.schema.js
12289
+ //#region ../../node_modules/.pnpm/@deepseek-ai+dsh-host-apiproxy@0.1.0-rc.6_8335eca05d5647f26dcc0c8fd5a39ec0/node_modules/@deepseek-ai/dsh-host-apiproxy/lib/types/api/sessions.schema.js
12290
12290
  /**
12291
12291
  * sessions domain zod schemas (names derived from map keys: sessionListRequestSchema /
12292
12292
  * sessionListValueSchema). SessionEvent passthrough = strict envelope (type/seq/time) + wide
@@ -12425,6 +12425,10 @@ const sessionProjectionsBlockSchema = object({
12425
12425
  asOfSeq: number().int().min(-1),
12426
12426
  values: record(string(), unknown())
12427
12427
  });
12428
+ object({
12429
+ blank: boolean(),
12430
+ lastPromptAt: number().nullable()
12431
+ });
12428
12432
  object({
12429
12433
  maxImageBytes: number().int().positive(),
12430
12434
  maxImagesPerMessage: number().int().positive(),
@@ -12518,7 +12522,7 @@ object({ accepted: literal(true) });
12518
12522
  object({ sessionId: sessionIdSchema });
12519
12523
  object({ accepted: literal(true) });
12520
12524
  //#endregion
12521
- //#region ../../../../.dsh/source/staging-20260812T235349Z/packages/host/apiproxy/lib/types/api/approvals.schema.js
12525
+ //#region ../../node_modules/.pnpm/@deepseek-ai+dsh-host-apiproxy@0.1.0-rc.6_8335eca05d5647f26dcc0c8fd5a39ec0/node_modules/@deepseek-ai/dsh-host-apiproxy/lib/types/api/approvals.schema.js
12522
12526
  /**
12523
12527
  * approvals domain zod schemas (respond is a client-response; the payload schema serves
12524
12528
  * the /api/respond endpoint's second parse after routing via the pending table).
@@ -12552,7 +12556,7 @@ const taskViewSchema = object({
12552
12556
  finishedAt: number().int().nonnegative().optional()
12553
12557
  });
12554
12558
  //#endregion
12555
- //#region ../../../../.dsh/source/staging-20260812T235349Z/packages/host/apiproxy/lib/types/api/workspace.schema.js
12559
+ //#region ../../node_modules/.pnpm/@deepseek-ai+dsh-host-apiproxy@0.1.0-rc.6_8335eca05d5647f26dcc0c8fd5a39ec0/node_modules/@deepseek-ai/dsh-host-apiproxy/lib/types/api/workspace.schema.js
12556
12560
  /**
12557
12561
  * workspace domain zod schemas (names derived from map keys). The
12558
12562
  * WorkspaceId brand cast lives in sessions.schema (see the note there) and
@@ -12598,7 +12602,7 @@ object({ workspace: workspaceViewSchema });
12598
12602
  object({ sessionId: sessionIdSchema });
12599
12603
  object({ archivedSessionIds: array(sessionIdSchema) });
12600
12604
  //#endregion
12601
- //#region ../../../../.dsh/source/staging-20260812T235349Z/packages/host/apiproxy/lib/types/api/events.schema.js
12605
+ //#region ../../node_modules/.pnpm/@deepseek-ai+dsh-host-apiproxy@0.1.0-rc.6_8335eca05d5647f26dcc0c8fd5a39ec0/node_modules/@deepseek-ai/dsh-host-apiproxy/lib/types/api/events.schema.js
12602
12606
  /**
12603
12607
  * events domain zod schemas: MuxFrame / HostFrame unions (discriminatedUnion('type')).
12604
12608
  * A frame is the payload slot of the ServerRequest full form; the SessionEvent inside
@@ -12760,17 +12764,41 @@ discriminatedUnion("type", [
12760
12764
  function browserSource(url) {
12761
12765
  return new EventSource(url);
12762
12766
  }
12767
+ const DEFAULT_POLL_INTERVAL_MS = 3e3;
12768
+ const DEFAULT_STALL_THRESHOLD_MS = 12e3;
12769
+ /** Poll window: enough recent events to cover a few seconds of agent output. */
12770
+ const DEFAULT_POLL_PAGE_SIZE = 50;
12763
12771
  /**
12764
12772
  * Keep one SSE subscription open, fanning validated frames out to
12765
12773
  * subscribers. EventSource owns reconnection (with its own backoff); this
12766
- * class only manages the subscription lifecycle.
12774
+ * class only manages the subscription lifecycle, plus a polling fallback
12775
+ * that keeps the open session live when the SSE channel cannot deliver.
12767
12776
  */
12768
12777
  var MuxClient = class {
12769
12778
  sourceFactory;
12779
+ pollLatest;
12780
+ pollIntervalMs;
12781
+ stallThresholdMs;
12782
+ now;
12770
12783
  listeners = /* @__PURE__ */ new Set();
12771
12784
  source;
12772
12785
  stopped = false;
12773
12786
  url;
12787
+ /** The session to keep live via fallback polling (undefined = none). */
12788
+ observeSessionId;
12789
+ /** Last epoch ms the SSE channel produced a frame (or the stream opened). */
12790
+ lastDataAt = 0;
12791
+ /**
12792
+ * Whether the SSE channel has ever delivered a frame in this stream (a
12793
+ * delivered frame proves the tunnel forwards SSE; silence alone then means
12794
+ * the agent idle, not a dead channel — only an onerror re-arms fallback).
12795
+ */
12796
+ sseAlive = false;
12797
+ /** Per-session highest event seq already emitted, for poll dedup. */
12798
+ pollWatermark = /* @__PURE__ */ new Map();
12799
+ stallTimer;
12800
+ pollTimer;
12801
+ polling = false;
12774
12802
  /**
12775
12803
  * @param url - the mobile events endpoint (browser-relative).
12776
12804
  * @param options - seams.
@@ -12778,17 +12806,25 @@ var MuxClient = class {
12778
12806
  constructor(url = "/m/api/events.mux", options = {}) {
12779
12807
  this.url = url;
12780
12808
  this.sourceFactory = options.sourceFactory ?? browserSource;
12809
+ this.pollLatest = options.pollLatest ?? ((sessionId) => history(sessionId, void 0, DEFAULT_POLL_PAGE_SIZE));
12810
+ this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
12811
+ this.stallThresholdMs = options.stallThresholdMs ?? DEFAULT_STALL_THRESHOLD_MS;
12812
+ this.now = options.now ?? (() => Date.now());
12781
12813
  }
12782
12814
  /** Open the stream (idempotent; EventSource reconnects until {@link stop}). */
12783
12815
  start() {
12784
12816
  this.stopped = false;
12785
- if (this.source !== void 0) return;
12786
- this.connect();
12817
+ this.lastDataAt = this.now();
12818
+ if (this.source === void 0) this.connect();
12819
+ this.startStallChecker();
12787
12820
  }
12788
12821
  /** Close for good. */
12789
12822
  stop() {
12790
12823
  this.stopped = true;
12824
+ this.stopStallChecker();
12825
+ this.stopPolling();
12791
12826
  this.closeSource();
12827
+ this.observeSessionId = void 0;
12792
12828
  }
12793
12829
  /** Subscribe to validated frames; returns an unsubscribe function. */
12794
12830
  onFrame(listener) {
@@ -12797,16 +12833,94 @@ var MuxClient = class {
12797
12833
  this.listeners.delete(listener);
12798
12834
  };
12799
12835
  }
12836
+ /**
12837
+ * Point the fallback at one open session (or `undefined` to stop it).
12838
+ * While the SSE channel is stalled this client polls that session's
12839
+ * history and re-emits new events as `session/event` frames.
12840
+ */
12841
+ observe(sessionId) {
12842
+ this.observeSessionId = sessionId;
12843
+ if (sessionId === void 0) {
12844
+ this.stopPolling();
12845
+ return;
12846
+ }
12847
+ if (!this.polling && !this.stopped && !this.sseAlive && this.now() - this.lastDataAt > this.stallThresholdMs) this.startPolling();
12848
+ }
12800
12849
  connect() {
12850
+ this.sseAlive = false;
12801
12851
  const source = this.sourceFactory(this.url);
12802
12852
  this.source = source;
12803
12853
  source.onmessage = (event) => {
12804
12854
  this.handleMessage(event.data);
12805
12855
  };
12806
12856
  source.onerror = () => {
12807
- if (this.stopped && this.source === source) this.closeSource();
12857
+ if (this.stopped && this.source === source) {
12858
+ this.closeSource();
12859
+ return;
12860
+ }
12861
+ this.sseAlive = false;
12862
+ if (this.observeSessionId !== void 0) this.startPolling();
12808
12863
  };
12809
12864
  }
12865
+ startStallChecker() {
12866
+ this.stopStallChecker();
12867
+ this.stallTimer = setInterval(() => {
12868
+ if (this.stopped) return;
12869
+ if (this.observeSessionId === void 0) return;
12870
+ if (this.polling) return;
12871
+ if (this.sseAlive) return;
12872
+ if (this.now() - this.lastDataAt > this.stallThresholdMs) this.startPolling();
12873
+ }, 1e3);
12874
+ }
12875
+ stopStallChecker() {
12876
+ if (this.stallTimer !== void 0) {
12877
+ clearInterval(this.stallTimer);
12878
+ this.stallTimer = void 0;
12879
+ }
12880
+ }
12881
+ startPolling() {
12882
+ if (this.polling || this.stopped) return;
12883
+ this.polling = true;
12884
+ this.pollTick();
12885
+ this.pollTimer = setInterval(() => {
12886
+ this.pollTick();
12887
+ }, this.pollIntervalMs);
12888
+ }
12889
+ stopPolling() {
12890
+ this.polling = false;
12891
+ if (this.pollTimer !== void 0) {
12892
+ clearInterval(this.pollTimer);
12893
+ this.pollTimer = void 0;
12894
+ }
12895
+ }
12896
+ /**
12897
+ * Fetch the latest history page for the observed session and re-emit any
12898
+ * event above the per-session watermark as a `session/event` frame.
12899
+ * Idempotent by seq: listeners (and the fold) never see a duplicate.
12900
+ */
12901
+ async pollTick() {
12902
+ const sessionId = this.observeSessionId;
12903
+ if (sessionId === void 0) {
12904
+ this.stopPolling();
12905
+ return;
12906
+ }
12907
+ try {
12908
+ const page = await this.pollLatest(sessionId);
12909
+ let maxSeq = this.pollWatermark.get(sessionId) ?? -1;
12910
+ for (const entry of page.events) {
12911
+ const event = entry.event;
12912
+ const seq = typeof event?.seq === "number" ? event.seq : -1;
12913
+ if (seq <= maxSeq) continue;
12914
+ maxSeq = seq;
12915
+ this.emit({
12916
+ type: "session/event",
12917
+ sessionId,
12918
+ event
12919
+ });
12920
+ }
12921
+ this.pollWatermark.set(sessionId, maxSeq);
12922
+ } catch {}
12923
+ }
12810
12924
  handleMessage(data) {
12811
12925
  if (typeof data !== "string" || data === "") return;
12812
12926
  let parsed;
@@ -12819,8 +12933,14 @@ var MuxClient = class {
12819
12933
  if (!envelope.success) return;
12820
12934
  const frame = muxFrameSchema.safeParse(envelope.data.payload);
12821
12935
  if (!frame.success) return;
12936
+ this.sseAlive = true;
12937
+ this.lastDataAt = this.now();
12938
+ if (this.polling) this.stopPolling();
12939
+ this.emit(frame.data);
12940
+ }
12941
+ emit(frame) {
12822
12942
  for (const listener of this.listeners) try {
12823
- listener(frame.data);
12943
+ listener(frame);
12824
12944
  } catch {}
12825
12945
  }
12826
12946
  closeSource() {
@@ -13495,7 +13615,6 @@ function ChatView({ session, mux, onBack }) {
13495
13615
  setLoading(false);
13496
13616
  const projections = page.projections?.values;
13497
13617
  setPermissions(parsePermissionSelect(projections?.["permissions"]));
13498
- scrollToBottom();
13499
13618
  }, (reason) => {
13500
13619
  if (cancelled) return;
13501
13620
  setError(errorText(reason));
@@ -13519,14 +13638,20 @@ function ChatView({ session, mux, onBack }) {
13519
13638
  if (frame.type === "session/projection" && frame.sessionId === session.sessionId && frame.key === "permissions") setPermissions(parsePermissionSelect(frame.value));
13520
13639
  });
13521
13640
  }, [mux, session.sessionId]);
13522
- (0, import_react.useEffect)(() => {
13523
- if (messages.some((message) => message.pending === true)) scrollToBottom();
13524
- }, [messages]);
13525
13641
  const scrollToBottom = (0, import_react.useCallback)(() => {
13526
13642
  const el = scrollRef.current;
13527
13643
  if (el === void 0) return;
13528
13644
  el.scrollTop = el.scrollHeight;
13529
13645
  }, []);
13646
+ const lastMessageKeyRef = (0, import_react.useRef)(void 0);
13647
+ (0, import_react.useEffect)(() => {
13648
+ const last = messages[messages.length - 1];
13649
+ if (last === void 0) return;
13650
+ const key = last.seq + ":" + (last.pending === true ? "p" : "f");
13651
+ if (key === lastMessageKeyRef.current) return;
13652
+ lastMessageKeyRef.current = key;
13653
+ scrollToBottom();
13654
+ }, [messages, scrollToBottom]);
13530
13655
  /** Load one older page and prepend it. The fold is directional (incremental
13531
13656
  * tails only), so the older page folds standalone and concatenates ahead —
13532
13657
  * host page boundaries never cut a message, so the seam is exact. */
@@ -14531,6 +14656,9 @@ function App() {
14531
14656
  mux.stop();
14532
14657
  };
14533
14658
  }, []);
14659
+ (0, import_react.useEffect)(() => {
14660
+ muxRef.current?.observe(route.kind === "chat" ? route.session.sessionId : void 0);
14661
+ }, [route]);
14534
14662
  const back = (0, import_react.useCallback)(() => {
14535
14663
  setRoute((previous) => {
14536
14664
  if (previous.kind === "chat") return {