dsh-inner-browser-use 0.34.96 → 0.34.98
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/client.js +95 -12
- package/index.js +15 -8
- package/package.json +1 -1
- package/sidecar.js +53 -0
package/client.js
CHANGED
|
@@ -119,12 +119,51 @@ window.__ModuleLoader__.load({
|
|
|
119
119
|
return ''
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
/* The chat that asked for the pane, and the chats whose pane the
|
|
123
|
+
person closed. A strip close and a layout restore do not both
|
|
124
|
+
report themselves here, so the mark is kept on this side too: no
|
|
125
|
+
automatic ask may redraw a column the person shut. */
|
|
126
|
+
let openWho = ''
|
|
127
|
+
let openAt = 0
|
|
128
|
+
let paneSeen = false
|
|
129
|
+
let missing = 0
|
|
130
|
+
const closedAt = new Map()
|
|
131
|
+
const hasPane = (sessionId) => {
|
|
124
132
|
try {
|
|
125
|
-
|
|
133
|
+
const rows = surface?.tabsIn?.(sessionId) ?? []
|
|
134
|
+
return rows.some((row) => {
|
|
135
|
+
const kind = String(row?.kind ?? row?.record?.kind ?? '')
|
|
136
|
+
const id = String(row?.id ?? row?.record?.id ?? '')
|
|
137
|
+
return kind === SIDE_KIND || id === SIDE_ID
|
|
138
|
+
})
|
|
126
139
|
} catch {
|
|
127
|
-
|
|
140
|
+
return false
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const closeHere = (sessionId, byPerson = false) => {
|
|
144
|
+
if (sessionId === '') return
|
|
145
|
+
closedAt.set(sessionId, Date.now())
|
|
146
|
+
if (openWho === sessionId) {
|
|
147
|
+
openWho = ''
|
|
148
|
+
openAt = 0
|
|
149
|
+
paneSeen = false
|
|
150
|
+
missing = 0
|
|
151
|
+
}
|
|
152
|
+
/* A note is sent only when the person is quitting the mirror, not when a
|
|
153
|
+
chat switch closes this column: the switch's own ask claims the
|
|
154
|
+
mirror a moment later, and a late note would take it away from the
|
|
155
|
+
chat that just asked. */
|
|
156
|
+
if (byPerson === true) {
|
|
157
|
+
/* A close usually unmounts the pane in the same tick, which cancels
|
|
158
|
+
an ordinary request; a beacon is delivered after the page is gone,
|
|
159
|
+
so the note always arrives. */
|
|
160
|
+
const url = `${ROUTE}/sidecar/forget?session=${encodeURIComponent(sessionId)}` // ui-copy-ignore
|
|
161
|
+
try {
|
|
162
|
+
const sent = typeof navigator?.sendBeacon === 'function' && navigator.sendBeacon(url) === true
|
|
163
|
+
if (sent !== true) void fetch(url, { method: 'POST' })
|
|
164
|
+
} catch {
|
|
165
|
+
/* a page that cannot hear it keeps its note */
|
|
166
|
+
}
|
|
128
167
|
}
|
|
129
168
|
let records = []
|
|
130
169
|
try {
|
|
@@ -149,10 +188,23 @@ window.__ModuleLoader__.load({
|
|
|
149
188
|
}
|
|
150
189
|
}
|
|
151
190
|
}
|
|
152
|
-
const ask = (sessionId) => {
|
|
191
|
+
const ask = (sessionId, reason = 'ask', at = 0) => {
|
|
153
192
|
if (sessionId === '' || mountedOf() !== sessionId) return
|
|
193
|
+
const shut = closedAt.get(sessionId)
|
|
194
|
+
/* A slow beat that only wants the frames back must not put a closed
|
|
195
|
+
column on screen again: it may refresh a pane that is drawn and
|
|
196
|
+
nothing else. */
|
|
197
|
+
if (reason === 'keepalive' && (shut !== undefined || !hasPane(sessionId))) return
|
|
198
|
+
/* A real ask is only newer than the close that preceded it: a
|
|
199
|
+
replayed one from before the person shut the pane stays spent. */
|
|
200
|
+
if (shut !== undefined && (reason !== 'ask' || (at !== 0 && at <= shut))) return
|
|
201
|
+
if (shut !== undefined) closedAt.delete(sessionId)
|
|
154
202
|
if (ownerSession !== '' && ownerSession !== sessionId) closeHere(ownerSession)
|
|
155
203
|
ownerSession = sessionId
|
|
204
|
+
openWho = sessionId
|
|
205
|
+
missing = 0
|
|
206
|
+
paneSeen = hasPane(sessionId)
|
|
207
|
+
if (paneSeen !== true) openAt = Date.now()
|
|
156
208
|
try {
|
|
157
209
|
if (typeof surface?.openTabIn === 'function') surface.openTabIn(sessionId, SIDE_KIND, {})
|
|
158
210
|
else if (typeof surface?.openTab === 'function') surface.openTab(SIDE_KIND)
|
|
@@ -160,6 +212,25 @@ window.__ModuleLoader__.load({
|
|
|
160
212
|
/* a column that refuses keeps its own tabs */
|
|
161
213
|
}
|
|
162
214
|
}
|
|
215
|
+
/* The pane this chat asked for can be taken down by paths that never
|
|
216
|
+
call onClose. One that was drawn and is gone was closed by the
|
|
217
|
+
person, and that is the answer this watcher waits for. */
|
|
218
|
+
const watchPane = () => {
|
|
219
|
+
if (openWho === '') return
|
|
220
|
+
if (mountedOf() !== openWho) return
|
|
221
|
+
if (hasPane(openWho)) {
|
|
222
|
+
paneSeen = true
|
|
223
|
+
missing = 0
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
/* A pane the person never saw draw is still being built; only one
|
|
227
|
+
that was on screen and then vanished is a close. */
|
|
228
|
+
if (paneSeen !== true) return
|
|
229
|
+
if (Date.now() - openAt < 2500) return
|
|
230
|
+
missing += 1
|
|
231
|
+
if (missing < 2) return
|
|
232
|
+
closeHere(openWho, true)
|
|
233
|
+
}
|
|
163
234
|
/* The same rule on this side: a second mount of the same tab is a
|
|
164
235
|
no-op, not a thrown error that takes the pane down. */
|
|
165
236
|
const safely = (register) => {
|
|
@@ -177,7 +248,7 @@ window.__ModuleLoader__.load({
|
|
|
177
248
|
priority: 'extension',
|
|
178
249
|
title: () => L('side.label'),
|
|
179
250
|
/* The tab strip's close control is not the pane's own button: a
|
|
180
|
-
tab closed there never
|
|
251
|
+
tab closed there never sent the note, the ask stayed alive, and
|
|
181
252
|
the keepalive put the mirror back a few seconds later — a tab
|
|
182
253
|
the person closed that would not stay closed. Every close path
|
|
183
254
|
ends here. */
|
|
@@ -186,7 +257,7 @@ window.__ModuleLoader__.load({
|
|
|
186
257
|
disposers.push(scope.slots.inject(SLOT, () => scope.slots.register({
|
|
187
258
|
name: SLOT, // ui-copy-ignore
|
|
188
259
|
key: SIDE_ID, // ui-copy-ignore
|
|
189
|
-
inject: (sessionId) => ({ sessionId, dropMirror: () => closeHere(sessionId) }),
|
|
260
|
+
inject: (sessionId) => ({ sessionId, dropMirror: () => closeHere(sessionId, true) }),
|
|
190
261
|
}, SidecarBody)))
|
|
191
262
|
disposers.push(scope.slots.inject(TITLE_SLOT, () => scope.slots.register({
|
|
192
263
|
name: TITLE_SLOT, // ui-copy-ignore
|
|
@@ -195,7 +266,7 @@ window.__ModuleLoader__.load({
|
|
|
195
266
|
}, () => h('span', { className: 'dshib-title' }, L('side.label')))))
|
|
196
267
|
/* The mirror comes only when this chat asked for it: the ask is pushed
|
|
197
268
|
to this chat's page, and nothing opens by itself. // ui-copy-ignore */
|
|
198
|
-
sidecarOpener = (sessionId) => ask(sessionId)
|
|
269
|
+
sidecarOpener = (sessionId, reason = 'ask', at = 0) => ask(sessionId, reason, at)
|
|
199
270
|
/* The ask arrives on this chat's own downlink, so a frame for another
|
|
200
271
|
chat never reaches this column. The stream follows the chat on
|
|
201
272
|
screen: a switch closes the old one and opens the new one. */
|
|
@@ -231,7 +302,7 @@ window.__ModuleLoader__.load({
|
|
|
231
302
|
}
|
|
232
303
|
if (frame?.type !== 'sidecar-wanted') return
|
|
233
304
|
const asked = String(frame.session ?? '')
|
|
234
|
-
if (asked !== '') sidecarOpener?.(asked)
|
|
305
|
+
if (asked !== '') sidecarOpener?.(asked, String(frame.reason ?? 'ask'), Number(frame.at ?? 0))
|
|
235
306
|
})
|
|
236
307
|
askSource = source
|
|
237
308
|
askWho = who
|
|
@@ -276,7 +347,7 @@ window.__ModuleLoader__.load({
|
|
|
276
347
|
again()
|
|
277
348
|
return
|
|
278
349
|
}
|
|
279
|
-
ask(who)
|
|
350
|
+
ask(who, 'restore')
|
|
280
351
|
})
|
|
281
352
|
.catch(again)
|
|
282
353
|
} catch {
|
|
@@ -285,7 +356,10 @@ window.__ModuleLoader__.load({
|
|
|
285
356
|
}
|
|
286
357
|
restore()
|
|
287
358
|
const offAsk = watchAsk()
|
|
288
|
-
const pollAsk = setInterval(
|
|
359
|
+
const pollAsk = setInterval(() => {
|
|
360
|
+
syncAsk()
|
|
361
|
+
watchPane()
|
|
362
|
+
}, 1000)
|
|
289
363
|
const stopAsk = () => {
|
|
290
364
|
clearInterval(pollAsk)
|
|
291
365
|
for (const timer of restoreTimers) clearTimeout(timer)
|
|
@@ -598,7 +672,16 @@ window.__ModuleLoader__.load({
|
|
|
598
672
|
})
|
|
599
673
|
.then((answer) => (answer.ok === true ? answer.json() : { ok: false }))
|
|
600
674
|
.then((said) => {
|
|
601
|
-
if (!live
|
|
675
|
+
if (!live) return
|
|
676
|
+
/* The browser answers with the shape it actually took: drawing
|
|
677
|
+
the picture at that size fills the pane even when the page
|
|
678
|
+
refused the exact one asked for. */
|
|
679
|
+
const took = said?.view ?? null
|
|
680
|
+
if (said?.ok === true && took !== null && Number(took.width) > 0 && Number(took.height) > 0) {
|
|
681
|
+
setView({ w: Math.round(Number(took.width)), h: Math.round(Number(took.height)) })
|
|
682
|
+
return
|
|
683
|
+
}
|
|
684
|
+
if (said?.ok === true || tries >= 5) return
|
|
602
685
|
tries += 1
|
|
603
686
|
timer = setTimeout(send, 400)
|
|
604
687
|
})
|
package/index.js
CHANGED
|
@@ -131,15 +131,16 @@ export class Bridge {
|
|
|
131
131
|
* Remember that a chat wants the mirror, and tell it now if it is listening.
|
|
132
132
|
* @param {string} sessionId - chat that asked.
|
|
133
133
|
* @param {number} [at] - when it asked; older asks are not replayed.
|
|
134
|
+
* @param {string} [reason] - 'ask' for a real request, 'keepalive' for a nudge.
|
|
134
135
|
* @returns {boolean} whether a live tab took the frame.
|
|
135
136
|
*/
|
|
136
|
-
want(sessionId, at = Date.now()) {
|
|
137
|
+
want(sessionId, at = Date.now(), reason = 'ask') {
|
|
137
138
|
const id = this.canon(sessionId)
|
|
138
139
|
if (id === '') return false
|
|
139
140
|
this.claim(id)
|
|
140
141
|
this.wanted = { session: id, at }
|
|
141
142
|
if (this.persist === true) writeMirror({ owner: this.owner, session: id, at })
|
|
142
|
-
return this.push(id, { type: 'sidecar-wanted', session: id, at })
|
|
143
|
+
return this.push(id, { type: 'sidecar-wanted', session: id, at, reason })
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
/**
|
|
@@ -155,7 +156,7 @@ export class Bridge {
|
|
|
155
156
|
ask older than two minutes is spent and is not replayed. */
|
|
156
157
|
const ask = this.wanted
|
|
157
158
|
if (ask.session === sessionId && Date.now() - ask.at <= 120_000) {
|
|
158
|
-
this.push(sessionId, { type: 'sidecar-wanted', session: sessionId, at: ask.at })
|
|
159
|
+
this.push(sessionId, { type: 'sidecar-wanted', session: sessionId, at: ask.at, reason: 'ask' })
|
|
159
160
|
}
|
|
160
161
|
}
|
|
161
162
|
|
|
@@ -187,14 +188,20 @@ export class Bridge {
|
|
|
187
188
|
}
|
|
188
189
|
|
|
189
190
|
/**
|
|
190
|
-
*
|
|
191
|
+
* Give the mirror up: the person closed the column, so the chat stops owning
|
|
192
|
+
* it and no keepalive may ask for it again. Ownership is what the column
|
|
193
|
+
* reads on reload, so it has to be dropped on disk as well as in memory.
|
|
191
194
|
* @param {string} sessionId - chat that closed its tab.
|
|
192
|
-
* @returns {boolean} whether the
|
|
195
|
+
* @returns {boolean} whether this chat held the mirror or the ask.
|
|
193
196
|
*/
|
|
194
197
|
forget(sessionId) {
|
|
195
|
-
|
|
198
|
+
const id = this.canon(sessionId)
|
|
199
|
+
if (id === '') return false
|
|
200
|
+
const held = this.owner === id || this.wanted.session === id
|
|
196
201
|
this.wanted = { session: '', at: 0 }
|
|
197
|
-
|
|
202
|
+
if (this.owner === id) this.owner = ''
|
|
203
|
+
if (this.persist === true) writeMirror({ owner: this.owner, session: '', at: 0 })
|
|
204
|
+
return held
|
|
198
205
|
}
|
|
199
206
|
|
|
200
207
|
/** Close every stream. */
|
|
@@ -328,7 +335,7 @@ export function apply(ctx) {
|
|
|
328
335
|
const due = nudgeDue({ askedAgo: Date.now() - (lastAsk.get(owner) ?? 0), sinceLast: Date.now() - last }) // ui-copy-ignore
|
|
329
336
|
if (due !== true) return
|
|
330
337
|
last = Date.now()
|
|
331
|
-
bridge.want(owner)
|
|
338
|
+
bridge.want(owner, Date.now(), 'keepalive')
|
|
332
339
|
appendFileSync(join(STATE_DIR, 'asks.log'), `${new Date().toISOString()} asked ${owner.slice(-8)} to draw the mirror again\n`)
|
|
333
340
|
} catch {}
|
|
334
341
|
}, 4000)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-inner-browser-use",
|
|
3
|
-
"version": "0.34.
|
|
3
|
+
"version": "0.34.98",
|
|
4
4
|
"description": "An agent-owned Chromium sidecar mirrored into the DeepSeek Harness right column: the agent drives it, the person watches and can take the mouse, and logins survive restarts.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
package/sidecar.js
CHANGED
|
@@ -1962,6 +1962,10 @@ export class Sidecar {
|
|
|
1962
1962
|
/* One tab per chat: a second chat opening an address gets a tab of its
|
|
1963
1963
|
own, so neither can navigate the other's page away. */
|
|
1964
1964
|
this.pages = new Map()
|
|
1965
|
+
/* Every tab a chat has driven, so its column can offer its own windows
|
|
1966
|
+
back after it switches away: a tab the chat left is still its own, and a
|
|
1967
|
+
tab nobody claimed belongs to no column at all. */
|
|
1968
|
+
this.claims = new Map()
|
|
1965
1969
|
/* Tabs left behind by an earlier plugin load are closed once, on the first
|
|
1966
1970
|
call that finds this browser: a profile used for weeks otherwise keeps a
|
|
1967
1971
|
renderer per chat that ever opened it. */
|
|
@@ -2048,6 +2052,41 @@ export class Sidecar {
|
|
|
2048
2052
|
* @param {string} session - chat id, empty for the default tab.
|
|
2049
2053
|
* @returns {SidecarPage | null} the tab, if the browser is up.
|
|
2050
2054
|
*/
|
|
2055
|
+
/**
|
|
2056
|
+
* Remember that a tab belongs to a chat: switching away from it keeps it in
|
|
2057
|
+
* that chat's window list, and no other chat can see it.
|
|
2058
|
+
* @param {string} session - chat that drove the tab. // ui-copy-ignore
|
|
2059
|
+
* @param {string} targetId - the browser target.
|
|
2060
|
+
*/
|
|
2061
|
+
claim(session, targetId) {
|
|
2062
|
+
const who = String(session ?? '')
|
|
2063
|
+
const target = String(targetId ?? '')
|
|
2064
|
+
if (who === '' || target === '') return
|
|
2065
|
+
let set = this.claims.get(who)
|
|
2066
|
+
if (set === undefined) {
|
|
2067
|
+
set = new Set()
|
|
2068
|
+
this.claims.set(who, set)
|
|
2069
|
+
}
|
|
2070
|
+
/* A chat cannot hold an unbounded list: the oldest tab it left falls off. */
|
|
2071
|
+
if (set.size >= 12 && set.has(target) !== true) {
|
|
2072
|
+
const first = set.values().next()
|
|
2073
|
+
if (first.done !== true) set.delete(first.value)
|
|
2074
|
+
}
|
|
2075
|
+
set.add(target)
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
/**
|
|
2079
|
+
* Forget a tab a chat no longer has.
|
|
2080
|
+
* @param {string} session - chat that owned it. // ui-copy-ignore
|
|
2081
|
+
* @param {string} targetId - the browser target.
|
|
2082
|
+
*/
|
|
2083
|
+
unclaim(session, targetId) {
|
|
2084
|
+
const set = this.claims.get(String(session ?? ''))
|
|
2085
|
+
if (set === undefined) return
|
|
2086
|
+
set.delete(String(targetId ?? ''))
|
|
2087
|
+
if (set.size === 0) this.claims.delete(String(session ?? ''))
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2051
2090
|
pageFor(session) {
|
|
2052
2091
|
const id = String(session ?? '')
|
|
2053
2092
|
if (id === '') return this.page
|
|
@@ -2265,6 +2304,7 @@ export class Sidecar {
|
|
|
2265
2304
|
await cdp.ready
|
|
2266
2305
|
const page = await this.bind(await new SidecarPage(cdp, this.profile, port, () => this.holding()).start()) // ui-copy-ignore
|
|
2267
2306
|
this.pages.set(id, page)
|
|
2307
|
+
this.claim(id, page.targetId)
|
|
2268
2308
|
this.touch()
|
|
2269
2309
|
this.rememberTab(id, address)
|
|
2270
2310
|
return page
|
|
@@ -2614,10 +2654,21 @@ export class Sidecar {
|
|
|
2614
2654
|
const list = await this.tabs(session)
|
|
2615
2655
|
const page = this.pageFor(session) ?? this.page
|
|
2616
2656
|
const foreign = new Set(typeof page?.siblings === 'function' ? page.siblings().map(String) : [])
|
|
2657
|
+
const who = String(session ?? '')
|
|
2658
|
+
const claimed = this.claims.get(who) ?? new Set()
|
|
2659
|
+
/* Tabs the browser no longer has must not linger in a chat's list. */
|
|
2660
|
+
if (claimed.size > 0) {
|
|
2661
|
+
const live = new Set(list.map((row) => String(row.targetId)))
|
|
2662
|
+
for (const id of [...claimed]) if (live.has(id) !== true) claimed.delete(id)
|
|
2663
|
+
if (claimed.size === 0) this.claims.delete(who)
|
|
2664
|
+
}
|
|
2617
2665
|
const shown = []
|
|
2618
2666
|
for (let at = 0; at < list.length; at += 1) {
|
|
2619
2667
|
const row = list[at]
|
|
2620
2668
|
if (foreign.has(String(row.targetId))) continue
|
|
2669
|
+
/* A tab nobody in this chat drove is not this chat's window: a page
|
|
2670
|
+
opened behind the browser's back belongs to no column. */
|
|
2671
|
+
if (row.current !== true && claimed.has(String(row.targetId)) !== true) continue
|
|
2621
2672
|
/* The index is the browser's own row, not this shorter list's: switching
|
|
2622
2673
|
goes back through `tabs()` unfiltered. */
|
|
2623
2674
|
shown.push({ title: String(row.title ?? ''), url: String(row.url ?? ''), current: row.current === true, index: at })
|
|
@@ -2666,6 +2717,7 @@ export class Sidecar {
|
|
|
2666
2717
|
await cdp.ready
|
|
2667
2718
|
const page = await this.bind(await new SidecarPage(cdp, this.profile, mine.port, () => this.holding()).start()) // ui-copy-ignore
|
|
2668
2719
|
this.pages.set(String(session), page)
|
|
2720
|
+
this.claim(session, tab.targetId)
|
|
2669
2721
|
mine.cdp.send('Page.stopScreencast').catch(() => {})
|
|
2670
2722
|
mine.cdp.close()
|
|
2671
2723
|
await page.cast()
|
|
@@ -2701,6 +2753,7 @@ export class Sidecar {
|
|
|
2701
2753
|
alone = other < 0
|
|
2702
2754
|
if (!alone) await this.useTab(other, session)
|
|
2703
2755
|
}
|
|
2756
|
+
this.unclaim(session, tab.targetId)
|
|
2704
2757
|
if (port > 0) await fetch(`http://127.0.0.1:${port}/json/close/${tab.targetId}`, { signal: AbortSignal.timeout(1000) }).catch(() => {}) // ui-copy-ignore
|
|
2705
2758
|
if (alone && mine !== null) {
|
|
2706
2759
|
/* The chat still needs somewhere to go: a fresh tab in the same browser. */
|