dsh-builtin-browser 0.4.1 → 0.4.5
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.
- package/CHANGELOG.md +1699 -0
- package/README.en.md +114 -33
- package/README.md +108 -32
- package/client.js +478 -29
- package/cordis.patch.yml +21 -0
- package/desktop-bridge/install.mjs +66 -6
- package/desktop-bridge/plugin-browser-bridge.js +426 -167
- package/docs/architecture.md +14 -4
- package/docs/tool-reference.md +13 -13
- package/docs/user-guide.md +58 -3
- package/docs/why-browser.md +1 -1
- package/lib/browser/types.d.ts +34 -3
- package/lib/browser-electron/bridge-connection.d.ts +49 -26
- package/lib/browser-electron/bridge-connection.js +185 -22
- package/lib/browser-electron/desktop-bridge-host.d.ts +69 -36
- package/lib/browser-electron/desktop-bridge-host.js +151 -86
- package/lib/browser-electron/entry.d.ts +1 -1
- package/lib/browser-electron/entry.js +133 -40
- package/lib/browser-electron/ephemeral-profile.d.ts +37 -0
- package/lib/browser-electron/ephemeral-profile.js +146 -0
- package/lib/browser-electron/history-store.d.ts +36 -2
- package/lib/browser-electron/history-store.js +76 -7
- package/lib/browser-electron/host-main.js +86 -28
- package/lib/browser-electron/provider.d.ts +106 -3
- package/lib/browser-electron/provider.js +407 -60
- package/lib/browser-electron/remote-host.d.ts +52 -3
- package/lib/browser-electron/remote-host.js +259 -25
- package/lib/browser-electron/rpc-ops.d.ts +18 -0
- package/lib/browser-electron/rpc-ops.js +36 -0
- package/lib/browser-electron/settings-route.d.ts +28 -0
- package/lib/browser-electron/settings-route.js +68 -0
- package/lib/browser-electron/settings-store.d.ts +40 -0
- package/lib/browser-electron/settings-store.js +160 -23
- package/lib/browser-electron/system-browser.d.ts +102 -12
- package/lib/browser-electron/system-browser.js +411 -51
- package/lib/index.js +0 -1
- package/lib/tool-browser/element-fields.d.ts +145 -0
- package/lib/tool-browser/element-fields.js +152 -0
- package/lib/tool-browser/index.js +149 -74
- package/package.json +10 -4
- package/scripts/verify-host-compat.mjs +195 -0
- package/src/browser/types.ts +34 -3
- package/src/browser-electron/bridge-connection.ts +179 -22
- package/src/browser-electron/desktop-bridge-host.ts +146 -84
- package/src/browser-electron/entry.ts +132 -39
- package/src/browser-electron/ephemeral-profile.ts +152 -0
- package/src/browser-electron/history-store.ts +81 -9
- package/src/browser-electron/host-main.ts +115 -36
- package/src/browser-electron/provider.ts +492 -69
- package/src/browser-electron/remote-host.ts +276 -38
- package/src/browser-electron/rpc-ops.ts +41 -0
- package/src/browser-electron/settings-route.ts +87 -0
- package/src/browser-electron/settings-store.ts +173 -24
- package/src/browser-electron/system-browser.ts +408 -39
- package/src/index.ts +3 -2
- package/src/tool-browser/element-fields.ts +167 -0
- package/src/tool-browser/index.ts +155 -76
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
1
2
|
/**
|
|
2
3
|
* Transport for the desktop shell's browser bridge: one long-lived connection to a
|
|
3
4
|
* loopback TCP service.
|
|
@@ -18,53 +19,157 @@
|
|
|
18
19
|
* @module dsh-browser/browser-electron/bridge-connection
|
|
19
20
|
*/
|
|
20
21
|
import { connect } from 'node:net';
|
|
22
|
+
/**
|
|
23
|
+
* Safety cap on one un-answered reply line. CDP answers from the sidebar (a screenshot's
|
|
24
|
+
* base64 is the big one) cross this socket, so the cap is generous; what it exists for is a
|
|
25
|
+
* peer that never sends a newline — `this.buffer += chunk` grew without bound, unlike the
|
|
26
|
+
* same buffer in remote-host.ts, which caps at 512 MiB.
|
|
27
|
+
*/
|
|
28
|
+
const MAX_BUFFER_BYTES = 128 * 1024 * 1024;
|
|
29
|
+
/**
|
|
30
|
+
* Field this client adds to every request so the shell can echo it back and never relies on
|
|
31
|
+
* byte order to pair an answer with its request. Loosely named, and additive on purpose: a
|
|
32
|
+
* bridge that does not know it simply ignores it (the current shipped shell does), and the
|
|
33
|
+
* client then falls back to arrival order, which is what the protocol guaranteed before.
|
|
34
|
+
*/
|
|
35
|
+
const REQUEST_ID_FIELD = 'bridgeRequestId';
|
|
21
36
|
/**
|
|
22
37
|
* A reusable connection to the bridge.
|
|
23
38
|
*
|
|
24
|
-
* Requests
|
|
25
|
-
*
|
|
26
|
-
*
|
|
39
|
+
* Requests may be in flight CONCURRENTLY, so answers are correlated by id rather than by
|
|
40
|
+
* position: `call()` stamps a unique {@link REQUEST_ID_FIELD} on the request and `onData()`
|
|
41
|
+
* resolves the entry that carries the echoed id. A bridge that does not echo it is still
|
|
42
|
+
* supported — its answer is handed to the oldest outstanding request, which is the
|
|
43
|
+
* arrival-order rule the protocol states. Once such an answer has been seen, the connection
|
|
44
|
+
* stops keeping two requests in flight (a second one re-dials), so position can never be the
|
|
45
|
+
* only thing distinguishing two callers' answers. An answer that matches nothing is never
|
|
46
|
+
* handed to whichever request happens to be first: the socket is dropped instead.
|
|
47
|
+
*
|
|
48
|
+
* A broken socket is discarded so the next call dials again — a dead connection must never
|
|
49
|
+
* become a dead plugin.
|
|
27
50
|
*/
|
|
28
51
|
export class BridgeConnection {
|
|
29
52
|
endpoint;
|
|
53
|
+
endpointPath;
|
|
30
54
|
socket;
|
|
31
55
|
buffer = '';
|
|
56
|
+
/** Outstanding requests by id. */
|
|
57
|
+
pending = new Map();
|
|
58
|
+
/** Ids of the requests sent on the current socket, oldest first. */
|
|
32
59
|
queue = [];
|
|
60
|
+
nextId = 1;
|
|
61
|
+
/** True once any reply arrived without an id: this bridge does not echo request ids. */
|
|
62
|
+
idLessBridge = false;
|
|
63
|
+
/** True once this connection may only have one request in flight at a time. */
|
|
64
|
+
serialisable = false;
|
|
33
65
|
/**
|
|
34
66
|
* @param endpoint - the shell's published bridge endpoint.
|
|
67
|
+
* @param endpointPath - where that endpoint is republished, so a moved port can be followed.
|
|
35
68
|
*/
|
|
36
|
-
constructor(endpoint) {
|
|
69
|
+
constructor(endpoint, endpointPath) {
|
|
37
70
|
this.endpoint = endpoint;
|
|
71
|
+
this.endpointPath = endpointPath;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Re-read the published endpoint, and adopt it if the shell has moved.
|
|
75
|
+
*
|
|
76
|
+
* The bridge binds an ephemeral port and picks a NEW one whenever its server is recreated. The
|
|
77
|
+
* endpoint file is rewritten every fifteen seconds, but a host that adopted the old one keeps
|
|
78
|
+
* dialling a dead port forever: measured here as `connect ECONNREFUSED 127.0.0.1:52276` while
|
|
79
|
+
* the live bridge sat on 52411. Discarding the socket is not enough — the comment below says a
|
|
80
|
+
* dead connection must never become a dead plugin, and a stale PORT makes exactly that happen.
|
|
81
|
+
*
|
|
82
|
+
* Cheap and safe: one small file read per call, no socket work.
|
|
83
|
+
*
|
|
84
|
+
* @returns true when a different endpoint was adopted.
|
|
85
|
+
*/
|
|
86
|
+
refreshEndpoint() {
|
|
87
|
+
if (this.endpointPath === undefined)
|
|
88
|
+
return false;
|
|
89
|
+
try {
|
|
90
|
+
const raw = JSON.parse(readFileSync(this.endpointPath, 'utf8'));
|
|
91
|
+
if (typeof raw.port !== 'number' || typeof raw.token !== 'string')
|
|
92
|
+
return false;
|
|
93
|
+
if (raw.port === this.endpoint.port && raw.token === this.endpoint.token)
|
|
94
|
+
return false;
|
|
95
|
+
this.endpoint = {
|
|
96
|
+
port: raw.port,
|
|
97
|
+
token: raw.token,
|
|
98
|
+
pid: typeof raw.pid === 'number' ? raw.pid : this.endpoint.pid,
|
|
99
|
+
};
|
|
100
|
+
// The old socket belongs to the old port.
|
|
101
|
+
this.reset(new Error('dsh-builtin-browser: the bridge moved; reconnecting to the new port'));
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
38
107
|
}
|
|
39
108
|
/**
|
|
40
109
|
* Send one request, reusing the socket if it is still healthy.
|
|
41
|
-
*
|
|
110
|
+
*
|
|
111
|
+
* Requests may overlap: the request is written as soon as it is queued, so two concurrent
|
|
112
|
+
* callers have two requests in flight. Each carries a unique {@link REQUEST_ID_FIELD}, and
|
|
113
|
+
* `onData()` resolves the entry that carries the echoed id — which is what makes
|
|
114
|
+
* correlation independent of arrival order. A bridge that does not echo the field (the
|
|
115
|
+
* shipped shell today) still works by arrival order; see {@link onData}.
|
|
116
|
+
* @param request - the request body (the token and the request id are added here).
|
|
42
117
|
* @param timeoutMs - how long to wait for the answer before dropping the socket.
|
|
43
118
|
* @returns the bridge's answer.
|
|
44
119
|
*/
|
|
45
120
|
async call(request, timeoutMs = 20_000) {
|
|
46
|
-
|
|
121
|
+
// One re-dial, and only for the case this exists for: a socket that has already carried
|
|
122
|
+
// an id-less answer cannot have a second request in flight, because two outstanding
|
|
123
|
+
// requests would be matched by position — the swap this correlation prevents. It is a
|
|
124
|
+
// straight-line re-dial rather than a retry loop: an earlier revision re-entered call()
|
|
125
|
+
// from inside its own promise executor, which left an orphaned pending entry behind and,
|
|
126
|
+
// when the condition could not clear, spun until the process ran out of heap.
|
|
127
|
+
// Follow the shell if its bridge moved to a new port (see refreshEndpoint).
|
|
128
|
+
this.refreshEndpoint();
|
|
129
|
+
let socket = this.ensureSocket();
|
|
130
|
+
if (this.serialisable && this.queue.length > 0) {
|
|
131
|
+
this.reset(new Error('dsh-builtin-browser: bridge connection reopened so replies stay attributable'));
|
|
132
|
+
socket = this.ensureSocket();
|
|
133
|
+
}
|
|
134
|
+
const id = this.nextId++;
|
|
47
135
|
return await new Promise((resolve, reject) => {
|
|
136
|
+
this.pending.set(id, {
|
|
137
|
+
resolve: answer => { clearTimeout(timer); resolve(answer); },
|
|
138
|
+
reject: error => { clearTimeout(timer); reject(error); },
|
|
139
|
+
});
|
|
140
|
+
this.queue.push(id);
|
|
48
141
|
const timer = setTimeout(() => {
|
|
49
|
-
const index = this.queue.findIndex(entry => entry.resolve === resolve);
|
|
50
|
-
if (index !== -1)
|
|
51
|
-
this.queue.splice(index, 1);
|
|
52
142
|
const error = new Error(`dsh-builtin-browser: bridge timed out after ${timeoutMs}ms`);
|
|
53
|
-
|
|
143
|
+
// An unanswered request desynchronises the stream for an id-less bridge, and leaves one
|
|
144
|
+
// outstanding for one that echoes ids — either way the next call should start on a
|
|
145
|
+
// fresh socket. Only when THIS request's socket is still the live one: tearing down the
|
|
146
|
+
// REPLACEMENT because an abandoned request timed out would break whatever is using it.
|
|
147
|
+
//
|
|
148
|
+
// This test used to read `this.pending.has(id) && this.socket === socket`, with
|
|
149
|
+
// `forget(id)` on the line above it — and forget() is what removes the entry that `has`
|
|
150
|
+
// was looking for, so the condition was false every single time and the rebuild never
|
|
151
|
+
// ran. A timed-out call therefore left the desynchronised socket in place, and every
|
|
152
|
+
// later call inherited exactly the ambiguity this reset exists to clear. Deciding
|
|
153
|
+
// before forgetting is the whole fix; the `else` still drops the stale entry.
|
|
154
|
+
if (this.socket === socket)
|
|
155
|
+
this.reset(error);
|
|
156
|
+
else
|
|
157
|
+
this.forget(id);
|
|
158
|
+
// reset() already rejected this entry through its wrapper when it ran, so this is
|
|
159
|
+
// either the only rejection or a no-op that guarantees the caller settles.
|
|
54
160
|
reject(error);
|
|
55
161
|
}, timeoutMs);
|
|
56
|
-
this.queue.push({
|
|
57
|
-
resolve: answer => { clearTimeout(timer); resolve(answer); },
|
|
58
|
-
reject: error => { clearTimeout(timer); reject(error); },
|
|
59
|
-
});
|
|
60
162
|
try {
|
|
61
|
-
|
|
163
|
+
// The id is added LAST so a request body can never shadow it, and the token is
|
|
164
|
+
// added here rather than by the caller.
|
|
165
|
+
socket.write(JSON.stringify({ token: this.endpoint.token, ...request, [REQUEST_ID_FIELD]: id }) + '\n');
|
|
62
166
|
}
|
|
63
167
|
catch (error) {
|
|
64
168
|
clearTimeout(timer);
|
|
65
|
-
this.
|
|
169
|
+
this.forget(id);
|
|
66
170
|
const failure = error instanceof Error ? error : new Error(String(error));
|
|
67
|
-
this.
|
|
171
|
+
if (this.socket === socket)
|
|
172
|
+
this.reset(failure);
|
|
68
173
|
reject(failure);
|
|
69
174
|
}
|
|
70
175
|
});
|
|
@@ -80,16 +185,33 @@ export class BridgeConnection {
|
|
|
80
185
|
const socket = connect({ host: '127.0.0.1', port: this.endpoint.port });
|
|
81
186
|
socket.setEncoding('utf8');
|
|
82
187
|
socket.on('data', chunk => this.onData(String(chunk)));
|
|
83
|
-
socket
|
|
84
|
-
|
|
188
|
+
// Both handlers pass the socket they belong to, so a late event from a socket that was
|
|
189
|
+
// already replaced cannot tear down its successor: 'close' still fires for a socket that
|
|
190
|
+
// was removed mid-recycle, and acting on it there would kill a perfectly good connection.
|
|
191
|
+
socket.on('error', error => this.socketFailed(socket, error));
|
|
192
|
+
socket.on('close', () => this.socketFailed(socket, new Error('dsh-builtin-browser: bridge connection closed')));
|
|
85
193
|
this.buffer = '';
|
|
86
194
|
this.socket = socket;
|
|
87
195
|
socket.write(JSON.stringify({ token: this.endpoint.token }) + '\n');
|
|
88
196
|
return socket;
|
|
89
197
|
}
|
|
90
|
-
/**
|
|
198
|
+
/** Tear the connection down for an event raised by `source`, unless it is already gone. */
|
|
199
|
+
socketFailed(source, error) {
|
|
200
|
+
if (this.socket !== source)
|
|
201
|
+
return;
|
|
202
|
+
this.reset(error);
|
|
203
|
+
}
|
|
204
|
+
/** Resolve queued requests by id, or in order when the bridge does not echo one. */
|
|
91
205
|
onData(chunk) {
|
|
92
206
|
this.buffer += chunk;
|
|
207
|
+
// A peer that never sends a newline must not grow this buffer without bound. Failing
|
|
208
|
+
// the connection is the honest outcome: the caller gets an error naming the cap, while
|
|
209
|
+
// the next call dials a fresh socket.
|
|
210
|
+
if (this.buffer.length > MAX_BUFFER_BYTES) {
|
|
211
|
+
this.buffer = '';
|
|
212
|
+
this.reset(new Error(`dsh-builtin-browser: bridge reply exceeded ${String(MAX_BUFFER_BYTES)} bytes`));
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
93
215
|
let newline = this.buffer.indexOf('\n');
|
|
94
216
|
while (newline !== -1) {
|
|
95
217
|
const line = this.buffer.slice(0, newline).trim();
|
|
@@ -97,9 +219,28 @@ export class BridgeConnection {
|
|
|
97
219
|
newline = this.buffer.indexOf('\n');
|
|
98
220
|
if (line === '')
|
|
99
221
|
continue;
|
|
100
|
-
const
|
|
222
|
+
const echoed = echoOf(line);
|
|
223
|
+
const id = echoed ?? this.queue[0];
|
|
224
|
+
if (id === undefined) {
|
|
225
|
+
// Nothing is waiting for this answer: the peer is answering something this socket
|
|
226
|
+
// never asked (or answered twice). Drop the socket rather than trust the stream.
|
|
227
|
+
this.reset(new Error('dsh-builtin-browser: bridge answered with no request outstanding'));
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const index = this.queue.indexOf(id);
|
|
231
|
+
if (index !== -1)
|
|
232
|
+
this.queue.splice(index, 1);
|
|
233
|
+
const entry = this.pending.get(id);
|
|
101
234
|
if (entry === undefined)
|
|
102
235
|
continue;
|
|
236
|
+
this.pending.delete(id);
|
|
237
|
+
// Id-less bridges pair by order, so a reply for the WRONG request cannot be told apart
|
|
238
|
+
// from the right one. Remember that this bridge does not echo ids, and serialise every
|
|
239
|
+
// later call so the ambiguity cannot arise again.
|
|
240
|
+
if (echoed === undefined) {
|
|
241
|
+
this.idLessBridge = true;
|
|
242
|
+
this.serialisable = true;
|
|
243
|
+
}
|
|
103
244
|
try {
|
|
104
245
|
const answer = JSON.parse(line);
|
|
105
246
|
// A rejected token means the shell restarted with a new one, so this socket
|
|
@@ -117,6 +258,13 @@ export class BridgeConnection {
|
|
|
117
258
|
}
|
|
118
259
|
}
|
|
119
260
|
}
|
|
261
|
+
/** Forget one request (its answer can no longer be matched). */
|
|
262
|
+
forget(id) {
|
|
263
|
+
this.pending.delete(id);
|
|
264
|
+
const index = this.queue.indexOf(id);
|
|
265
|
+
if (index !== -1)
|
|
266
|
+
this.queue.splice(index, 1);
|
|
267
|
+
}
|
|
120
268
|
/** Tear the socket down, failing pending work. */
|
|
121
269
|
reset(error) {
|
|
122
270
|
if (this.socket !== undefined) {
|
|
@@ -126,8 +274,23 @@ export class BridgeConnection {
|
|
|
126
274
|
socket.destroy();
|
|
127
275
|
}
|
|
128
276
|
this.buffer = '';
|
|
129
|
-
|
|
277
|
+
this.queue.length = 0;
|
|
278
|
+
const pending = [...this.pending.values()];
|
|
279
|
+
this.pending.clear();
|
|
130
280
|
for (const entry of pending)
|
|
131
281
|
entry.reject(error);
|
|
132
282
|
}
|
|
133
283
|
}
|
|
284
|
+
/** The request id a raw answer line echoes back, when it carries one. */
|
|
285
|
+
function echoOf(line) {
|
|
286
|
+
try {
|
|
287
|
+
const answer = JSON.parse(line);
|
|
288
|
+
const value = answer[REQUEST_ID_FIELD];
|
|
289
|
+
return typeof value === 'number' && Number.isSafeInteger(value) ? value : undefined;
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
// A malformed line cannot be matched by id; the caller consumes it in order and
|
|
293
|
+
// reports the parse failure itself.
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
@@ -36,14 +36,51 @@ export declare function bridgeEndpointPath(): string;
|
|
|
36
36
|
*/
|
|
37
37
|
export declare class DesktopBridgeViewHost implements ElectronBrowserViewHost {
|
|
38
38
|
private readonly connection;
|
|
39
|
+
/**
|
|
40
|
+
* One entry per view this host handed out: the conversation it serves, and the guest backing it.
|
|
41
|
+
*
|
|
42
|
+
* The conversation belongs to the ENTRY, not to this host. A single plugin process serves every
|
|
43
|
+
* conversation — DSH runs them all through one host instance — so an owner held on the host
|
|
44
|
+
* would be one conversation's id applied to all of them, and a page could still land in
|
|
45
|
+
* another's sidebar however carefully the bridge checked it.
|
|
46
|
+
*/
|
|
39
47
|
private readonly views;
|
|
40
48
|
/**
|
|
41
|
-
*
|
|
49
|
+
* This process's identity with the bridge, for tab ownership.
|
|
50
|
+
*
|
|
51
|
+
* Every DSH session is its own plugin process, and the sidebar is one surface they all share.
|
|
52
|
+
* A session can only see its own `views` map, so before the bridge kept a ledger it adopted
|
|
53
|
+
* whichever tab it found — including one another session had opened, which is how a URL from
|
|
54
|
+
* one session appeared in another's sidebar. The id is per process, so two sessions never
|
|
55
|
+
* collide and one session's tabs are never handed to another; it is deliberately not the DSH
|
|
56
|
+
* session id, which this layer has no access to and does not need.
|
|
57
|
+
*
|
|
58
|
+
* The whole ledger rests on that premise, so it was measured rather than assumed: with two
|
|
59
|
+
* sessions running, the desktop had TWO host processes (plus one main, two renderers and a
|
|
60
|
+
* gpu/utility pair). If sessions ever shared a process, every owner check in the bridge would
|
|
61
|
+
* compare equal and the bleed would return in a new shape — so a change to how DSH spawns
|
|
62
|
+
* sessions invalidates this file, not just this comment.
|
|
63
|
+
*
|
|
64
|
+
* It is now the CONVERSATION id rather than an invented uuid, read from the plugin's ctx by the
|
|
65
|
+
* same path DSH's own desktop host uses. That matters because the shell keeps one sidebar per
|
|
66
|
+
* conversation and writes its session id onto each sidebar container (on the React fiber,
|
|
67
|
+
* measured on the running app) — so the bridge can only answer "is this sidebar mine" if it has
|
|
68
|
+
* a comparable id. With an invented uuid it never could, which is how a command came to be typed
|
|
69
|
+
* into another conversation's address bar.
|
|
70
|
+
*
|
|
71
|
+
* Falls back to a random uuid when the host cannot supply one — an older DSH, or a non-desktop
|
|
72
|
+
* composition. That is no worse than before: the bridge then knows only "this process", and it
|
|
73
|
+
* refuses to operate on a sidebar it cannot prove is its own.
|
|
74
|
+
*/
|
|
75
|
+
private readonly owner;
|
|
76
|
+
/**
|
|
77
|
+
* Guests whose view is gone but whose page may still be open in the sidebar, with the
|
|
78
|
+
* conversation each belongs to.
|
|
42
79
|
*
|
|
43
|
-
* The provider destroys its view handles before it asks for a release, so the
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
80
|
+
* The provider destroys its view handles before it asks for a release, so the mapping is dropped
|
|
81
|
+
* by then — keeping the guest ids here is what lets us close only our own tabs when several
|
|
82
|
+
* conversations are running (requirements §4: each session gets its own page). The owner rides
|
|
83
|
+
* along for the same reason it does in {@link views}: a release names one conversation.
|
|
47
84
|
*/
|
|
48
85
|
private readonly orphaned;
|
|
49
86
|
/**
|
|
@@ -69,49 +106,43 @@ export declare class DesktopBridgeViewHost implements ElectronBrowserViewHost {
|
|
|
69
106
|
* caller falls back to self-hosting, which always works.
|
|
70
107
|
* @returns the host, or undefined when no bridge is available.
|
|
71
108
|
*/
|
|
72
|
-
static discover(): Promise<DesktopBridgeViewHost | undefined>;
|
|
109
|
+
static discover(sessionId?: string): Promise<DesktopBridgeViewHost | undefined>;
|
|
73
110
|
/** Whether this host can back views: the bridge already answered `list`. */
|
|
74
111
|
available(): boolean;
|
|
75
112
|
/**
|
|
76
|
-
* The guest
|
|
113
|
+
* The guest backing a view, materialized on first use.
|
|
77
114
|
*
|
|
78
|
-
* The
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* every existing guest is already spoken for.
|
|
115
|
+
* The conversation that called owns exactly one sidebar page on this carrier — a human and the
|
|
116
|
+
* agent look at the same page — so every view resolves to that conversation's own guest, and the
|
|
117
|
+
* page does not multiply behind the provider's own tab bookkeeping.
|
|
118
|
+
*
|
|
119
|
+
* A cached guest is used as-is: probing it first cost a round-trip on every command, so liveness
|
|
120
|
+
* is established by the command failing instead.
|
|
85
121
|
* @param viewId - the view whose guest is wanted.
|
|
86
|
-
* @param url - address to use when
|
|
122
|
+
* @param url - address to use when this conversation has no page yet.
|
|
87
123
|
*/
|
|
88
124
|
private guestFor;
|
|
89
125
|
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
126
|
+
* @param owner - the conversation this view serves. Recorded WITH the view: one host instance
|
|
127
|
+
* serves every conversation, so the owner cannot live on the host.
|
|
92
128
|
*/
|
|
93
|
-
|
|
94
|
-
createView(): ElectronViewHandle;
|
|
129
|
+
createView(owner?: string): ElectronViewHandle;
|
|
95
130
|
destroyView(handle: ElectronViewHandle): void;
|
|
96
131
|
/**
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
132
|
+
* Bring this view's tab to the front in the sidebar.
|
|
133
|
+
*
|
|
134
|
+
* This used to be empty, on the reasoning that the sidebar is already on screen so there is
|
|
135
|
+
* nothing to show. That confused "the sidebar is visible" with "this page is the one being
|
|
136
|
+
* looked at": opening a page left it behind whatever the human had in front, which is issue
|
|
137
|
+
* #23. The other two carriers both do this — the self-hosted window re-adds the view and the
|
|
138
|
+
* system browser calls `Page.bringToFront` — so this carrier was the odd one out.
|
|
139
|
+
*
|
|
140
|
+
* Best-effort by design: the tab strip belongs to the shell's renderer, and failing to raise
|
|
141
|
+
* it must not fail the navigation that has already happened.
|
|
142
|
+
* @param handle - the view to bring forward.
|
|
108
143
|
*/
|
|
109
|
-
|
|
110
|
-
/** Focus is the shell's to manage, and there is no separate window to raise. */
|
|
111
|
-
focus(): Promise<void>;
|
|
144
|
+
showView(handle: ElectronViewHandle): void;
|
|
112
145
|
onUserAction(handler: (action: BrowserUserAction) => void): void;
|
|
113
|
-
/** The shell reports its own window lifecycle; nothing to subscribe to here. */
|
|
114
|
-
onViewClosed(): void;
|
|
115
146
|
/**
|
|
116
147
|
* Release the sidebar's browser pages (requirements §3, `ui.closeWithSession`).
|
|
117
148
|
*
|
|
@@ -119,9 +150,11 @@ export declare class DesktopBridgeViewHost implements ElectronBrowserViewHost {
|
|
|
119
150
|
* belongs to the shell and would happily keep the page (and its renderer) alive.
|
|
120
151
|
* Closing the tabs is what actually ends the page — and only the page: cookies
|
|
121
152
|
* live in the partition, history on disk, so both survive.
|
|
153
|
+
* @param owner - the conversation whose pages to close. Omit to close every conversation's,
|
|
154
|
+
* which is only correct from a teardown that is itself global.
|
|
122
155
|
* @returns a promise that settles once the shell has been asked.
|
|
123
156
|
*/
|
|
124
|
-
releasePage(): Promise<void>;
|
|
157
|
+
releasePage(owner?: string): Promise<void>;
|
|
125
158
|
/**
|
|
126
159
|
* Fold the sidebar away without ending the page (`ui.autoExpandOnce` is off, or
|
|
127
160
|
* the caller wants the screen back while work continues).
|