@memberjunction/ng-conversations 5.41.0 → 5.43.0

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 (69) hide show
  1. package/dist/__tests__/remote-browser-channel.test.js +163 -1
  2. package/dist/__tests__/remote-browser-channel.test.js.map +1 -1
  3. package/dist/lib/components/collection/collections-full-view.component.d.ts +105 -2
  4. package/dist/lib/components/collection/collections-full-view.component.d.ts.map +1 -1
  5. package/dist/lib/components/collection/collections-full-view.component.js +1378 -489
  6. package/dist/lib/components/collection/collections-full-view.component.js.map +1 -1
  7. package/dist/lib/components/conversation/conversation-chat-area.component.d.ts +4 -0
  8. package/dist/lib/components/conversation/conversation-chat-area.component.d.ts.map +1 -1
  9. package/dist/lib/components/conversation/conversation-chat-area.component.js +13 -8
  10. package/dist/lib/components/conversation/conversation-chat-area.component.js.map +1 -1
  11. package/dist/lib/components/conversation/conversation-list.component.d.ts +98 -5
  12. package/dist/lib/components/conversation/conversation-list.component.d.ts.map +1 -1
  13. package/dist/lib/components/conversation/conversation-list.component.js +1102 -401
  14. package/dist/lib/components/conversation/conversation-list.component.js.map +1 -1
  15. package/dist/lib/components/mention/mention-dropdown.component.js +37 -19
  16. package/dist/lib/components/mention/mention-dropdown.component.js.map +1 -1
  17. package/dist/lib/components/mention/mention-editor.component.d.ts.map +1 -1
  18. package/dist/lib/components/mention/mention-editor.component.js +10 -3
  19. package/dist/lib/components/mention/mention-editor.component.js.map +1 -1
  20. package/dist/lib/components/message/message-input.component.js +1 -1
  21. package/dist/lib/components/message/message-input.component.js.map +1 -1
  22. package/dist/lib/components/message/message-item.component.d.ts +5 -0
  23. package/dist/lib/components/message/message-item.component.d.ts.map +1 -1
  24. package/dist/lib/components/message/message-item.component.js +26 -3
  25. package/dist/lib/components/message/message-item.component.js.map +1 -1
  26. package/dist/lib/components/project/project-form-modal.component.d.ts +5 -1
  27. package/dist/lib/components/project/project-form-modal.component.d.ts.map +1 -1
  28. package/dist/lib/components/project/project-form-modal.component.js +104 -85
  29. package/dist/lib/components/project/project-form-modal.component.js.map +1 -1
  30. package/dist/lib/components/realtime/channels/base-realtime-channel-client.d.ts +22 -0
  31. package/dist/lib/components/realtime/channels/base-realtime-channel-client.d.ts.map +1 -1
  32. package/dist/lib/components/realtime/channels/base-realtime-channel-client.js +40 -0
  33. package/dist/lib/components/realtime/channels/base-realtime-channel-client.js.map +1 -1
  34. package/dist/lib/components/realtime/realtime-agent-picker.component.d.ts +12 -1
  35. package/dist/lib/components/realtime/realtime-agent-picker.component.d.ts.map +1 -1
  36. package/dist/lib/components/realtime/realtime-agent-picker.component.js +97 -19
  37. package/dist/lib/components/realtime/realtime-agent-picker.component.js.map +1 -1
  38. package/dist/lib/components/realtime/remote-browser/remote-browser-channel.d.ts +33 -0
  39. package/dist/lib/components/realtime/remote-browser/remote-browser-channel.d.ts.map +1 -1
  40. package/dist/lib/components/realtime/remote-browser/remote-browser-channel.js +164 -7
  41. package/dist/lib/components/realtime/remote-browser/remote-browser-channel.js.map +1 -1
  42. package/dist/lib/components/realtime/remote-browser/remote-browser-surface.component.d.ts +35 -2
  43. package/dist/lib/components/realtime/remote-browser/remote-browser-surface.component.d.ts.map +1 -1
  44. package/dist/lib/components/realtime/remote-browser/remote-browser-surface.component.js +67 -4
  45. package/dist/lib/components/realtime/remote-browser/remote-browser-surface.component.js.map +1 -1
  46. package/dist/lib/components/realtime/remote-browser/remote-browser-tools.d.ts +1 -0
  47. package/dist/lib/components/realtime/remote-browser/remote-browser-tools.d.ts.map +1 -1
  48. package/dist/lib/components/realtime/remote-browser/remote-browser-tools.js +13 -0
  49. package/dist/lib/components/realtime/remote-browser/remote-browser-tools.js.map +1 -1
  50. package/dist/lib/components/workspace/conversation-workspace.component.d.ts +4 -0
  51. package/dist/lib/components/workspace/conversation-workspace.component.d.ts.map +1 -1
  52. package/dist/lib/components/workspace/conversation-workspace.component.js +13 -8
  53. package/dist/lib/components/workspace/conversation-workspace.component.js.map +1 -1
  54. package/dist/lib/models/conversation-state.model.d.ts +1 -1
  55. package/dist/lib/models/conversation-state.model.d.ts.map +1 -1
  56. package/dist/lib/models/conversation-state.model.js.map +1 -1
  57. package/dist/lib/services/artifact-state.service.d.ts +1 -1
  58. package/dist/lib/services/artifact-state.service.d.ts.map +1 -1
  59. package/dist/lib/services/artifact-state.service.js +7 -2
  60. package/dist/lib/services/artifact-state.service.js.map +1 -1
  61. package/dist/lib/services/mention-autocomplete.service.d.ts +25 -5
  62. package/dist/lib/services/mention-autocomplete.service.d.ts.map +1 -1
  63. package/dist/lib/services/mention-autocomplete.service.js +86 -18
  64. package/dist/lib/services/mention-autocomplete.service.js.map +1 -1
  65. package/dist/lib/services/realtime-pairing.d.ts +8 -7
  66. package/dist/lib/services/realtime-pairing.d.ts.map +1 -1
  67. package/dist/lib/services/realtime-pairing.js +19 -9
  68. package/dist/lib/services/realtime-pairing.js.map +1 -1
  69. package/package.json +26 -26
@@ -50,6 +50,13 @@ export declare class RemoteBrowserChannel extends BaseRealtimeChannelClient<Remo
50
50
  private audioPlayer;
51
51
  /** Subscription to the bound surface's `AudioMutedChange` output (the speaker toggle), torn down on unbind. */
52
52
  private audioMutedSub;
53
+ /**
54
+ * Poll cadence + deadline for {@link pollGoalResult} (the async `browser_AchieveGoal` result poll).
55
+ * Protected so tests can shrink them; production polls every 2.5s for up to 5 minutes (a goal loop
56
+ * past 5 min keeps running server-side, the model just gets a "still running" note).
57
+ */
58
+ protected GoalPollIntervalMs: number;
59
+ protected GoalPollTimeoutMs: number;
53
60
  get ChannelName(): string;
54
61
  get ToolNamePrefix(): string;
55
62
  get TabTitle(): string;
@@ -120,6 +127,13 @@ export declare class RemoteBrowserChannel extends BaseRealtimeChannelClient<Remo
120
127
  * @param input The flattened pointer/keyboard input the user performed on the live canvas.
121
128
  */
122
129
  private relayHumanInput;
130
+ /**
131
+ * Reads the remote page's current selection for COPY-OUT — backs the surface's `FetchSelection`. Awaits
132
+ * the `GetRemoteBrowserSelection` query and returns its text. Best-effort by contract
133
+ * ({@link RemoteBrowserSelectionFetcher}): returns `''` when no session is live or the query fails, so a
134
+ * best-effort copy never throws.
135
+ */
136
+ private fetchSelection;
123
137
  /** Asks the server to stop the screencast (best-effort) and clears the streaming flag. */
124
138
  private stopScreencast;
125
139
  /**
@@ -138,6 +152,25 @@ export declare class RemoteBrowserChannel extends BaseRealtimeChannelClient<Remo
138
152
  ApplyAgentTool(toolName: string, argsJson: string): Promise<string>;
139
153
  /** Builds the flat mutation variables from a normalized action (omitted fields ride as null). */
140
154
  private buildVariables;
155
+ /**
156
+ * GOAL path for the `browser_AchieveGoal` tool: hands a high-level goal to the server's autonomous
157
+ * browser agent (computer-use loop / native AI) via {@link EXECUTE_REMOTE_BROWSER_GOAL_MUTATION} and
158
+ * returns a concise result for the model. Never throws — failures map to a `{ success: false }` string.
159
+ *
160
+ * @param argsJson The raw tool-call arguments (`{ goal, startUrl? }`).
161
+ * @returns The model-readable result string.
162
+ */
163
+ private achieveGoal;
164
+ /**
165
+ * Polls {@link GET_REMOTE_BROWSER_GOAL_RESULT_QUERY} for a started goal run until it reports a
166
+ * terminal status (anything other than `Running`), then returns that terminal payload. Each poll is
167
+ * a short request, so no transport boundary times out while a long goal loop runs server-side.
168
+ *
169
+ * Returns `null` if the goal is still `Running` after {@link GoalPollTimeoutMs} (the loop keeps
170
+ * running server-side; the caller maps this to a "still running" message), the session went away, or
171
+ * the run id is no longer known (`Unknown` — expired/superseded).
172
+ */
173
+ private pollGoalResult;
141
174
  /**
142
175
  * Runs the VISION-QUERY path for the `browser_DescribePage` / `browser_LocateElement` tools: awaits the
143
176
  * `InterpretRemoteBrowserPage` mutation (server captures a screenshot + runs a fast vision model) and returns
@@ -1 +1 @@
1
- {"version":3,"file":"remote-browser-channel.d.ts","sourceRoot":"","sources":["../../../../../src/lib/components/realtime/remote-browser/remote-browser-channel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAG1C,OAAO,EAAa,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AACvE,OAAO,EAAE,yBAAyB,EAAE,wBAAwB,EAAE,MAAM,0CAA0C,CAAC;AAC/G,OAAO,EAA2D,6BAA6B,EAAE,MAAM,oCAAoC,CAAC;AAS5I,OAAO,EAAwB,4BAA4B,EAA4B,MAAM,+BAA+B,CAAC;AA0K7H;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,qBACa,oBAAqB,SAAQ,yBAAyB,CAAC,6BAA6B,CAAC;IAChG,2EAA2E;IAC3E,OAAO,CAAC,OAAO,CAA8C;IAE7D,4FAA4F;IAC5F,OAAO,CAAC,aAAa,CAA6B;IAElD;;;;OAIG;IACH,OAAO,CAAC,SAAS,CAAS;IAE1B;;;;OAIG;IACH,OAAO,CAAC,cAAc,CAAS;IAE/B,yGAAyG;IACzG,OAAO,CAAC,WAAW,CAAyC;IAE5D,+GAA+G;IAC/G,OAAO,CAAC,aAAa,CAA6B;IAElD,IAAW,WAAW,IAAI,MAAM,CAE/B;IAED,IAAW,cAAc,IAAI,MAAM,CAElC;IAED,IAAW,QAAQ,IAAI,MAAM,CAE5B;IAED,IAAW,OAAO,IAAI,MAAM,CAE3B;IAEM,kBAAkB,IAAI,sBAAsB,EAAE;IAIrC,mBAAmB,IAAI,IAAI,CAAC,6BAA6B,CAAC;IAI1E,2FAA2F;IAC3E,oBAAoB,IAAI,wBAAwB;IAgBhE;;;;;;;;;;;;;;;;;OAiBG;IACI,WAAW,CAAC,QAAQ,EAAE,6BAA6B,GAAG,IAAI;IAUjD,aAAa,IAAI,IAAI;IAUrB,OAAO,IAAI,IAAI;IAU/B;;;;;;OAMG;IACI,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAMlD;;;;;;OAMG;IACI,YAAY,CAAC,KAAK,EAAE,4BAA4B,GAAG,IAAI;IAM9D;;;;OAIG;YACW,eAAe;IAY7B;;;;;;;OAOG;YACW,gBAAgB;IAe9B,yGAAyG;YAC3F,eAAe;IAY7B;;;;;;OAMG;IACH,OAAO,CAAC,eAAe;IAmBvB,0FAA0F;YAC5E,cAAc;IAU5B;;;;OAIG;YACW,aAAa;IAS3B;;;;;;OAMG;IACU,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAwDhF,iGAAiG;IACjG,OAAO,CAAC,cAAc;IAgBtB;;;;;;;;;OASG;YACW,aAAa;IAmC3B,iGAAiG;IACjG,OAAO,CAAC,WAAW;IAInB,6FAA6F;IAC7F,OAAO,CAAC,SAAS;IAajB,iEAAiE;IACjE,OAAO,CAAC,EAAE;IAWV,6DAA6D;IAC7D,OAAO,CAAC,IAAI;CAIb;AAED;;;;;GAKG;AACH,wBAAgB,gCAAgC,IAAI,IAAI,CAEvD"}
1
+ {"version":3,"file":"remote-browser-channel.d.ts","sourceRoot":"","sources":["../../../../../src/lib/components/realtime/remote-browser/remote-browser-channel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAG1C,OAAO,EAAa,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AACvE,OAAO,EAAE,yBAAyB,EAAE,wBAAwB,EAAE,MAAM,0CAA0C,CAAC;AAC/G,OAAO,EAA2D,6BAA6B,EAAE,MAAM,oCAAoC,CAAC;AAS5I,OAAO,EAAwB,4BAA4B,EAA4B,MAAM,+BAA+B,CAAC;AA2P7H;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,qBACa,oBAAqB,SAAQ,yBAAyB,CAAC,6BAA6B,CAAC;IAChG,2EAA2E;IAC3E,OAAO,CAAC,OAAO,CAA8C;IAE7D,4FAA4F;IAC5F,OAAO,CAAC,aAAa,CAA6B;IAElD;;;;OAIG;IACH,OAAO,CAAC,SAAS,CAAS;IAE1B;;;;OAIG;IACH,OAAO,CAAC,cAAc,CAAS;IAE/B,yGAAyG;IACzG,OAAO,CAAC,WAAW,CAAyC;IAE5D,+GAA+G;IAC/G,OAAO,CAAC,aAAa,CAA6B;IAElD;;;;OAIG;IACH,SAAS,CAAC,kBAAkB,SAAQ;IACpC,SAAS,CAAC,iBAAiB,SAAiB;IAE5C,IAAW,WAAW,IAAI,MAAM,CAE/B;IAED,IAAW,cAAc,IAAI,MAAM,CAElC;IAED,IAAW,QAAQ,IAAI,MAAM,CAE5B;IAED,IAAW,OAAO,IAAI,MAAM,CAE3B;IAEM,kBAAkB,IAAI,sBAAsB,EAAE;IAIrC,mBAAmB,IAAI,IAAI,CAAC,6BAA6B,CAAC;IAI1E,2FAA2F;IAC3E,oBAAoB,IAAI,wBAAwB;IAgBhE;;;;;;;;;;;;;;;;;OAiBG;IACI,WAAW,CAAC,QAAQ,EAAE,6BAA6B,GAAG,IAAI;IAYjD,aAAa,IAAI,IAAI;IAUrB,OAAO,IAAI,IAAI;IAU/B;;;;;;OAMG;IACI,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAMlD;;;;;;OAMG;IACI,YAAY,CAAC,KAAK,EAAE,4BAA4B,GAAG,IAAI;IAM9D;;;;OAIG;YACW,eAAe;IAY7B;;;;;;;OAOG;YACW,gBAAgB;IAe9B,yGAAyG;YAC3F,eAAe;IAY7B;;;;;;OAMG;IACH,OAAO,CAAC,eAAe;IAqBvB;;;;;OAKG;YACW,cAAc;IAS5B,0FAA0F;YAC5E,cAAc;IAU5B;;;;OAIG;YACW,aAAa;IAS3B;;;;;;OAMG;IACU,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IA6DhF,iGAAiG;IACjG,OAAO,CAAC,cAAc;IAgBtB;;;;;;;OAOG;YACW,WAAW;IAyCzB;;;;;;;;OAQG;YACW,cAAc;IAqB5B;;;;;;;;;OASG;YACW,aAAa;IAoC3B,iGAAiG;IACjG,OAAO,CAAC,WAAW;IAInB,6FAA6F;IAC7F,OAAO,CAAC,SAAS;IAajB,iEAAiE;IACjE,OAAO,CAAC,EAAE;IAWV,6DAA6D;IAC7D,OAAO,CAAC,IAAI;CAIb;AAED;;;;;GAKG;AACH,wBAAgB,gCAAgC,IAAI,IAAI,CAEvD"}
@@ -30,6 +30,44 @@ const EXECUTE_REMOTE_BROWSER_ACTION_MUTATION = `
30
30
  }
31
31
  }
32
32
  `;
33
+ /**
34
+ * GOAL mutation — hands a high-level goal to the server's autonomous browser agent (computer-use loop or
35
+ * backend native AI) instead of relaying a granular action. The server plans + executes; we await the
36
+ * terminal outcome. (Live per-step narration is a server-bridged concern; over this client-direct mutation
37
+ * we get the final result.)
38
+ */
39
+ const EXECUTE_REMOTE_BROWSER_GOAL_MUTATION = `
40
+ mutation ExecuteRemoteBrowserGoal($agentSessionID: String!, $goal: String!, $startUrl: String) {
41
+ ExecuteRemoteBrowserGoal(agentSessionID: $agentSessionID, goal: $goal, startUrl: $startUrl) {
42
+ Success
43
+ GoalRunID
44
+ Strategy
45
+ CurrentUrl
46
+ Status
47
+ StepCount
48
+ Detail
49
+ }
50
+ }
51
+ `;
52
+ /**
53
+ * POLL query for the async goal run. `ExecuteRemoteBrowserGoal` STARTS the (potentially multi-minute)
54
+ * goal loop and returns a `GoalRunID` with `Status: 'Running'`; the channel polls this until the run
55
+ * reports a terminal status — so no single request is held open for the whole loop (which would die at
56
+ * a browser/proxy/janitor transport timeout and lose the result).
57
+ */
58
+ const GET_REMOTE_BROWSER_GOAL_RESULT_QUERY = `
59
+ query GetRemoteBrowserGoalResult($agentSessionID: String!, $goalRunID: String!) {
60
+ GetRemoteBrowserGoalResult(agentSessionID: $agentSessionID, goalRunID: $goalRunID) {
61
+ Success
62
+ GoalRunID
63
+ Strategy
64
+ CurrentUrl
65
+ Status
66
+ StepCount
67
+ Detail
68
+ }
69
+ }
70
+ `;
33
71
  /**
34
72
  * GraphQL mutation that asks the server to start PUSHING live CDP screencast frames for the session.
35
73
  * Returns `Streaming: true` when the backend supports `ScreenStreaming` and the stream started; the
@@ -76,14 +114,26 @@ const STOP_AUDIO_STREAM_MUTATION = `
76
114
  const RELAY_HUMAN_INPUT_MUTATION = `
77
115
  mutation RelayRemoteBrowserHumanInput(
78
116
  $agentSessionID: String!, $kind: String!, $x: Float, $y: Float, $button: String, $key: String,
79
- $deltaX: Float, $deltaY: Float, $modifiers: String
117
+ $text: String, $deltaX: Float, $deltaY: Float, $modifiers: String
80
118
  ) {
81
119
  RelayRemoteBrowserHumanInput(
82
120
  agentSessionID: $agentSessionID, kind: $kind, x: $x, y: $y, button: $button, key: $key,
83
- deltaX: $deltaX, deltaY: $deltaY, modifiers: $modifiers
121
+ text: $text, deltaX: $deltaX, deltaY: $deltaY, modifiers: $modifiers
84
122
  )
85
123
  }
86
124
  `;
125
+ /**
126
+ * COPY-OUT query — reads the remote page's current text selection so the surface can write it to the LOCAL
127
+ * clipboard on a `copy` / Cmd+C (the mirror of the `'text'` paste-in path). Returns `''` when nothing is
128
+ * selected, no live browser exists, or the backend lacks `HumanTakeover`.
129
+ */
130
+ const GET_SELECTION_QUERY = `
131
+ query GetRemoteBrowserSelection($agentSessionID: String!) {
132
+ GetRemoteBrowserSelection(agentSessionID: $agentSessionID) {
133
+ Text
134
+ }
135
+ }
136
+ `;
87
137
  /** GraphQL query the surface fetcher runs — the live screenshot + URL of the server browser. */
88
138
  const REMOTE_BROWSER_SNAPSHOT_QUERY = `
89
139
  query RemoteBrowserSnapshot($agentSessionID: String!) {
@@ -160,6 +210,13 @@ let RemoteBrowserChannel = class RemoteBrowserChannel extends BaseRealtimeChanne
160
210
  audioPlayer = null;
161
211
  /** Subscription to the bound surface's `AudioMutedChange` output (the speaker toggle), torn down on unbind. */
162
212
  audioMutedSub = null;
213
+ /**
214
+ * Poll cadence + deadline for {@link pollGoalResult} (the async `browser_AchieveGoal` result poll).
215
+ * Protected so tests can shrink them; production polls every 2.5s for up to 5 minutes (a goal loop
216
+ * past 5 min keeps running server-side, the model just gets a "still running" note).
217
+ */
218
+ GoalPollIntervalMs = 2500;
219
+ GoalPollTimeoutMs = 5 * 60 * 1000;
163
220
  get ChannelName() {
164
221
  return 'Remote Browser';
165
222
  }
@@ -187,7 +244,7 @@ let RemoteBrowserChannel = class RemoteBrowserChannel extends BaseRealtimeChanne
187
244
  'up right where you left off.',
188
245
  Tips: [
189
246
  'Ask the agent to look something up, open a site or fill in a form — it controls the page.',
190
- "Click on the live view to take over, then click and type to drive it yourself — the \"You're driving\" badge shows when takeover is active.",
247
+ 'Click on the live view to take over, then click and type to drive it yourself — the "You\'re driving" badge shows when takeover is active.',
191
248
  'The current page URL is shown so you always know where the agent has navigated.',
192
249
  ],
193
250
  IconClass: 'fa-solid fa-globe',
@@ -214,6 +271,8 @@ let RemoteBrowserChannel = class RemoteBrowserChannel extends BaseRealtimeChanne
214
271
  BindSurface(instance) {
215
272
  this.surface = instance;
216
273
  instance.Fetch = () => this.fetchSnapshot();
274
+ // Copy-out: the surface reads the remote selection through this on a local `copy` and writes it locally.
275
+ instance.FetchSelection = () => this.fetchSelection();
217
276
  instance.Interactive = true;
218
277
  this.humanInputSub = instance.HumanInput.subscribe((input) => this.relayHumanInput(input));
219
278
  this.audioMutedSub = instance.AudioMutedChange.subscribe((muted) => this.audioPlayer?.SetMuted(muted));
@@ -331,12 +390,28 @@ let RemoteBrowserChannel = class RemoteBrowserChannel extends BaseRealtimeChanne
331
390
  y: input.y ?? null,
332
391
  button: input.button ?? null,
333
392
  key: input.key ?? null,
393
+ // Paste-in: the `'text'` kind carries the local clipboard text the server inserts into the page.
394
+ text: input.text ?? null,
334
395
  deltaX: input.deltaX ?? null,
335
396
  deltaY: input.deltaY ?? null,
336
397
  // Server expects a comma-separated modifier list (e.g. "Shift,Control"); null when none held.
337
398
  modifiers: input.modifiers && input.modifiers.length > 0 ? input.modifiers.join(',') : null,
338
399
  });
339
400
  }
401
+ /**
402
+ * Reads the remote page's current selection for COPY-OUT — backs the surface's `FetchSelection`. Awaits
403
+ * the `GetRemoteBrowserSelection` query and returns its text. Best-effort by contract
404
+ * ({@link RemoteBrowserSelectionFetcher}): returns `''` when no session is live or the query fails, so a
405
+ * best-effort copy never throws.
406
+ */
407
+ async fetchSelection() {
408
+ const sessionId = this.Context?.AgentSessionID;
409
+ if (!sessionId) {
410
+ return '';
411
+ }
412
+ const data = await this.Context?.ExecuteServerAction(GET_SELECTION_QUERY, { agentSessionID: sessionId });
413
+ return data?.GetRemoteBrowserSelection?.Text ?? '';
414
+ }
340
415
  /** Asks the server to stop the screencast (best-effort) and clears the streaming flag. */
341
416
  async stopScreencast() {
342
417
  const wasStreaming = this.streaming;
@@ -373,6 +448,9 @@ let RemoteBrowserChannel = class RemoteBrowserChannel extends BaseRealtimeChanne
373
448
  if (toolName === REMOTE_BROWSER_TOOL_NAMES.DescribePage) {
374
449
  return this.interpretPage(undefined);
375
450
  }
451
+ if (toolName === REMOTE_BROWSER_TOOL_NAMES.AchieveGoal) {
452
+ return this.achieveGoal(argsJson);
453
+ }
376
454
  if (toolName === REMOTE_BROWSER_TOOL_NAMES.LocateElement) {
377
455
  const description = this.asArgString(this.parseArgs(argsJson)['description']);
378
456
  if (!description) {
@@ -388,10 +466,12 @@ let RemoteBrowserChannel = class RemoteBrowserChannel extends BaseRealtimeChanne
388
466
  const message = error instanceof RemoteBrowserToolArgError ? error.message : 'Invalid Remote Browser tool arguments.';
389
467
  return this.fail(message);
390
468
  }
391
- const sessionId = this.Context?.AgentSessionID;
469
+ // Briefly WAIT for the session id rather than failing instantly: the model can fire a tool the
470
+ // first beat it connects, before mintSession binds the id (the "session id missing" race).
471
+ const sessionId = await this.ResolveAgentSessionId();
392
472
  if (!sessionId) {
393
- // Diagnostic: distinguishes "channel never Initialized (no Context)" from "session id not yet
394
- // set on the service" — the two distinct causes of a null id at tool-execution time.
473
+ // Diagnostic: distinguishes "channel never Initialized (no Context)" from "session id never
474
+ // resolved within the wait window" — the two distinct causes of a null id at tool-execution time.
395
475
  console.warn('[RemoteBrowser] browser tool invoked but AgentSessionID is null', {
396
476
  tool: toolName,
397
477
  hasContext: !!this.Context,
@@ -432,6 +512,82 @@ let RemoteBrowserChannel = class RemoteBrowserChannel extends BaseRealtimeChanne
432
512
  ms: action.Ms ?? null,
433
513
  };
434
514
  }
515
+ /**
516
+ * GOAL path for the `browser_AchieveGoal` tool: hands a high-level goal to the server's autonomous
517
+ * browser agent (computer-use loop / native AI) via {@link EXECUTE_REMOTE_BROWSER_GOAL_MUTATION} and
518
+ * returns a concise result for the model. Never throws — failures map to a `{ success: false }` string.
519
+ *
520
+ * @param argsJson The raw tool-call arguments (`{ goal, startUrl? }`).
521
+ * @returns The model-readable result string.
522
+ */
523
+ async achieveGoal(argsJson) {
524
+ // Wait briefly for the session id (mint/reconnect race) before giving up — see ResolveAgentSessionId.
525
+ const sessionId = await this.ResolveAgentSessionId();
526
+ if (!sessionId) {
527
+ return this.fail('No live browser session is available yet — the realtime session may still be connecting; try again in a moment.');
528
+ }
529
+ const args = this.parseArgs(argsJson);
530
+ const goal = this.asArgString(args['goal']);
531
+ if (!goal) {
532
+ return this.fail('browser_AchieveGoal requires a "goal" describing what to accomplish.');
533
+ }
534
+ const startUrl = this.asArgString(args['startUrl']) ?? null;
535
+ // START the goal (returns immediately with a GoalRunID) then POLL — a goal loop can run for
536
+ // minutes, far longer than any single request survives, so we never hold one open for the loop.
537
+ const startData = await this.Context?.ExecuteServerAction(EXECUTE_REMOTE_BROWSER_GOAL_MUTATION, {
538
+ agentSessionID: sessionId,
539
+ goal,
540
+ startUrl,
541
+ });
542
+ const started = startData?.ExecuteRemoteBrowserGoal ?? null;
543
+ if (!started) {
544
+ return this.fail('The browser goal could not be started (no response from the server).');
545
+ }
546
+ // Defensive: if the server ever returns a terminal status synchronously, use it directly.
547
+ const result = started.Status === 'Running' && started.GoalRunID
548
+ ? await this.pollGoalResult(sessionId, started.GoalRunID)
549
+ : started;
550
+ if (!result) {
551
+ return this.fail('The browser goal is taking longer than expected and could not be confirmed. It may still be running.');
552
+ }
553
+ if (result.CurrentUrl) {
554
+ this.Context?.SendContextNote(`[browser] current page: ${result.CurrentUrl}`);
555
+ this.surface?.SetCurrentUrl(result.CurrentUrl);
556
+ }
557
+ if (!result.Success) {
558
+ return this.fail(result.Detail ?? `The goal could not be completed (${result.Status ?? 'unknown'}).`);
559
+ }
560
+ return this.ok(result.CurrentUrl, result.Detail ?? `Goal completed (${result.Status ?? 'Completed'}, ${result.StepCount ?? 0} steps).`);
561
+ }
562
+ /**
563
+ * Polls {@link GET_REMOTE_BROWSER_GOAL_RESULT_QUERY} for a started goal run until it reports a
564
+ * terminal status (anything other than `Running`), then returns that terminal payload. Each poll is
565
+ * a short request, so no transport boundary times out while a long goal loop runs server-side.
566
+ *
567
+ * Returns `null` if the goal is still `Running` after {@link GoalPollTimeoutMs} (the loop keeps
568
+ * running server-side; the caller maps this to a "still running" message), the session went away, or
569
+ * the run id is no longer known (`Unknown` — expired/superseded).
570
+ */
571
+ async pollGoalResult(sessionId, goalRunID) {
572
+ const interval = Math.max(1, this.GoalPollIntervalMs);
573
+ for (let waited = 0; waited < this.GoalPollTimeoutMs; waited += interval) {
574
+ await new Promise((resolve) => setTimeout(resolve, interval));
575
+ // The session ended mid-goal — stop polling (the live browser is gone).
576
+ if (!this.Context?.AgentSessionID) {
577
+ return null;
578
+ }
579
+ const data = await this.Context.ExecuteServerAction(GET_REMOTE_BROWSER_GOAL_RESULT_QUERY, {
580
+ agentSessionID: sessionId,
581
+ goalRunID,
582
+ });
583
+ const payload = data?.GetRemoteBrowserGoalResult ?? null;
584
+ // A transient null (one failed poll) shouldn't abort — keep polling until the deadline.
585
+ if (payload && payload.Status !== 'Running') {
586
+ return payload;
587
+ }
588
+ }
589
+ return null;
590
+ }
435
591
  /**
436
592
  * Runs the VISION-QUERY path for the `browser_DescribePage` / `browser_LocateElement` tools: awaits the
437
593
  * `InterpretRemoteBrowserPage` mutation (server captures a screenshot + runs a fast vision model) and returns
@@ -443,7 +599,8 @@ let RemoteBrowserChannel = class RemoteBrowserChannel extends BaseRealtimeChanne
443
599
  * @returns A JSON string describing the interpretation for the model.
444
600
  */
445
601
  async interpretPage(query) {
446
- const sessionId = this.Context?.AgentSessionID;
602
+ // Wait briefly for the session id (mint/reconnect race) before giving up — see ResolveAgentSessionId.
603
+ const sessionId = await this.ResolveAgentSessionId();
447
604
  if (!sessionId) {
448
605
  console.warn('[RemoteBrowser] interpret tool invoked but AgentSessionID is null', {
449
606
  hasContext: !!this.Context,
@@ -1 +1 @@
1
- {"version":3,"file":"remote-browser-channel.js","sourceRoot":"","sources":["../../../../../src/lib/components/realtime/remote-browser/remote-browser-channel.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Type } from '@angular/core';\nimport type { Subscription } from 'rxjs';\nimport { RegisterClass } from '@memberjunction/global';\nimport { JSONValue, RealtimeToolDefinition } from '@memberjunction/ai';\nimport { BaseRealtimeChannelClient, ChannelOnboardingDetails } from '../channels/base-realtime-channel-client';\nimport { RemoteBrowserHumanInputEvent, RemoteBrowserSnapshotView, RemoteBrowserSurfaceComponent } from './remote-browser-surface.component';\nimport {\n MapToolToAction,\n REMOTE_BROWSER_TOOL_DEFINITIONS,\n REMOTE_BROWSER_TOOL_NAMES,\n REMOTE_BROWSER_TOOL_PREFIX,\n RemoteBrowserAction,\n RemoteBrowserToolArgError,\n} from './remote-browser-tools';\nimport { MediaSourceAudioSink, RemoteBrowserAudioChunkInput, RemoteBrowserAudioPlayer } from './remote-browser-audio-player';\n\n/**\n * GraphQL mutation that drives the SERVER-hosted browser. The channel awaits it for every\n * `browser_*` tool call and feeds the result back to the model.\n */\nconst EXECUTE_REMOTE_BROWSER_ACTION_MUTATION = `\n mutation ExecuteRemoteBrowserAction(\n $agentSessionID: String!, $kind: String!, $url: String, $selector: String,\n $x: Float, $y: Float, $text: String, $key: String,\n $deltaX: Float, $deltaY: Float, $ms: Float\n ) {\n ExecuteRemoteBrowserAction(\n agentSessionID: $agentSessionID, kind: $kind, url: $url, selector: $selector,\n x: $x, y: $y, text: $text, key: $key,\n deltaX: $deltaX, deltaY: $deltaY, ms: $ms\n ) {\n Success\n CurrentUrl\n Detail\n }\n }\n`;\n\n/** The narrow projection of the `ExecuteRemoteBrowserAction` mutation payload the channel reads. */\ninterface ExecuteRemoteBrowserActionResult {\n ExecuteRemoteBrowserAction: {\n Success: boolean;\n CurrentUrl: string | null;\n Detail: string | null;\n } | null;\n}\n\n/**\n * GraphQL mutation that asks the server to start PUSHING live CDP screencast frames for the session.\n * Returns `Streaming: true` when the backend supports `ScreenStreaming` and the stream started; the\n * channel then drives the surface's canvas. `Streaming: false` (capability absent / start failed) leaves\n * the surface on its 700ms snapshot poll fallback.\n */\nconst START_SCREENCAST_MUTATION = `\n mutation StartRemoteBrowserScreencast($agentSessionID: String!) {\n StartRemoteBrowserScreencast(agentSessionID: $agentSessionID) {\n Streaming\n }\n }\n`;\n\n/** GraphQL mutation that stops the server-pushed screencast (best-effort teardown). */\nconst STOP_SCREENCAST_MUTATION = `\n mutation StopRemoteBrowserScreencast($agentSessionID: String!) {\n StopRemoteBrowserScreencast(agentSessionID: $agentSessionID)\n }\n`;\n\n/**\n * GraphQL mutation that asks the server to start PUSHING live tab-audio chunks for the session — so a\n * co-agent demoing a video/audio site is HEARD. Returns `Streaming: true` when the backend can capture\n * audio (v1 gates by backend implementation) and the stream started; `Streaming: false` means no audio\n * (capability absent / start failed) and the client simply plays nothing.\n */\nconst START_AUDIO_STREAM_MUTATION = `\n mutation StartRemoteBrowserAudioStream($agentSessionID: String!) {\n StartRemoteBrowserAudioStream(agentSessionID: $agentSessionID) {\n Streaming\n }\n }\n`;\n\n/** GraphQL mutation that stops the server-pushed tab-audio stream (best-effort teardown). */\nconst STOP_AUDIO_STREAM_MUTATION = `\n mutation StopRemoteBrowserAudioStream($agentSessionID: String!) {\n StopRemoteBrowserAudioStream(agentSessionID: $agentSessionID)\n }\n`;\n\n/** The narrow projection of the `StartRemoteBrowserAudioStream` mutation payload the channel reads. */\ninterface StartRemoteBrowserAudioStreamResult {\n StartRemoteBrowserAudioStream: {\n Streaming: boolean;\n } | null;\n}\n\n/**\n * HUMAN-TAKEOVER mutation — relays ONE pointer/keyboard input the user performed on the live canvas into the\n * server-hosted browser (Collaborative control). Best-effort: the server returns `false` when the backend\n * lacks `HumanTakeover` or no live browser exists; the channel doesn't act on the boolean.\n */\nconst RELAY_HUMAN_INPUT_MUTATION = `\n mutation RelayRemoteBrowserHumanInput(\n $agentSessionID: String!, $kind: String!, $x: Float, $y: Float, $button: String, $key: String,\n $deltaX: Float, $deltaY: Float, $modifiers: String\n ) {\n RelayRemoteBrowserHumanInput(\n agentSessionID: $agentSessionID, kind: $kind, x: $x, y: $y, button: $button, key: $key,\n deltaX: $deltaX, deltaY: $deltaY, modifiers: $modifiers\n )\n }\n`;\n\n/** The narrow projection of the `StartRemoteBrowserScreencast` mutation payload the channel reads. */\ninterface StartRemoteBrowserScreencastResult {\n StartRemoteBrowserScreencast: {\n Streaming: boolean;\n } | null;\n}\n\n/** GraphQL query the surface fetcher runs — the live screenshot + URL of the server browser. */\nconst REMOTE_BROWSER_SNAPSHOT_QUERY = `\n query RemoteBrowserSnapshot($agentSessionID: String!) {\n RemoteBrowserSnapshot(agentSessionID: $agentSessionID) {\n ScreenshotBase64\n CurrentUrl\n }\n }\n`;\n\n/** The narrow projection of the `RemoteBrowserSnapshot` query payload the channel reads. */\ninterface RemoteBrowserSnapshotResult {\n RemoteBrowserSnapshot: RemoteBrowserSnapshotView | null;\n}\n\n/**\n * VISION-QUERY mutation (separate from {@link EXECUTE_REMOTE_BROWSER_ACTION_MUTATION} — this OBSERVES the page,\n * it does not drive it). The server captures the live screenshot and runs a fast vision model over it, so the\n * non-omnimodal voice agent can \"see\" the page: a text description and/or localized elements with pixel\n * centroids. Backs both `browser_DescribePage` (no target) and `browser_LocateElement` (a target description).\n */\nconst INTERPRET_PAGE_MUTATION = `\n mutation InterpretRemoteBrowserPage($agentSessionID: String!, $query: String) {\n InterpretRemoteBrowserPage(agentSessionID: $agentSessionID, query: $query) {\n Description\n Elements {\n Label\n X\n Y\n Confidence\n }\n Detail\n }\n }\n`;\n\n/** One element localized by the visual interpreter — label + pixel centroid + confidence. */\ninterface RemoteBrowserInterpretedElement {\n Label: string;\n X: number;\n Y: number;\n Confidence: number;\n}\n\n/** The narrow projection of the `InterpretRemoteBrowserPage` mutation payload the channel reads. */\ninterface InterpretRemoteBrowserPageResult {\n InterpretRemoteBrowserPage: {\n Description: string | null;\n Elements: RemoteBrowserInterpretedElement[];\n Detail: string | null;\n } | null;\n}\n\n/** The result payload (serialized to JSON) the channel returns to the model per tool call. */\ninterface RemoteBrowserToolResult {\n success: boolean;\n /** The page URL after the action, when the server reported one. */\n currentUrl?: string;\n /** Human-readable detail (e.g. extracted page text for `browser_GetPageText`, or an error). */\n detail?: string;\n /** Error description when `success` is false. */\n error?: string;\n}\n\n/**\n * The REMOTE BROWSER as a pluggable interactive channel — a {@link BaseRealtimeChannelClient}\n * resolved from the `MJ: AI Agent Channels` registry row whose `ClientPluginClass` is\n * `'RealtimeRemoteBrowserChannel'`. One instance per session (ClassFactory-created at session\n * start).\n *\n * Topology (CLIENT-DIRECT, like the whiteboard — the browser just happens to live on the\n * server):\n * - **Action**: the `browser_*` client-executed tool set ({@link REMOTE_BROWSER_TOOL_DEFINITIONS}).\n * {@link ApplyAgentTool} maps a tool call → a normalized {@link RemoteBrowserAction}\n * ({@link MapToolToAction}) and awaits the `ExecuteRemoteBrowserAction` GraphQL mutation\n * (via {@link RealtimeChannelContext.ExecuteServerAction}) to drive the server browser,\n * returning a concise JSON result for the model. Never throws — argument and transport\n * failures map to a `{ success: false, error }` payload.\n * - **Perception**: after a successful action, a `[browser]` context note carries the new\n * page URL so the agent perceives where the page went; the surface independently polls a\n * live screenshot.\n * - **Surface**: {@link RemoteBrowserSurfaceComponent}, created dynamically in the channel\n * tab; the plugin hands it the session's provider + id in {@link BindSurface} so it can\n * poll `RemoteBrowserSnapshot`. HUMAN TAKEOVER is enabled by default (Collaborative): the\n * user can click/type into the live canvas and those events relay to the server browser via\n * {@link RELAY_HUMAN_INPUT_MUTATION} (server-gated on the backend's `HumanTakeover` capability).\n *\n * The channel keeps NO client-side state of record — the browser's state lives entirely on\n * the server — so {@link SerializeState} / {@link RestoreState} use the base no-op behavior.\n */\n@RegisterClass(BaseRealtimeChannelClient, 'RealtimeRemoteBrowserChannel')\nexport class RemoteBrowserChannel extends BaseRealtimeChannelClient<RemoteBrowserSurfaceComponent> {\n /** The live bound surface, when the channel tab's pane is instantiated. */\n private surface: RemoteBrowserSurfaceComponent | null = null;\n\n /** Subscription to the bound surface's `HumanInput` output, torn down on unbind/dispose. */\n private humanInputSub: Subscription | null = null;\n\n /**\n * Whether the server confirmed it is PUSHING screencast frames for this session (`ScreenStreaming`\n * supported). When `true` the surface renders pushed frames on its canvas and skips its poll, and\n * {@link OnScreencastFrame} forwards each pushed frame to it. When `false` the surface keeps polling.\n */\n private streaming = false;\n\n /**\n * Whether the server confirmed it is PUSHING tab-audio chunks for this session (the backend can capture\n * audio). When `true` {@link OnAudioChunk} feeds each pushed chunk to {@link audioPlayer}; when `false`\n * no audio plays.\n */\n private audioStreaming = false;\n\n /** The client-side audio player fed pushed chunks while {@link audioStreaming}; `null` until started. */\n private audioPlayer: RemoteBrowserAudioPlayer | null = null;\n\n /** Subscription to the bound surface's `AudioMutedChange` output (the speaker toggle), torn down on unbind. */\n private audioMutedSub: Subscription | null = null;\n\n public get ChannelName(): string {\n return 'Remote Browser';\n }\n\n public get ToolNamePrefix(): string {\n return REMOTE_BROWSER_TOOL_PREFIX;\n }\n\n public get TabTitle(): string {\n return 'Browser';\n }\n\n public get TabIcon(): string {\n return 'fa-solid fa-globe';\n }\n\n public GetToolDefinitions(): RealtimeToolDefinition[] {\n return REMOTE_BROWSER_TOOL_DEFINITIONS;\n }\n\n public override GetSurfaceComponent(): Type<RemoteBrowserSurfaceComponent> {\n return RemoteBrowserSurfaceComponent;\n }\n\n /** First-run intro shown the first time the user opens the Browser tab (once per user). */\n public override GetOnboardingDetails(): ChannelOnboardingDetails {\n return {\n Heading: 'Remote Browser',\n Description:\n 'The agent drives a live web browser on the server and you can watch it work right here. ' +\n 'You can also grab the wheel: click and type directly on the live page and the agent picks ' +\n 'up right where you left off.',\n Tips: [\n 'Ask the agent to look something up, open a site or fill in a form — it controls the page.',\n \"Click on the live view to take over, then click and type to drive it yourself — the \\\"You're driving\\\" badge shows when takeover is active.\",\n 'The current page URL is shown so you always know where the agent has navigated.',\n ],\n IconClass: 'fa-solid fa-globe',\n };\n }\n\n /**\n * Wires the dynamically-created surface: hands it a snapshot fetcher closing over the\n * channel context (session id + provider live there), so the surface stays transport-\n * agnostic. Set BEFORE the surface's first change detection (the pane binds synchronously),\n * so its `ngOnInit` poll has the fetcher.\n *\n * Also asks the server to start a live screencast. When the backend supports `ScreenStreaming` the\n * server starts PUSHING frames and reports `Streaming: true`; we flip the surface to canvas mode (its\n * poll is then skipped) and {@link OnScreencastFrame} paints each pushed frame. When the backend lacks\n * the capability (`Streaming: false`) the surface keeps the snapshot poll already running — graceful\n * fallback, no further action.\n *\n * HUMAN TAKEOVER is enabled BY DEFAULT (Collaborative): the surface is flipped `Interactive = true` and\n * its `HumanInput` output subscribed, forwarding each pointer/keyboard event to the server via\n * {@link RELAY_HUMAN_INPUT_MUTATION}. Takeover only takes effect on the canvas (screencast) path; the\n * `<img>` poll fallback stays view-only. The server gates the actual routing on the backend's\n * `HumanTakeover` capability, so this is safe to enable unconditionally.\n */\n public BindSurface(instance: RemoteBrowserSurfaceComponent): void {\n this.surface = instance;\n instance.Fetch = () => this.fetchSnapshot();\n instance.Interactive = true;\n this.humanInputSub = instance.HumanInput.subscribe((input) => this.relayHumanInput(input));\n this.audioMutedSub = instance.AudioMutedChange.subscribe((muted) => this.audioPlayer?.SetMuted(muted));\n void this.startScreencast(instance);\n void this.startAudioStream(instance);\n }\n\n public override UnbindSurface(): void {\n this.humanInputSub?.unsubscribe();\n this.humanInputSub = null;\n this.audioMutedSub?.unsubscribe();\n this.audioMutedSub = null;\n void this.stopScreencast();\n void this.stopAudioStream();\n this.surface = null;\n }\n\n public override Dispose(): void {\n this.humanInputSub?.unsubscribe();\n this.humanInputSub = null;\n this.audioMutedSub?.unsubscribe();\n this.audioMutedSub = null;\n void this.stopScreencast();\n void this.stopAudioStream();\n super.Dispose();\n }\n\n /**\n * Forwards one PUSHED screencast frame to the bound surface's canvas. Called by the session service\n * when a `RemoteBrowserScreencastFrame` arrives on the push-status stream for THIS session. No-op when\n * the channel isn't streaming or has no bound surface (e.g. the tab pane is collapsed).\n *\n * @param dataBase64 The frame image as raw base64 JPEG (no `data:` prefix).\n */\n public OnScreencastFrame(dataBase64: string): void {\n if (this.streaming) {\n this.surface?.RenderFrame(dataBase64);\n }\n }\n\n /**\n * Feeds one PUSHED tab-audio chunk to the client-side audio player. Called by the session service when a\n * `RemoteBrowserAudioChunk` arrives on the push-status stream for THIS session. No-op when the channel\n * isn't audio-streaming or the player has been torn down.\n *\n * @param chunk The pushed audio chunk (base64 webm-opus + codec/seq metadata).\n */\n public OnAudioChunk(chunk: RemoteBrowserAudioChunkInput): void {\n if (this.audioStreaming) {\n this.audioPlayer?.Enqueue(chunk);\n }\n }\n\n /**\n * Asks the server to start the live screencast and, on success, flips the surface to canvas mode so it\n * stops polling and starts painting pushed frames. Best-effort: a `null`/`Streaming: false` result\n * (capability absent or transport failure) leaves the surface on its poll fallback.\n */\n private async startScreencast(instance: RemoteBrowserSurfaceComponent): Promise<void> {\n const sessionId = this.Context?.AgentSessionID;\n if (!sessionId) {\n return;\n }\n const data = await this.Context?.ExecuteServerAction<StartRemoteBrowserScreencastResult>(START_SCREENCAST_MUTATION, { agentSessionID: sessionId });\n if (data?.StartRemoteBrowserScreencast?.Streaming === true) {\n this.streaming = true;\n instance.Streaming = true;\n }\n }\n\n /**\n * Asks the server to start the live tab-audio stream and, on success, spins up the client-side\n * {@link RemoteBrowserAudioPlayer} (speaker ON by default — the call is the activating user gesture) so\n * pushed chunks play. Best-effort: a `null`/`Streaming: false` result (no audio capability or transport\n * failure) leaves the channel silent with no player.\n *\n * @param instance The bound surface, flipped to show the speaker toggle when audio starts.\n */\n private async startAudioStream(instance: RemoteBrowserSurfaceComponent): Promise<void> {\n const sessionId = this.Context?.AgentSessionID;\n if (!sessionId) {\n return;\n }\n const data = await this.Context?.ExecuteServerAction<StartRemoteBrowserAudioStreamResult>(START_AUDIO_STREAM_MUTATION, { agentSessionID: sessionId });\n if (data?.StartRemoteBrowserAudioStream?.Streaming === true) {\n this.audioStreaming = true;\n this.audioPlayer = new RemoteBrowserAudioPlayer(new MediaSourceAudioSink());\n // Speaker defaults ON; reflect that on the surface so the toggle renders un-muted.\n instance.AudioAvailable = true;\n instance.AudioMuted = false;\n }\n }\n\n /** Asks the server to stop the tab-audio stream (best-effort), disposes the player, and clears state. */\n private async stopAudioStream(): Promise<void> {\n const wasStreaming = this.audioStreaming;\n this.audioStreaming = false;\n this.audioPlayer?.Dispose();\n this.audioPlayer = null;\n const sessionId = this.Context?.AgentSessionID;\n if (!wasStreaming || !sessionId) {\n return;\n }\n await this.Context?.ExecuteServerAction(STOP_AUDIO_STREAM_MUTATION, { agentSessionID: sessionId });\n }\n\n /**\n * Relays ONE human-takeover input from the surface to the server's {@link RELAY_HUMAN_INPUT_MUTATION}.\n * Best-effort and fire-and-forget: never throws, and the boolean result is ignored (the server already\n * gates routing on the backend's `HumanTakeover` capability). No-op when no live session id exists.\n *\n * @param input The flattened pointer/keyboard input the user performed on the live canvas.\n */\n private relayHumanInput(input: RemoteBrowserHumanInputEvent): void {\n const sessionId = this.Context?.AgentSessionID;\n if (!sessionId) {\n return;\n }\n void this.Context?.ExecuteServerAction(RELAY_HUMAN_INPUT_MUTATION, {\n agentSessionID: sessionId,\n kind: input.kind,\n x: input.x ?? null,\n y: input.y ?? null,\n button: input.button ?? null,\n key: input.key ?? null,\n deltaX: input.deltaX ?? null,\n deltaY: input.deltaY ?? null,\n // Server expects a comma-separated modifier list (e.g. \"Shift,Control\"); null when none held.\n modifiers: input.modifiers && input.modifiers.length > 0 ? input.modifiers.join(',') : null,\n });\n }\n\n /** Asks the server to stop the screencast (best-effort) and clears the streaming flag. */\n private async stopScreencast(): Promise<void> {\n const wasStreaming = this.streaming;\n this.streaming = false;\n const sessionId = this.Context?.AgentSessionID;\n if (!wasStreaming || !sessionId) {\n return;\n }\n await this.Context?.ExecuteServerAction(STOP_SCREENCAST_MUTATION, { agentSessionID: sessionId });\n }\n\n /**\n * Fetches one live snapshot of the server browser for the surface — the PERCEPTION poll.\n * Best-effort by contract ({@link RemoteBrowserSnapshotFetcher}): returns `null` when no\n * session is live or the query fails (the surface keeps its last good frame).\n */\n private async fetchSnapshot(): Promise<RemoteBrowserSnapshotView | null> {\n const sessionId = this.Context?.AgentSessionID;\n if (!sessionId) {\n return null;\n }\n const data = await this.Context?.ExecuteServerAction<RemoteBrowserSnapshotResult>(REMOTE_BROWSER_SNAPSHOT_QUERY, { agentSessionID: sessionId });\n return data?.RemoteBrowserSnapshot ?? null;\n }\n\n /**\n * Executes ONE `browser_*` tool call: maps it to a server action and awaits the\n * `ExecuteRemoteBrowserAction` mutation, then returns a concise JSON result the model reads.\n * On success it also feeds the new page URL to the agent as a `[browser]` context note. Never\n * throws — bad arguments, no live session, and server failures all become a failed-result\n * payload so the model can narrate the problem.\n */\n public async ApplyAgentTool(toolName: string, argsJson: string): Promise<string> {\n // VISION-QUERY branch: the two interpreter tools OBSERVE the page (run a vision model over a screenshot)\n // rather than drive it, so they route to the separate interpret mutation, not MapToolToAction.\n if (toolName === REMOTE_BROWSER_TOOL_NAMES.DescribePage) {\n return this.interpretPage(undefined);\n }\n if (toolName === REMOTE_BROWSER_TOOL_NAMES.LocateElement) {\n const description = this.asArgString(this.parseArgs(argsJson)['description']);\n if (!description) {\n return this.fail('browser_LocateElement requires a \"description\" of the element to find.');\n }\n return this.interpretPage(description);\n }\n\n let action: RemoteBrowserAction;\n try {\n action = MapToolToAction(toolName, this.parseArgs(argsJson));\n } catch (error) {\n const message = error instanceof RemoteBrowserToolArgError ? error.message : 'Invalid Remote Browser tool arguments.';\n return this.fail(message);\n }\n\n const sessionId = this.Context?.AgentSessionID;\n if (!sessionId) {\n // Diagnostic: distinguishes \"channel never Initialized (no Context)\" from \"session id not yet\n // set on the service\" — the two distinct causes of a null id at tool-execution time.\n console.warn('[RemoteBrowser] browser tool invoked but AgentSessionID is null', {\n tool: toolName,\n hasContext: !!this.Context,\n agentSessionID: this.Context?.AgentSessionID ?? null,\n });\n return this.fail('No live browser session is available yet — the realtime session may still be connecting; try again in a moment.');\n }\n\n const data = await this.Context?.ExecuteServerAction<ExecuteRemoteBrowserActionResult>(\n EXECUTE_REMOTE_BROWSER_ACTION_MUTATION,\n this.buildVariables(sessionId, action),\n );\n const result = data?.ExecuteRemoteBrowserAction ?? null;\n if (!result) {\n return this.fail('The browser action could not be executed (no response from the server).');\n }\n if (!result.Success) {\n return this.fail(result.Detail ?? 'The browser action failed.');\n }\n\n if (result.CurrentUrl) {\n // PERCEPTION: tell the agent where the page is now (background — no spoken reply).\n this.Context?.SendContextNote(`[browser] current page: ${result.CurrentUrl}`);\n // In streaming mode the surface's snapshot poll (which carries the URL) is stopped, so push the new\n // URL to the live-view bar directly — otherwise it stays stuck on \"No page loaded yet\".\n this.surface?.SetCurrentUrl(result.CurrentUrl);\n }\n return this.ok(result.CurrentUrl, result.Detail);\n }\n\n /** Builds the flat mutation variables from a normalized action (omitted fields ride as null). */\n private buildVariables(sessionId: string, action: RemoteBrowserAction): Record<string, JSONValue> {\n return {\n agentSessionID: sessionId,\n kind: action.Kind,\n url: action.Url ?? null,\n selector: action.Selector ?? null,\n x: action.X ?? null,\n y: action.Y ?? null,\n text: action.Text ?? null,\n key: action.Key ?? null,\n deltaX: action.DeltaX ?? null,\n deltaY: action.DeltaY ?? null,\n ms: action.Ms ?? null,\n };\n }\n\n /**\n * Runs the VISION-QUERY path for the `browser_DescribePage` / `browser_LocateElement` tools: awaits the\n * `InterpretRemoteBrowserPage` mutation (server captures a screenshot + runs a fast vision model) and returns\n * a concise JSON string the model reads. For a describe (no `query`) it returns `{ description }`; for a locate\n * it returns `{ found, element, all }` so the agent can `browser_Click` the centroid. Never throws — a null\n * session, no response, or a server detail-only result all map to a result string the model can narrate.\n *\n * @param query The visual target to locate, or `undefined` for a plain page description.\n * @returns A JSON string describing the interpretation for the model.\n */\n private async interpretPage(query: string | undefined): Promise<string> {\n const sessionId = this.Context?.AgentSessionID;\n if (!sessionId) {\n console.warn('[RemoteBrowser] interpret tool invoked but AgentSessionID is null', {\n hasContext: !!this.Context,\n agentSessionID: this.Context?.AgentSessionID ?? null,\n });\n return this.fail('No live browser session is available yet — the realtime session may still be connecting; try again in a moment.');\n }\n\n const data = await this.Context?.ExecuteServerAction<InterpretRemoteBrowserPageResult>(INTERPRET_PAGE_MUTATION, {\n agentSessionID: sessionId,\n query: query ?? null,\n });\n const result = data?.InterpretRemoteBrowserPage ?? null;\n if (!result) {\n return this.fail('The page could not be interpreted (no response from the server).');\n }\n\n if (query === undefined) {\n // DescribePage — hand back the description (or the server's detail note when nothing was interpreted).\n return JSON.stringify({ description: result.Description ?? result.Detail ?? null });\n }\n\n // LocateElement — surface whether anything matched, the best match, and all candidates.\n const elements = result.Elements ?? [];\n const best = elements[0];\n return JSON.stringify({\n found: elements.length > 0,\n element: best ? { label: best.Label, x: best.X, y: best.Y } : null,\n all: elements,\n ...(result.Detail ? { detail: result.Detail } : {}),\n });\n }\n\n /** Coerces a parsed tool-arg to a non-empty string, or `undefined` when absent / wrong-typed. */\n private asArgString(value: JSONValue | undefined): string | undefined {\n return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;\n }\n\n /** Parses the tool-args JSON into a plain object; returns `{}` for empty/malformed input. */\n private parseArgs(argsJson: string): Record<string, JSONValue> {\n const trimmed = argsJson?.trim();\n if (!trimmed) {\n return {};\n }\n try {\n const parsed: unknown = JSON.parse(trimmed);\n return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed) ? (parsed as Record<string, JSONValue>) : {};\n } catch {\n return {};\n }\n }\n\n /** Builds a successful tool-result JSON string for the model. */\n private ok(currentUrl: string | null, detail: string | null): string {\n const result: RemoteBrowserToolResult = { success: true };\n if (currentUrl) {\n result.currentUrl = currentUrl;\n }\n if (detail) {\n result.detail = detail;\n }\n return JSON.stringify(result);\n }\n\n /** Builds a failed tool-result JSON string for the model. */\n private fail(error: string): string {\n const result: RemoteBrowserToolResult = { success: false, error };\n return JSON.stringify(result);\n }\n}\n\n/**\n * Tree-shaking prevention: the Remote Browser channel is resolved dynamically through the\n * ClassFactory (by the registry row's `ClientPluginClass` key), so this static call is what\n * keeps its `@RegisterClass` side effect from being eliminated by the bundler. Called from\n * `conversations.module.ts` alongside {@link LoadRealtimeWhiteboardChannel}.\n */\nexport function LoadRealtimeRemoteBrowserChannel(): void {\n // intentional no-op — the import side effect performs the registration\n}\n"]}
1
+ {"version":3,"file":"remote-browser-channel.js","sourceRoot":"","sources":["../../../../../src/lib/components/realtime/remote-browser/remote-browser-channel.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Type } from '@angular/core';\nimport type { Subscription } from 'rxjs';\nimport { RegisterClass } from '@memberjunction/global';\nimport { JSONValue, RealtimeToolDefinition } from '@memberjunction/ai';\nimport { BaseRealtimeChannelClient, ChannelOnboardingDetails } from '../channels/base-realtime-channel-client';\nimport { RemoteBrowserHumanInputEvent, RemoteBrowserSnapshotView, RemoteBrowserSurfaceComponent } from './remote-browser-surface.component';\nimport {\n MapToolToAction,\n REMOTE_BROWSER_TOOL_DEFINITIONS,\n REMOTE_BROWSER_TOOL_NAMES,\n REMOTE_BROWSER_TOOL_PREFIX,\n RemoteBrowserAction,\n RemoteBrowserToolArgError,\n} from './remote-browser-tools';\nimport { MediaSourceAudioSink, RemoteBrowserAudioChunkInput, RemoteBrowserAudioPlayer } from './remote-browser-audio-player';\n\n/**\n * GraphQL mutation that drives the SERVER-hosted browser. The channel awaits it for every\n * `browser_*` tool call and feeds the result back to the model.\n */\nconst EXECUTE_REMOTE_BROWSER_ACTION_MUTATION = `\n mutation ExecuteRemoteBrowserAction(\n $agentSessionID: String!, $kind: String!, $url: String, $selector: String,\n $x: Float, $y: Float, $text: String, $key: String,\n $deltaX: Float, $deltaY: Float, $ms: Float\n ) {\n ExecuteRemoteBrowserAction(\n agentSessionID: $agentSessionID, kind: $kind, url: $url, selector: $selector,\n x: $x, y: $y, text: $text, key: $key,\n deltaX: $deltaX, deltaY: $deltaY, ms: $ms\n ) {\n Success\n CurrentUrl\n Detail\n }\n }\n`;\n\n/** The narrow projection of the `ExecuteRemoteBrowserAction` mutation payload the channel reads. */\ninterface ExecuteRemoteBrowserActionResult {\n ExecuteRemoteBrowserAction: {\n Success: boolean;\n CurrentUrl: string | null;\n Detail: string | null;\n } | null;\n}\n\n/**\n * GOAL mutation — hands a high-level goal to the server's autonomous browser agent (computer-use loop or\n * backend native AI) instead of relaying a granular action. The server plans + executes; we await the\n * terminal outcome. (Live per-step narration is a server-bridged concern; over this client-direct mutation\n * we get the final result.)\n */\nconst EXECUTE_REMOTE_BROWSER_GOAL_MUTATION = `\n mutation ExecuteRemoteBrowserGoal($agentSessionID: String!, $goal: String!, $startUrl: String) {\n ExecuteRemoteBrowserGoal(agentSessionID: $agentSessionID, goal: $goal, startUrl: $startUrl) {\n Success\n GoalRunID\n Strategy\n CurrentUrl\n Status\n StepCount\n Detail\n }\n }\n`;\n\n/**\n * POLL query for the async goal run. `ExecuteRemoteBrowserGoal` STARTS the (potentially multi-minute)\n * goal loop and returns a `GoalRunID` with `Status: 'Running'`; the channel polls this until the run\n * reports a terminal status — so no single request is held open for the whole loop (which would die at\n * a browser/proxy/janitor transport timeout and lose the result).\n */\nconst GET_REMOTE_BROWSER_GOAL_RESULT_QUERY = `\n query GetRemoteBrowserGoalResult($agentSessionID: String!, $goalRunID: String!) {\n GetRemoteBrowserGoalResult(agentSessionID: $agentSessionID, goalRunID: $goalRunID) {\n Success\n GoalRunID\n Strategy\n CurrentUrl\n Status\n StepCount\n Detail\n }\n }\n`;\n\n/** The narrow projection of a goal-result payload (shared by the start mutation + the poll query). */\ninterface RemoteBrowserGoalPayload {\n Success: boolean;\n GoalRunID: string | null;\n Strategy: string | null;\n CurrentUrl: string | null;\n Status: string | null;\n StepCount: number | null;\n Detail: string | null;\n}\n\n/** The narrow projection of the `ExecuteRemoteBrowserGoal` (start) mutation payload the channel reads. */\ninterface ExecuteRemoteBrowserGoalResult {\n ExecuteRemoteBrowserGoal: RemoteBrowserGoalPayload | null;\n}\n\n/** The narrow projection of the `GetRemoteBrowserGoalResult` (poll) query payload the channel reads. */\ninterface GetRemoteBrowserGoalResultPayload {\n GetRemoteBrowserGoalResult: RemoteBrowserGoalPayload | null;\n}\n\n/**\n * GraphQL mutation that asks the server to start PUSHING live CDP screencast frames for the session.\n * Returns `Streaming: true` when the backend supports `ScreenStreaming` and the stream started; the\n * channel then drives the surface's canvas. `Streaming: false` (capability absent / start failed) leaves\n * the surface on its 700ms snapshot poll fallback.\n */\nconst START_SCREENCAST_MUTATION = `\n mutation StartRemoteBrowserScreencast($agentSessionID: String!) {\n StartRemoteBrowserScreencast(agentSessionID: $agentSessionID) {\n Streaming\n }\n }\n`;\n\n/** GraphQL mutation that stops the server-pushed screencast (best-effort teardown). */\nconst STOP_SCREENCAST_MUTATION = `\n mutation StopRemoteBrowserScreencast($agentSessionID: String!) {\n StopRemoteBrowserScreencast(agentSessionID: $agentSessionID)\n }\n`;\n\n/**\n * GraphQL mutation that asks the server to start PUSHING live tab-audio chunks for the session — so a\n * co-agent demoing a video/audio site is HEARD. Returns `Streaming: true` when the backend can capture\n * audio (v1 gates by backend implementation) and the stream started; `Streaming: false` means no audio\n * (capability absent / start failed) and the client simply plays nothing.\n */\nconst START_AUDIO_STREAM_MUTATION = `\n mutation StartRemoteBrowserAudioStream($agentSessionID: String!) {\n StartRemoteBrowserAudioStream(agentSessionID: $agentSessionID) {\n Streaming\n }\n }\n`;\n\n/** GraphQL mutation that stops the server-pushed tab-audio stream (best-effort teardown). */\nconst STOP_AUDIO_STREAM_MUTATION = `\n mutation StopRemoteBrowserAudioStream($agentSessionID: String!) {\n StopRemoteBrowserAudioStream(agentSessionID: $agentSessionID)\n }\n`;\n\n/** The narrow projection of the `StartRemoteBrowserAudioStream` mutation payload the channel reads. */\ninterface StartRemoteBrowserAudioStreamResult {\n StartRemoteBrowserAudioStream: {\n Streaming: boolean;\n } | null;\n}\n\n/**\n * HUMAN-TAKEOVER mutation — relays ONE pointer/keyboard input the user performed on the live canvas into the\n * server-hosted browser (Collaborative control). Best-effort: the server returns `false` when the backend\n * lacks `HumanTakeover` or no live browser exists; the channel doesn't act on the boolean.\n */\nconst RELAY_HUMAN_INPUT_MUTATION = `\n mutation RelayRemoteBrowserHumanInput(\n $agentSessionID: String!, $kind: String!, $x: Float, $y: Float, $button: String, $key: String,\n $text: String, $deltaX: Float, $deltaY: Float, $modifiers: String\n ) {\n RelayRemoteBrowserHumanInput(\n agentSessionID: $agentSessionID, kind: $kind, x: $x, y: $y, button: $button, key: $key,\n text: $text, deltaX: $deltaX, deltaY: $deltaY, modifiers: $modifiers\n )\n }\n`;\n\n/**\n * COPY-OUT query — reads the remote page's current text selection so the surface can write it to the LOCAL\n * clipboard on a `copy` / Cmd+C (the mirror of the `'text'` paste-in path). Returns `''` when nothing is\n * selected, no live browser exists, or the backend lacks `HumanTakeover`.\n */\nconst GET_SELECTION_QUERY = `\n query GetRemoteBrowserSelection($agentSessionID: String!) {\n GetRemoteBrowserSelection(agentSessionID: $agentSessionID) {\n Text\n }\n }\n`;\n\n/** The narrow projection of the `GetRemoteBrowserSelection` query payload the channel reads. */\ninterface GetRemoteBrowserSelectionResult {\n GetRemoteBrowserSelection: {\n Text: string;\n } | null;\n}\n\n/** The narrow projection of the `StartRemoteBrowserScreencast` mutation payload the channel reads. */\ninterface StartRemoteBrowserScreencastResult {\n StartRemoteBrowserScreencast: {\n Streaming: boolean;\n } | null;\n}\n\n/** GraphQL query the surface fetcher runs — the live screenshot + URL of the server browser. */\nconst REMOTE_BROWSER_SNAPSHOT_QUERY = `\n query RemoteBrowserSnapshot($agentSessionID: String!) {\n RemoteBrowserSnapshot(agentSessionID: $agentSessionID) {\n ScreenshotBase64\n CurrentUrl\n }\n }\n`;\n\n/** The narrow projection of the `RemoteBrowserSnapshot` query payload the channel reads. */\ninterface RemoteBrowserSnapshotResult {\n RemoteBrowserSnapshot: RemoteBrowserSnapshotView | null;\n}\n\n/**\n * VISION-QUERY mutation (separate from {@link EXECUTE_REMOTE_BROWSER_ACTION_MUTATION} — this OBSERVES the page,\n * it does not drive it). The server captures the live screenshot and runs a fast vision model over it, so the\n * non-omnimodal voice agent can \"see\" the page: a text description and/or localized elements with pixel\n * centroids. Backs both `browser_DescribePage` (no target) and `browser_LocateElement` (a target description).\n */\nconst INTERPRET_PAGE_MUTATION = `\n mutation InterpretRemoteBrowserPage($agentSessionID: String!, $query: String) {\n InterpretRemoteBrowserPage(agentSessionID: $agentSessionID, query: $query) {\n Description\n Elements {\n Label\n X\n Y\n Confidence\n }\n Detail\n }\n }\n`;\n\n/** One element localized by the visual interpreter — label + pixel centroid + confidence. */\ninterface RemoteBrowserInterpretedElement {\n Label: string;\n X: number;\n Y: number;\n Confidence: number;\n}\n\n/** The narrow projection of the `InterpretRemoteBrowserPage` mutation payload the channel reads. */\ninterface InterpretRemoteBrowserPageResult {\n InterpretRemoteBrowserPage: {\n Description: string | null;\n Elements: RemoteBrowserInterpretedElement[];\n Detail: string | null;\n } | null;\n}\n\n/** The result payload (serialized to JSON) the channel returns to the model per tool call. */\ninterface RemoteBrowserToolResult {\n success: boolean;\n /** The page URL after the action, when the server reported one. */\n currentUrl?: string;\n /** Human-readable detail (e.g. extracted page text for `browser_GetPageText`, or an error). */\n detail?: string;\n /** Error description when `success` is false. */\n error?: string;\n}\n\n/**\n * The REMOTE BROWSER as a pluggable interactive channel — a {@link BaseRealtimeChannelClient}\n * resolved from the `MJ: AI Agent Channels` registry row whose `ClientPluginClass` is\n * `'RealtimeRemoteBrowserChannel'`. One instance per session (ClassFactory-created at session\n * start).\n *\n * Topology (CLIENT-DIRECT, like the whiteboard — the browser just happens to live on the\n * server):\n * - **Action**: the `browser_*` client-executed tool set ({@link REMOTE_BROWSER_TOOL_DEFINITIONS}).\n * {@link ApplyAgentTool} maps a tool call → a normalized {@link RemoteBrowserAction}\n * ({@link MapToolToAction}) and awaits the `ExecuteRemoteBrowserAction` GraphQL mutation\n * (via {@link RealtimeChannelContext.ExecuteServerAction}) to drive the server browser,\n * returning a concise JSON result for the model. Never throws — argument and transport\n * failures map to a `{ success: false, error }` payload.\n * - **Perception**: after a successful action, a `[browser]` context note carries the new\n * page URL so the agent perceives where the page went; the surface independently polls a\n * live screenshot.\n * - **Surface**: {@link RemoteBrowserSurfaceComponent}, created dynamically in the channel\n * tab; the plugin hands it the session's provider + id in {@link BindSurface} so it can\n * poll `RemoteBrowserSnapshot`. HUMAN TAKEOVER is enabled by default (Collaborative): the\n * user can click/type into the live canvas and those events relay to the server browser via\n * {@link RELAY_HUMAN_INPUT_MUTATION} (server-gated on the backend's `HumanTakeover` capability).\n *\n * The channel keeps NO client-side state of record — the browser's state lives entirely on\n * the server — so {@link SerializeState} / {@link RestoreState} use the base no-op behavior.\n */\n@RegisterClass(BaseRealtimeChannelClient, 'RealtimeRemoteBrowserChannel')\nexport class RemoteBrowserChannel extends BaseRealtimeChannelClient<RemoteBrowserSurfaceComponent> {\n /** The live bound surface, when the channel tab's pane is instantiated. */\n private surface: RemoteBrowserSurfaceComponent | null = null;\n\n /** Subscription to the bound surface's `HumanInput` output, torn down on unbind/dispose. */\n private humanInputSub: Subscription | null = null;\n\n /**\n * Whether the server confirmed it is PUSHING screencast frames for this session (`ScreenStreaming`\n * supported). When `true` the surface renders pushed frames on its canvas and skips its poll, and\n * {@link OnScreencastFrame} forwards each pushed frame to it. When `false` the surface keeps polling.\n */\n private streaming = false;\n\n /**\n * Whether the server confirmed it is PUSHING tab-audio chunks for this session (the backend can capture\n * audio). When `true` {@link OnAudioChunk} feeds each pushed chunk to {@link audioPlayer}; when `false`\n * no audio plays.\n */\n private audioStreaming = false;\n\n /** The client-side audio player fed pushed chunks while {@link audioStreaming}; `null` until started. */\n private audioPlayer: RemoteBrowserAudioPlayer | null = null;\n\n /** Subscription to the bound surface's `AudioMutedChange` output (the speaker toggle), torn down on unbind. */\n private audioMutedSub: Subscription | null = null;\n\n /**\n * Poll cadence + deadline for {@link pollGoalResult} (the async `browser_AchieveGoal` result poll).\n * Protected so tests can shrink them; production polls every 2.5s for up to 5 minutes (a goal loop\n * past 5 min keeps running server-side, the model just gets a \"still running\" note).\n */\n protected GoalPollIntervalMs = 2500;\n protected GoalPollTimeoutMs = 5 * 60 * 1000;\n\n public get ChannelName(): string {\n return 'Remote Browser';\n }\n\n public get ToolNamePrefix(): string {\n return REMOTE_BROWSER_TOOL_PREFIX;\n }\n\n public get TabTitle(): string {\n return 'Browser';\n }\n\n public get TabIcon(): string {\n return 'fa-solid fa-globe';\n }\n\n public GetToolDefinitions(): RealtimeToolDefinition[] {\n return REMOTE_BROWSER_TOOL_DEFINITIONS;\n }\n\n public override GetSurfaceComponent(): Type<RemoteBrowserSurfaceComponent> {\n return RemoteBrowserSurfaceComponent;\n }\n\n /** First-run intro shown the first time the user opens the Browser tab (once per user). */\n public override GetOnboardingDetails(): ChannelOnboardingDetails {\n return {\n Heading: 'Remote Browser',\n Description:\n 'The agent drives a live web browser on the server and you can watch it work right here. ' +\n 'You can also grab the wheel: click and type directly on the live page and the agent picks ' +\n 'up right where you left off.',\n Tips: [\n 'Ask the agent to look something up, open a site or fill in a form — it controls the page.',\n 'Click on the live view to take over, then click and type to drive it yourself — the \"You\\'re driving\" badge shows when takeover is active.',\n 'The current page URL is shown so you always know where the agent has navigated.',\n ],\n IconClass: 'fa-solid fa-globe',\n };\n }\n\n /**\n * Wires the dynamically-created surface: hands it a snapshot fetcher closing over the\n * channel context (session id + provider live there), so the surface stays transport-\n * agnostic. Set BEFORE the surface's first change detection (the pane binds synchronously),\n * so its `ngOnInit` poll has the fetcher.\n *\n * Also asks the server to start a live screencast. When the backend supports `ScreenStreaming` the\n * server starts PUSHING frames and reports `Streaming: true`; we flip the surface to canvas mode (its\n * poll is then skipped) and {@link OnScreencastFrame} paints each pushed frame. When the backend lacks\n * the capability (`Streaming: false`) the surface keeps the snapshot poll already running — graceful\n * fallback, no further action.\n *\n * HUMAN TAKEOVER is enabled BY DEFAULT (Collaborative): the surface is flipped `Interactive = true` and\n * its `HumanInput` output subscribed, forwarding each pointer/keyboard event to the server via\n * {@link RELAY_HUMAN_INPUT_MUTATION}. Takeover only takes effect on the canvas (screencast) path; the\n * `<img>` poll fallback stays view-only. The server gates the actual routing on the backend's\n * `HumanTakeover` capability, so this is safe to enable unconditionally.\n */\n public BindSurface(instance: RemoteBrowserSurfaceComponent): void {\n this.surface = instance;\n instance.Fetch = () => this.fetchSnapshot();\n // Copy-out: the surface reads the remote selection through this on a local `copy` and writes it locally.\n instance.FetchSelection = () => this.fetchSelection();\n instance.Interactive = true;\n this.humanInputSub = instance.HumanInput.subscribe((input) => this.relayHumanInput(input));\n this.audioMutedSub = instance.AudioMutedChange.subscribe((muted) => this.audioPlayer?.SetMuted(muted));\n void this.startScreencast(instance);\n void this.startAudioStream(instance);\n }\n\n public override UnbindSurface(): void {\n this.humanInputSub?.unsubscribe();\n this.humanInputSub = null;\n this.audioMutedSub?.unsubscribe();\n this.audioMutedSub = null;\n void this.stopScreencast();\n void this.stopAudioStream();\n this.surface = null;\n }\n\n public override Dispose(): void {\n this.humanInputSub?.unsubscribe();\n this.humanInputSub = null;\n this.audioMutedSub?.unsubscribe();\n this.audioMutedSub = null;\n void this.stopScreencast();\n void this.stopAudioStream();\n super.Dispose();\n }\n\n /**\n * Forwards one PUSHED screencast frame to the bound surface's canvas. Called by the session service\n * when a `RemoteBrowserScreencastFrame` arrives on the push-status stream for THIS session. No-op when\n * the channel isn't streaming or has no bound surface (e.g. the tab pane is collapsed).\n *\n * @param dataBase64 The frame image as raw base64 JPEG (no `data:` prefix).\n */\n public OnScreencastFrame(dataBase64: string): void {\n if (this.streaming) {\n this.surface?.RenderFrame(dataBase64);\n }\n }\n\n /**\n * Feeds one PUSHED tab-audio chunk to the client-side audio player. Called by the session service when a\n * `RemoteBrowserAudioChunk` arrives on the push-status stream for THIS session. No-op when the channel\n * isn't audio-streaming or the player has been torn down.\n *\n * @param chunk The pushed audio chunk (base64 webm-opus + codec/seq metadata).\n */\n public OnAudioChunk(chunk: RemoteBrowserAudioChunkInput): void {\n if (this.audioStreaming) {\n this.audioPlayer?.Enqueue(chunk);\n }\n }\n\n /**\n * Asks the server to start the live screencast and, on success, flips the surface to canvas mode so it\n * stops polling and starts painting pushed frames. Best-effort: a `null`/`Streaming: false` result\n * (capability absent or transport failure) leaves the surface on its poll fallback.\n */\n private async startScreencast(instance: RemoteBrowserSurfaceComponent): Promise<void> {\n const sessionId = this.Context?.AgentSessionID;\n if (!sessionId) {\n return;\n }\n const data = await this.Context?.ExecuteServerAction<StartRemoteBrowserScreencastResult>(START_SCREENCAST_MUTATION, { agentSessionID: sessionId });\n if (data?.StartRemoteBrowserScreencast?.Streaming === true) {\n this.streaming = true;\n instance.Streaming = true;\n }\n }\n\n /**\n * Asks the server to start the live tab-audio stream and, on success, spins up the client-side\n * {@link RemoteBrowserAudioPlayer} (speaker ON by default — the call is the activating user gesture) so\n * pushed chunks play. Best-effort: a `null`/`Streaming: false` result (no audio capability or transport\n * failure) leaves the channel silent with no player.\n *\n * @param instance The bound surface, flipped to show the speaker toggle when audio starts.\n */\n private async startAudioStream(instance: RemoteBrowserSurfaceComponent): Promise<void> {\n const sessionId = this.Context?.AgentSessionID;\n if (!sessionId) {\n return;\n }\n const data = await this.Context?.ExecuteServerAction<StartRemoteBrowserAudioStreamResult>(START_AUDIO_STREAM_MUTATION, { agentSessionID: sessionId });\n if (data?.StartRemoteBrowserAudioStream?.Streaming === true) {\n this.audioStreaming = true;\n this.audioPlayer = new RemoteBrowserAudioPlayer(new MediaSourceAudioSink());\n // Speaker defaults ON; reflect that on the surface so the toggle renders un-muted.\n instance.AudioAvailable = true;\n instance.AudioMuted = false;\n }\n }\n\n /** Asks the server to stop the tab-audio stream (best-effort), disposes the player, and clears state. */\n private async stopAudioStream(): Promise<void> {\n const wasStreaming = this.audioStreaming;\n this.audioStreaming = false;\n this.audioPlayer?.Dispose();\n this.audioPlayer = null;\n const sessionId = this.Context?.AgentSessionID;\n if (!wasStreaming || !sessionId) {\n return;\n }\n await this.Context?.ExecuteServerAction(STOP_AUDIO_STREAM_MUTATION, { agentSessionID: sessionId });\n }\n\n /**\n * Relays ONE human-takeover input from the surface to the server's {@link RELAY_HUMAN_INPUT_MUTATION}.\n * Best-effort and fire-and-forget: never throws, and the boolean result is ignored (the server already\n * gates routing on the backend's `HumanTakeover` capability). No-op when no live session id exists.\n *\n * @param input The flattened pointer/keyboard input the user performed on the live canvas.\n */\n private relayHumanInput(input: RemoteBrowserHumanInputEvent): void {\n const sessionId = this.Context?.AgentSessionID;\n if (!sessionId) {\n return;\n }\n void this.Context?.ExecuteServerAction(RELAY_HUMAN_INPUT_MUTATION, {\n agentSessionID: sessionId,\n kind: input.kind,\n x: input.x ?? null,\n y: input.y ?? null,\n button: input.button ?? null,\n key: input.key ?? null,\n // Paste-in: the `'text'` kind carries the local clipboard text the server inserts into the page.\n text: input.text ?? null,\n deltaX: input.deltaX ?? null,\n deltaY: input.deltaY ?? null,\n // Server expects a comma-separated modifier list (e.g. \"Shift,Control\"); null when none held.\n modifiers: input.modifiers && input.modifiers.length > 0 ? input.modifiers.join(',') : null,\n });\n }\n\n /**\n * Reads the remote page's current selection for COPY-OUT — backs the surface's `FetchSelection`. Awaits\n * the `GetRemoteBrowserSelection` query and returns its text. Best-effort by contract\n * ({@link RemoteBrowserSelectionFetcher}): returns `''` when no session is live or the query fails, so a\n * best-effort copy never throws.\n */\n private async fetchSelection(): Promise<string> {\n const sessionId = this.Context?.AgentSessionID;\n if (!sessionId) {\n return '';\n }\n const data = await this.Context?.ExecuteServerAction<GetRemoteBrowserSelectionResult>(GET_SELECTION_QUERY, { agentSessionID: sessionId });\n return data?.GetRemoteBrowserSelection?.Text ?? '';\n }\n\n /** Asks the server to stop the screencast (best-effort) and clears the streaming flag. */\n private async stopScreencast(): Promise<void> {\n const wasStreaming = this.streaming;\n this.streaming = false;\n const sessionId = this.Context?.AgentSessionID;\n if (!wasStreaming || !sessionId) {\n return;\n }\n await this.Context?.ExecuteServerAction(STOP_SCREENCAST_MUTATION, { agentSessionID: sessionId });\n }\n\n /**\n * Fetches one live snapshot of the server browser for the surface — the PERCEPTION poll.\n * Best-effort by contract ({@link RemoteBrowserSnapshotFetcher}): returns `null` when no\n * session is live or the query fails (the surface keeps its last good frame).\n */\n private async fetchSnapshot(): Promise<RemoteBrowserSnapshotView | null> {\n const sessionId = this.Context?.AgentSessionID;\n if (!sessionId) {\n return null;\n }\n const data = await this.Context?.ExecuteServerAction<RemoteBrowserSnapshotResult>(REMOTE_BROWSER_SNAPSHOT_QUERY, { agentSessionID: sessionId });\n return data?.RemoteBrowserSnapshot ?? null;\n }\n\n /**\n * Executes ONE `browser_*` tool call: maps it to a server action and awaits the\n * `ExecuteRemoteBrowserAction` mutation, then returns a concise JSON result the model reads.\n * On success it also feeds the new page URL to the agent as a `[browser]` context note. Never\n * throws — bad arguments, no live session, and server failures all become a failed-result\n * payload so the model can narrate the problem.\n */\n public async ApplyAgentTool(toolName: string, argsJson: string): Promise<string> {\n // VISION-QUERY branch: the two interpreter tools OBSERVE the page (run a vision model over a screenshot)\n // rather than drive it, so they route to the separate interpret mutation, not MapToolToAction.\n if (toolName === REMOTE_BROWSER_TOOL_NAMES.DescribePage) {\n return this.interpretPage(undefined);\n }\n if (toolName === REMOTE_BROWSER_TOOL_NAMES.AchieveGoal) {\n return this.achieveGoal(argsJson);\n }\n if (toolName === REMOTE_BROWSER_TOOL_NAMES.LocateElement) {\n const description = this.asArgString(this.parseArgs(argsJson)['description']);\n if (!description) {\n return this.fail('browser_LocateElement requires a \"description\" of the element to find.');\n }\n return this.interpretPage(description);\n }\n\n let action: RemoteBrowserAction;\n try {\n action = MapToolToAction(toolName, this.parseArgs(argsJson));\n } catch (error) {\n const message = error instanceof RemoteBrowserToolArgError ? error.message : 'Invalid Remote Browser tool arguments.';\n return this.fail(message);\n }\n\n // Briefly WAIT for the session id rather than failing instantly: the model can fire a tool the\n // first beat it connects, before mintSession binds the id (the \"session id missing\" race).\n const sessionId = await this.ResolveAgentSessionId();\n if (!sessionId) {\n // Diagnostic: distinguishes \"channel never Initialized (no Context)\" from \"session id never\n // resolved within the wait window\" — the two distinct causes of a null id at tool-execution time.\n console.warn('[RemoteBrowser] browser tool invoked but AgentSessionID is null', {\n tool: toolName,\n hasContext: !!this.Context,\n agentSessionID: this.Context?.AgentSessionID ?? null,\n });\n return this.fail('No live browser session is available yet — the realtime session may still be connecting; try again in a moment.');\n }\n\n const data = await this.Context?.ExecuteServerAction<ExecuteRemoteBrowserActionResult>(\n EXECUTE_REMOTE_BROWSER_ACTION_MUTATION,\n this.buildVariables(sessionId, action),\n );\n const result = data?.ExecuteRemoteBrowserAction ?? null;\n if (!result) {\n return this.fail('The browser action could not be executed (no response from the server).');\n }\n if (!result.Success) {\n return this.fail(result.Detail ?? 'The browser action failed.');\n }\n\n if (result.CurrentUrl) {\n // PERCEPTION: tell the agent where the page is now (background — no spoken reply).\n this.Context?.SendContextNote(`[browser] current page: ${result.CurrentUrl}`);\n // In streaming mode the surface's snapshot poll (which carries the URL) is stopped, so push the new\n // URL to the live-view bar directly — otherwise it stays stuck on \"No page loaded yet\".\n this.surface?.SetCurrentUrl(result.CurrentUrl);\n }\n return this.ok(result.CurrentUrl, result.Detail);\n }\n\n /** Builds the flat mutation variables from a normalized action (omitted fields ride as null). */\n private buildVariables(sessionId: string, action: RemoteBrowserAction): Record<string, JSONValue> {\n return {\n agentSessionID: sessionId,\n kind: action.Kind,\n url: action.Url ?? null,\n selector: action.Selector ?? null,\n x: action.X ?? null,\n y: action.Y ?? null,\n text: action.Text ?? null,\n key: action.Key ?? null,\n deltaX: action.DeltaX ?? null,\n deltaY: action.DeltaY ?? null,\n ms: action.Ms ?? null,\n };\n }\n\n /**\n * GOAL path for the `browser_AchieveGoal` tool: hands a high-level goal to the server's autonomous\n * browser agent (computer-use loop / native AI) via {@link EXECUTE_REMOTE_BROWSER_GOAL_MUTATION} and\n * returns a concise result for the model. Never throws — failures map to a `{ success: false }` string.\n *\n * @param argsJson The raw tool-call arguments (`{ goal, startUrl? }`).\n * @returns The model-readable result string.\n */\n private async achieveGoal(argsJson: string): Promise<string> {\n // Wait briefly for the session id (mint/reconnect race) before giving up — see ResolveAgentSessionId.\n const sessionId = await this.ResolveAgentSessionId();\n if (!sessionId) {\n return this.fail('No live browser session is available yet — the realtime session may still be connecting; try again in a moment.');\n }\n const args = this.parseArgs(argsJson);\n const goal = this.asArgString(args['goal']);\n if (!goal) {\n return this.fail('browser_AchieveGoal requires a \"goal\" describing what to accomplish.');\n }\n const startUrl = this.asArgString(args['startUrl']) ?? null;\n\n // START the goal (returns immediately with a GoalRunID) then POLL — a goal loop can run for\n // minutes, far longer than any single request survives, so we never hold one open for the loop.\n const startData = await this.Context?.ExecuteServerAction<ExecuteRemoteBrowserGoalResult>(EXECUTE_REMOTE_BROWSER_GOAL_MUTATION, {\n agentSessionID: sessionId,\n goal,\n startUrl,\n });\n const started = startData?.ExecuteRemoteBrowserGoal ?? null;\n if (!started) {\n return this.fail('The browser goal could not be started (no response from the server).');\n }\n // Defensive: if the server ever returns a terminal status synchronously, use it directly.\n const result = started.Status === 'Running' && started.GoalRunID\n ? await this.pollGoalResult(sessionId, started.GoalRunID)\n : started;\n if (!result) {\n return this.fail('The browser goal is taking longer than expected and could not be confirmed. It may still be running.');\n }\n if (result.CurrentUrl) {\n this.Context?.SendContextNote(`[browser] current page: ${result.CurrentUrl}`);\n this.surface?.SetCurrentUrl(result.CurrentUrl);\n }\n if (!result.Success) {\n return this.fail(result.Detail ?? `The goal could not be completed (${result.Status ?? 'unknown'}).`);\n }\n return this.ok(result.CurrentUrl, result.Detail ?? `Goal completed (${result.Status ?? 'Completed'}, ${result.StepCount ?? 0} steps).`);\n }\n\n /**\n * Polls {@link GET_REMOTE_BROWSER_GOAL_RESULT_QUERY} for a started goal run until it reports a\n * terminal status (anything other than `Running`), then returns that terminal payload. Each poll is\n * a short request, so no transport boundary times out while a long goal loop runs server-side.\n *\n * Returns `null` if the goal is still `Running` after {@link GoalPollTimeoutMs} (the loop keeps\n * running server-side; the caller maps this to a \"still running\" message), the session went away, or\n * the run id is no longer known (`Unknown` — expired/superseded).\n */\n private async pollGoalResult(sessionId: string, goalRunID: string): Promise<RemoteBrowserGoalPayload | null> {\n const interval = Math.max(1, this.GoalPollIntervalMs);\n for (let waited = 0; waited < this.GoalPollTimeoutMs; waited += interval) {\n await new Promise<void>((resolve) => setTimeout(resolve, interval));\n // The session ended mid-goal — stop polling (the live browser is gone).\n if (!this.Context?.AgentSessionID) {\n return null;\n }\n const data = await this.Context.ExecuteServerAction<GetRemoteBrowserGoalResultPayload>(GET_REMOTE_BROWSER_GOAL_RESULT_QUERY, {\n agentSessionID: sessionId,\n goalRunID,\n });\n const payload = data?.GetRemoteBrowserGoalResult ?? null;\n // A transient null (one failed poll) shouldn't abort — keep polling until the deadline.\n if (payload && payload.Status !== 'Running') {\n return payload;\n }\n }\n return null;\n }\n\n /**\n * Runs the VISION-QUERY path for the `browser_DescribePage` / `browser_LocateElement` tools: awaits the\n * `InterpretRemoteBrowserPage` mutation (server captures a screenshot + runs a fast vision model) and returns\n * a concise JSON string the model reads. For a describe (no `query`) it returns `{ description }`; for a locate\n * it returns `{ found, element, all }` so the agent can `browser_Click` the centroid. Never throws — a null\n * session, no response, or a server detail-only result all map to a result string the model can narrate.\n *\n * @param query The visual target to locate, or `undefined` for a plain page description.\n * @returns A JSON string describing the interpretation for the model.\n */\n private async interpretPage(query: string | undefined): Promise<string> {\n // Wait briefly for the session id (mint/reconnect race) before giving up — see ResolveAgentSessionId.\n const sessionId = await this.ResolveAgentSessionId();\n if (!sessionId) {\n console.warn('[RemoteBrowser] interpret tool invoked but AgentSessionID is null', {\n hasContext: !!this.Context,\n agentSessionID: this.Context?.AgentSessionID ?? null,\n });\n return this.fail('No live browser session is available yet — the realtime session may still be connecting; try again in a moment.');\n }\n\n const data = await this.Context?.ExecuteServerAction<InterpretRemoteBrowserPageResult>(INTERPRET_PAGE_MUTATION, {\n agentSessionID: sessionId,\n query: query ?? null,\n });\n const result = data?.InterpretRemoteBrowserPage ?? null;\n if (!result) {\n return this.fail('The page could not be interpreted (no response from the server).');\n }\n\n if (query === undefined) {\n // DescribePage — hand back the description (or the server's detail note when nothing was interpreted).\n return JSON.stringify({ description: result.Description ?? result.Detail ?? null });\n }\n\n // LocateElement — surface whether anything matched, the best match, and all candidates.\n const elements = result.Elements ?? [];\n const best = elements[0];\n return JSON.stringify({\n found: elements.length > 0,\n element: best ? { label: best.Label, x: best.X, y: best.Y } : null,\n all: elements,\n ...(result.Detail ? { detail: result.Detail } : {}),\n });\n }\n\n /** Coerces a parsed tool-arg to a non-empty string, or `undefined` when absent / wrong-typed. */\n private asArgString(value: JSONValue | undefined): string | undefined {\n return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;\n }\n\n /** Parses the tool-args JSON into a plain object; returns `{}` for empty/malformed input. */\n private parseArgs(argsJson: string): Record<string, JSONValue> {\n const trimmed = argsJson?.trim();\n if (!trimmed) {\n return {};\n }\n try {\n const parsed: unknown = JSON.parse(trimmed);\n return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed) ? (parsed as Record<string, JSONValue>) : {};\n } catch {\n return {};\n }\n }\n\n /** Builds a successful tool-result JSON string for the model. */\n private ok(currentUrl: string | null, detail: string | null): string {\n const result: RemoteBrowserToolResult = { success: true };\n if (currentUrl) {\n result.currentUrl = currentUrl;\n }\n if (detail) {\n result.detail = detail;\n }\n return JSON.stringify(result);\n }\n\n /** Builds a failed tool-result JSON string for the model. */\n private fail(error: string): string {\n const result: RemoteBrowserToolResult = { success: false, error };\n return JSON.stringify(result);\n }\n}\n\n/**\n * Tree-shaking prevention: the Remote Browser channel is resolved dynamically through the\n * ClassFactory (by the registry row's `ClientPluginClass` key), so this static call is what\n * keeps its `@RegisterClass` side effect from being eliminated by the bundler. Called from\n * `conversations.module.ts` alongside {@link LoadRealtimeWhiteboardChannel}.\n */\nexport function LoadRealtimeRemoteBrowserChannel(): void {\n // intentional no-op — the import side effect performs the registration\n}\n"]}
@@ -10,7 +10,7 @@ export type RemoteBrowserModifier = 'Shift' | 'Control' | 'Alt' | 'Meta';
10
10
  */
11
11
  export interface RemoteBrowserHumanInputEvent {
12
12
  /** Which input occurred. */
13
- kind: 'pointer-move' | 'pointer-click' | 'pointer-down' | 'pointer-up' | 'key' | 'scroll';
13
+ kind: 'pointer-move' | 'pointer-click' | 'pointer-down' | 'pointer-up' | 'key' | 'text' | 'scroll';
14
14
  /** Viewport X (pointer + scroll kinds only). */
15
15
  x?: number;
16
16
  /** Viewport Y (pointer + scroll kinds only). */
@@ -19,6 +19,8 @@ export interface RemoteBrowserHumanInputEvent {
19
19
  button?: 'left' | 'middle' | 'right';
20
20
  /** The pressed key (key kind only). */
21
21
  key?: string;
22
+ /** The pasted clipboard text (text kind only) — inserted into the remote page's focused element. */
23
+ text?: string;
22
24
  /** Horizontal wheel delta in pixels (scroll kind only; positive = right). */
23
25
  deltaX?: number;
24
26
  /** Vertical wheel delta in pixels (scroll kind only; positive = down). */
@@ -68,6 +70,13 @@ export interface RemoteBrowserSnapshotView {
68
70
  * `null` on any failure rather than throwing, so the surface keeps the last good frame.
69
71
  */
70
72
  export type RemoteBrowserSnapshotFetcher = () => Promise<RemoteBrowserSnapshotView | null>;
73
+ /**
74
+ * Reads the remote page's CURRENT text selection for copy-out (the mirror of the `'text'` paste-in path).
75
+ * Supplied by the channel plugin (it owns the session id + GraphQL provider). Best-effort by contract:
76
+ * resolves to `''` on any failure / when nothing is selected rather than throwing, so a best-effort copy
77
+ * never breaks the live view.
78
+ */
79
+ export type RemoteBrowserSelectionFetcher = () => Promise<string>;
71
80
  /**
72
81
  * LIVE REMOTE-BROWSER surface (`mj-realtime-remote-browser-surface`) — the Browser channel
73
82
  * tab's pane. It renders the SERVER-hosted browser the agent drives: a refreshing screenshot
@@ -91,6 +100,13 @@ export type RemoteBrowserSnapshotFetcher = () => Promise<RemoteBrowserSnapshotVi
91
100
  export declare class RemoteBrowserSurfaceComponent implements OnInit, OnDestroy {
92
101
  /** Snapshot fetcher supplied by the channel plugin (closes over the session id + provider). */
93
102
  Fetch: RemoteBrowserSnapshotFetcher | null;
103
+ /**
104
+ * Selection fetcher for copy-out, supplied by the channel plugin (closes over the session id + provider).
105
+ * On a local `copy` / Cmd+C over the live canvas the surface calls this to read the remote page's
106
+ * selection, then writes it to the local clipboard. Best-effort: when unset or it resolves to `''`, the
107
+ * copy is a no-op (the local clipboard is left untouched).
108
+ */
109
+ FetchSelection: RemoteBrowserSelectionFetcher | null;
94
110
  /**
95
111
  * Whether the server is PUSHING live screencast frames for this session (the backend advertised the
96
112
  * `ScreenStreaming` capability and the start succeeded). When `true` the surface paints pushed frames
@@ -203,6 +219,8 @@ export declare class RemoteBrowserSurfaceComponent implements OnInit, OnDestroy
203
219
  private readonly onCanvasMouseUp;
204
220
  private readonly onCanvasClick;
205
221
  private readonly onCanvasKeyDown;
222
+ private readonly onCanvasPaste;
223
+ private readonly onCanvasCopy;
206
224
  private readonly onCanvasMouseLeave;
207
225
  private readonly onCanvasWheel;
208
226
  private readonly onCanvasFocus;
@@ -296,6 +314,21 @@ export declare class RemoteBrowserSurfaceComponent implements OnInit, OnDestroy
296
314
  * next key/click.
297
315
  */
298
316
  private handleKeyDown;
317
+ /**
318
+ * Paste-in: a local `paste` (Cmd/Ctrl+V or context-menu paste) over the canvas → reads the LOCAL clipboard
319
+ * text and relays it as a `'text'` human input, which the server inserts into the remote page's focused
320
+ * element (the VNC/remote-desktop paste model — no remote-clipboard sync). `preventDefault` keeps the host
321
+ * app from also acting on the paste; empty/non-text clipboards are a no-op.
322
+ */
323
+ private handlePaste;
324
+ /**
325
+ * Copy-out: a local `copy` (Cmd/Ctrl+C or context-menu copy) over the canvas → reads the REMOTE page's
326
+ * current selection via {@link FetchSelection} and writes it to the LOCAL clipboard (the mirror of paste-in).
327
+ * `preventDefault` stops the host app from copying its own empty selection. Best-effort throughout: when the
328
+ * fetcher is unwired, resolves to `''`, or `navigator.clipboard` is unavailable / denies permission, the copy
329
+ * is silently a no-op — it never throws and never breaks the live view.
330
+ */
331
+ private handleCopy;
299
332
  /**
300
333
  * Collects the modifier keys currently held during a DOM mouse/keyboard event into the relayed
301
334
  * {@link RemoteBrowserModifier} list (empty when none are held).
@@ -341,6 +374,6 @@ export declare class RemoteBrowserSurfaceComponent implements OnInit, OnDestroy
341
374
  /** Maps a DOM `MouseEvent.button` (0/1/2) to the relayed button union, defaulting to `'left'`. */
342
375
  private mapButton;
343
376
  static ɵfac: i0.ɵɵFactoryDeclaration<RemoteBrowserSurfaceComponent, never>;
344
- static ɵcmp: i0.ɵɵComponentDeclaration<RemoteBrowserSurfaceComponent, "mj-realtime-remote-browser-surface", never, { "Fetch": { "alias": "Fetch"; "required": false; }; "Streaming": { "alias": "Streaming"; "required": false; }; "Interactive": { "alias": "Interactive"; "required": false; }; "AudioAvailable": { "alias": "AudioAvailable"; "required": false; }; "AudioMuted": { "alias": "AudioMuted"; "required": false; }; }, { "HumanInput": "HumanInput"; "AudioMutedChange": "AudioMutedChange"; }, never, never, true, never>;
377
+ static ɵcmp: i0.ɵɵComponentDeclaration<RemoteBrowserSurfaceComponent, "mj-realtime-remote-browser-surface", never, { "Fetch": { "alias": "Fetch"; "required": false; }; "FetchSelection": { "alias": "FetchSelection"; "required": false; }; "Streaming": { "alias": "Streaming"; "required": false; }; "Interactive": { "alias": "Interactive"; "required": false; }; "AudioAvailable": { "alias": "AudioAvailable"; "required": false; }; "AudioMuted": { "alias": "AudioMuted"; "required": false; }; }, { "HumanInput": "HumanInput"; "AudioMutedChange": "AudioMutedChange"; }, never, never, true, never>;
345
378
  }
346
379
  //# sourceMappingURL=remote-browser-surface.component.d.ts.map