@poncho-ai/browser 0.6.25 → 0.6.26

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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @poncho-ai/browser@0.6.25 build /home/runner/work/poncho-ai/poncho-ai/packages/browser
2
+ > @poncho-ai/browser@0.6.26 build /home/runner/work/poncho-ai/poncho-ai/packages/browser
3
3
  > tsup src/index.ts --format esm --dts
4
4
 
5
5
  CLI Building entry: src/index.ts
@@ -7,8 +7,8 @@
7
7
  CLI tsup v8.5.1
8
8
  CLI Target: es2022
9
9
  ESM Build start
10
- ESM dist/index.js 47.44 KB
11
- ESM ⚡️ Build success in 63ms
10
+ ESM dist/index.js 47.98 KB
11
+ ESM ⚡️ Build success in 60ms
12
12
  DTS Build start
13
- DTS ⚡️ Build success in 5080ms
14
- DTS dist/index.d.ts 13.69 KB
13
+ DTS ⚡️ Build success in 4894ms
14
+ DTS dist/index.d.ts 13.77 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # @poncho-ai/browser
2
2
 
3
+ ## 0.6.26
4
+
5
+ ### Patch Changes
6
+
7
+ - [#182](https://github.com/cesr/poncho-ai/pull/182) [`5ca3615`](https://github.com/cesr/poncho-ai/commit/5ca361576cbe1a97e6315f550a58a302b4e70aca) Thanks [@cesr](https://github.com/cesr)! - Keep host viewport listeners alive across browser sessions. `onFrame` /
8
+ `onStatus` listeners were stored inside the per-conversation `ConversationTab`
9
+ object, so `closeTab` (and LRU eviction) deleted them along with the tab. When
10
+ an agent closed one browser and opened another in the same conversation, the
11
+ new tab had empty listener sets — the host's live-viewport subscription was
12
+ silently orphaned, so the second session's `browser:status` / frames never
13
+ reached the client until it reconnected (the "pill/sheet doesn't appear, or is
14
+ left over after close, until I navigate away and back" bug). Listeners now live
15
+ in session-level maps keyed by conversationId, independent of any tab's
16
+ lifetime; they persist until the host unsubscribes, and `emitStatus` delivers
17
+ the final `active:false` on close before the tab is removed.
18
+
3
19
  ## 0.6.25
4
20
 
5
21
  ### Patch Changes
package/dist/index.d.ts CHANGED
@@ -80,6 +80,8 @@ declare class BrowserSession {
80
80
  private readonly sessionId;
81
81
  private manager;
82
82
  private readonly tabs;
83
+ private readonly frameListeners;
84
+ private readonly statusListeners;
83
85
  private _contextStealthInstalled;
84
86
  private readonly _uaOverrideApplied;
85
87
  private _lockQueue;
package/dist/index.js CHANGED
@@ -250,6 +250,15 @@ var BrowserSession = class {
250
250
  manager;
251
251
  // Tab management: conversationId → tab state
252
252
  tabs = /* @__PURE__ */ new Map();
253
+ // Viewport listeners, keyed by conversationId and kept SEPARATE from the
254
+ // tab. A host (e.g. a live iOS viewport) subscribes once; its listeners must
255
+ // outlive any individual tab so that closing one browser and opening another
256
+ // in the same conversation — or an LRU tab eviction — doesn't silently
257
+ // orphan the subscription (the symptom: the second session's status/frames
258
+ // never reach the client until it reconnects). Tabs come and go; listeners
259
+ // persist until the host unsubscribes.
260
+ frameListeners = /* @__PURE__ */ new Map();
261
+ statusListeners = /* @__PURE__ */ new Map();
253
262
  // Whether context-level stealth init script has been installed
254
263
  _contextStealthInstalled = false;
255
264
  // Track which tabs have had per-page CDP UA override applied
@@ -549,13 +558,10 @@ var BrowserSession = class {
549
558
  if (realTabs > 0) {
550
559
  await mgr.newTab();
551
560
  }
552
- const existing = tab;
553
561
  tab = {
554
562
  tabIndex: mgr.getActiveIndex(),
555
563
  active: true,
556
- lastUsed: Date.now(),
557
- frameListeners: existing?.frameListeners ?? /* @__PURE__ */ new Set(),
558
- statusListeners: existing?.statusListeners ?? /* @__PURE__ */ new Set()
564
+ lastUsed: Date.now()
559
565
  };
560
566
  this.tabs.set(conversationId, tab);
561
567
  } else {
@@ -829,15 +835,15 @@ var BrowserSession = class {
829
835
  (frame) => {
830
836
  const cid = this._screencastConversation;
831
837
  if (!cid) return;
832
- const t = this.tabs.get(cid);
833
- if (!t) return;
838
+ const listeners = this.frameListeners.get(cid);
839
+ if (!listeners || listeners.size === 0) return;
834
840
  const browserFrame = {
835
841
  data: frame.data,
836
842
  width: frame.metadata.deviceWidth,
837
843
  height: frame.metadata.deviceHeight,
838
844
  timestamp: Date.now()
839
845
  };
840
- for (const listener of t.frameListeners) {
846
+ for (const listener of listeners) {
841
847
  try {
842
848
  listener(browserFrame);
843
849
  } catch {
@@ -865,29 +871,37 @@ var BrowserSession = class {
865
871
  // Per-conversation event listeners
866
872
  // -----------------------------------------------------------------------
867
873
  onFrame(conversationId, listener) {
868
- let tab = this.tabs.get(conversationId);
869
- if (!tab) {
870
- tab = { tabIndex: -1, active: false, lastUsed: Date.now(), frameListeners: /* @__PURE__ */ new Set(), statusListeners: /* @__PURE__ */ new Set() };
871
- this.tabs.set(conversationId, tab);
874
+ let set = this.frameListeners.get(conversationId);
875
+ if (!set) {
876
+ set = /* @__PURE__ */ new Set();
877
+ this.frameListeners.set(conversationId, set);
872
878
  }
873
- tab.frameListeners.add(listener);
879
+ set.add(listener);
874
880
  return () => {
875
- tab.frameListeners.delete(listener);
876
- if (tab.frameListeners.size === 0 && this._screencastConversation === conversationId) {
877
- this.stopScreencast().catch(() => {
878
- });
881
+ const s = this.frameListeners.get(conversationId);
882
+ if (!s) return;
883
+ s.delete(listener);
884
+ if (s.size === 0) {
885
+ this.frameListeners.delete(conversationId);
886
+ if (this._screencastConversation === conversationId) {
887
+ this.stopScreencast().catch(() => {
888
+ });
889
+ }
879
890
  }
880
891
  };
881
892
  }
882
893
  onStatus(conversationId, listener) {
883
- let tab = this.tabs.get(conversationId);
884
- if (!tab) {
885
- tab = { tabIndex: -1, active: false, lastUsed: Date.now(), frameListeners: /* @__PURE__ */ new Set(), statusListeners: /* @__PURE__ */ new Set() };
886
- this.tabs.set(conversationId, tab);
894
+ let set = this.statusListeners.get(conversationId);
895
+ if (!set) {
896
+ set = /* @__PURE__ */ new Set();
897
+ this.statusListeners.set(conversationId, set);
887
898
  }
888
- tab.statusListeners.add(listener);
899
+ set.add(listener);
889
900
  return () => {
890
- tab.statusListeners.delete(listener);
901
+ const s = this.statusListeners.get(conversationId);
902
+ if (!s) return;
903
+ s.delete(listener);
904
+ if (s.size === 0) this.statusListeners.delete(conversationId);
891
905
  };
892
906
  }
893
907
  // -----------------------------------------------------------------------
@@ -1087,8 +1101,9 @@ var BrowserSession = class {
1087
1101
  url: tab?.url,
1088
1102
  interactionAllowed: tab?.active ?? false
1089
1103
  };
1090
- if (tab) {
1091
- for (const listener of tab.statusListeners) {
1104
+ const listeners = this.statusListeners.get(conversationId);
1105
+ if (listeners) {
1106
+ for (const listener of listeners) {
1092
1107
  try {
1093
1108
  listener(status);
1094
1109
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poncho-ai/browser",
3
- "version": "0.6.25",
3
+ "version": "0.6.26",
4
4
  "description": "Browser automation for Poncho agents, powered by agent-browser",
5
5
  "repository": {
6
6
  "type": "git",
package/src/session.ts CHANGED
@@ -144,8 +144,6 @@ interface ConversationTab {
144
144
  url?: string;
145
145
  active: boolean;
146
146
  lastUsed: number;
147
- frameListeners: Set<FrameListener>;
148
- statusListeners: Set<StatusListener>;
149
147
  }
150
148
 
151
149
  export class BrowserSession {
@@ -156,6 +154,16 @@ export class BrowserSession {
156
154
  // Tab management: conversationId → tab state
157
155
  private readonly tabs = new Map<string, ConversationTab>();
158
156
 
157
+ // Viewport listeners, keyed by conversationId and kept SEPARATE from the
158
+ // tab. A host (e.g. a live iOS viewport) subscribes once; its listeners must
159
+ // outlive any individual tab so that closing one browser and opening another
160
+ // in the same conversation — or an LRU tab eviction — doesn't silently
161
+ // orphan the subscription (the symptom: the second session's status/frames
162
+ // never reach the client until it reconnects). Tabs come and go; listeners
163
+ // persist until the host unsubscribes.
164
+ private readonly frameListeners = new Map<string, Set<FrameListener>>();
165
+ private readonly statusListeners = new Map<string, Set<StatusListener>>();
166
+
159
167
  // Whether context-level stealth init script has been installed
160
168
  private _contextStealthInstalled = false;
161
169
 
@@ -500,13 +508,10 @@ export class BrowserSession {
500
508
  if (realTabs > 0) {
501
509
  await mgr.newTab();
502
510
  }
503
- const existing = tab;
504
511
  tab = {
505
512
  tabIndex: mgr.getActiveIndex(),
506
513
  active: true,
507
514
  lastUsed: Date.now(),
508
- frameListeners: existing?.frameListeners ?? new Set(),
509
- statusListeners: existing?.statusListeners ?? new Set(),
510
515
  };
511
516
  this.tabs.set(conversationId, tab);
512
517
  } else {
@@ -799,15 +804,15 @@ export class BrowserSession {
799
804
  (frame) => {
800
805
  const cid = this._screencastConversation;
801
806
  if (!cid) return;
802
- const t = this.tabs.get(cid);
803
- if (!t) return;
807
+ const listeners = this.frameListeners.get(cid);
808
+ if (!listeners || listeners.size === 0) return;
804
809
  const browserFrame: BrowserFrame = {
805
810
  data: frame.data,
806
811
  width: frame.metadata.deviceWidth,
807
812
  height: frame.metadata.deviceHeight,
808
813
  timestamp: Date.now(),
809
814
  };
810
- for (const listener of t.frameListeners) {
815
+ for (const listener of listeners) {
811
816
  try { listener(browserFrame); } catch { /* */ }
812
817
  }
813
818
  },
@@ -835,28 +840,38 @@ export class BrowserSession {
835
840
  // -----------------------------------------------------------------------
836
841
 
837
842
  onFrame(conversationId: string, listener: FrameListener): () => void {
838
- let tab = this.tabs.get(conversationId);
839
- if (!tab) {
840
- tab = { tabIndex: -1, active: false, lastUsed: Date.now(), frameListeners: new Set(), statusListeners: new Set() };
841
- this.tabs.set(conversationId, tab);
843
+ let set = this.frameListeners.get(conversationId);
844
+ if (!set) {
845
+ set = new Set();
846
+ this.frameListeners.set(conversationId, set);
842
847
  }
843
- tab.frameListeners.add(listener);
848
+ set.add(listener);
844
849
  return () => {
845
- tab!.frameListeners.delete(listener);
846
- if (tab!.frameListeners.size === 0 && this._screencastConversation === conversationId) {
847
- this.stopScreencast().catch(() => {});
850
+ const s = this.frameListeners.get(conversationId);
851
+ if (!s) return;
852
+ s.delete(listener);
853
+ if (s.size === 0) {
854
+ this.frameListeners.delete(conversationId);
855
+ if (this._screencastConversation === conversationId) {
856
+ this.stopScreencast().catch(() => {});
857
+ }
848
858
  }
849
859
  };
850
860
  }
851
861
 
852
862
  onStatus(conversationId: string, listener: StatusListener): () => void {
853
- let tab = this.tabs.get(conversationId);
854
- if (!tab) {
855
- tab = { tabIndex: -1, active: false, lastUsed: Date.now(), frameListeners: new Set(), statusListeners: new Set() };
856
- this.tabs.set(conversationId, tab);
863
+ let set = this.statusListeners.get(conversationId);
864
+ if (!set) {
865
+ set = new Set();
866
+ this.statusListeners.set(conversationId, set);
857
867
  }
858
- tab.statusListeners.add(listener);
859
- return () => { tab!.statusListeners.delete(listener); };
868
+ set.add(listener);
869
+ return () => {
870
+ const s = this.statusListeners.get(conversationId);
871
+ if (!s) return;
872
+ s.delete(listener);
873
+ if (s.size === 0) this.statusListeners.delete(conversationId);
874
+ };
860
875
  }
861
876
 
862
877
  // -----------------------------------------------------------------------
@@ -1068,8 +1083,12 @@ export class BrowserSession {
1068
1083
  url: tab?.url,
1069
1084
  interactionAllowed: tab?.active ?? false,
1070
1085
  };
1071
- if (tab) {
1072
- for (const listener of tab.statusListeners) {
1086
+ // Listeners live at the session level, so a close (which deletes the tab)
1087
+ // still delivers the final active:false to the host before the tab is
1088
+ // gone — and a later reopen reuses the same subscription.
1089
+ const listeners = this.statusListeners.get(conversationId);
1090
+ if (listeners) {
1091
+ for (const listener of listeners) {
1073
1092
  try { listener(status); } catch { /* */ }
1074
1093
  }
1075
1094
  }