dsh-inner-browser-use 0.34.99 → 0.35.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.
- package/client.js +25 -25
- package/index.js +6 -3
- package/package.json +1 -1
- package/sidecar.js +92 -6
package/client.js
CHANGED
|
@@ -355,10 +355,27 @@ window.__ModuleLoader__.load({
|
|
|
355
355
|
}
|
|
356
356
|
}
|
|
357
357
|
restore()
|
|
358
|
+
/* A pane whose page the browser reclaimed is closed once, so coming
|
|
359
|
+
back to the chat does not bring the empty column back. */
|
|
360
|
+
const dropExpired = () => {
|
|
361
|
+
const who = mountedOf()
|
|
362
|
+
if (who === '' || hasPane(who) !== true) return
|
|
363
|
+
void fetch(`${ROUTE}/sidecar/state?session=${encodeURIComponent(who)}`) // ui-copy-ignore
|
|
364
|
+
.then((said) => (said.ok === true ? said.json() : null))
|
|
365
|
+
.then((state) => {
|
|
366
|
+
if (state?.chat?.expired !== true) return
|
|
367
|
+
if (mountedOf() !== who) return
|
|
368
|
+
closeHere(who, false)
|
|
369
|
+
})
|
|
370
|
+
.catch(() => {})
|
|
371
|
+
}
|
|
358
372
|
const offAsk = watchAsk()
|
|
373
|
+
let beats = 0
|
|
359
374
|
const pollAsk = setInterval(() => {
|
|
360
375
|
syncAsk()
|
|
361
376
|
watchPane()
|
|
377
|
+
beats += 1
|
|
378
|
+
if (beats % 5 === 1) dropExpired()
|
|
362
379
|
}, 1000)
|
|
363
380
|
const stopAsk = () => {
|
|
364
381
|
clearInterval(pollAsk)
|
|
@@ -674,15 +691,11 @@ window.__ModuleLoader__.load({
|
|
|
674
691
|
/* The page already has that shape: a resize that lands between two
|
|
675
692
|
viewport sizes would only cost a relayout and another frame. */
|
|
676
693
|
if (Math.abs(Number(view?.w ?? 0) - size.w) < 1 && Math.abs(Number(view?.h ?? 0) - size.h) < 1) return undefined
|
|
677
|
-
/* The page is told the column's size a beat after the resize stops
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
let tries = 0
|
|
683
|
-
let timer = 0
|
|
684
|
-
const send = () => {
|
|
685
|
-
if (!live) return
|
|
694
|
+
/* The page is told the column's size a beat after the resize stops.
|
|
695
|
+
One ask: the pane draws at whatever shape the frame says it has, so
|
|
696
|
+
a browser that is still starting costs nothing more than the next
|
|
697
|
+
resize doing the same thing again. */
|
|
698
|
+
const timer = setTimeout(() => {
|
|
686
699
|
fetch(`${ROUTE}/sidecar/viewport`, {
|
|
687
700
|
method: 'POST',
|
|
688
701
|
headers: { 'content-type': 'application/json' },
|
|
@@ -690,30 +703,17 @@ window.__ModuleLoader__.load({
|
|
|
690
703
|
})
|
|
691
704
|
.then((answer) => (answer.ok === true ? answer.json() : { ok: false }))
|
|
692
705
|
.then((said) => {
|
|
693
|
-
if (!live) return
|
|
694
706
|
/* The browser answers with the shape it actually took: drawing
|
|
695
707
|
the picture at that size fills the pane even when the page
|
|
696
708
|
refused the exact one asked for. */
|
|
697
709
|
const took = said?.view ?? null
|
|
698
710
|
if (said?.ok === true && took !== null && Number(took.width) > 0 && Number(took.height) > 0) {
|
|
699
711
|
setView({ w: Math.round(Number(took.width)), h: Math.round(Number(took.height)) })
|
|
700
|
-
return
|
|
701
712
|
}
|
|
702
|
-
if (said?.ok === true || tries >= 5) return
|
|
703
|
-
tries += 1
|
|
704
|
-
timer = setTimeout(send, 400)
|
|
705
|
-
})
|
|
706
|
-
.catch(() => {
|
|
707
|
-
if (!live || tries >= 5) return
|
|
708
|
-
tries += 1
|
|
709
|
-
timer = setTimeout(send, 600)
|
|
710
713
|
})
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
return () =>
|
|
714
|
-
live = false
|
|
715
|
-
clearTimeout(timer)
|
|
716
|
-
}
|
|
714
|
+
.catch(() => {})
|
|
715
|
+
}, 250)
|
|
716
|
+
return () => clearTimeout(timer)
|
|
717
717
|
}, [mine, size.w, size.h, view?.w, view?.h])
|
|
718
718
|
|
|
719
719
|
const who = String(props.sessionId ?? '')
|
package/index.js
CHANGED
|
@@ -280,6 +280,9 @@ export function cwdOf(execution) {
|
|
|
280
280
|
|
|
281
281
|
export function needsTab(chatState, chat, owner) {
|
|
282
282
|
if (String(chat ?? '') === '' || owner !== chat) return false
|
|
283
|
+
/* A tab the browser reclaimed is not a tab that went missing: opening one
|
|
284
|
+
again would bring back the column the person no longer needs. */
|
|
285
|
+
if (chatState?.expired === true) return false
|
|
283
286
|
return chatState?.alive !== true
|
|
284
287
|
}
|
|
285
288
|
|
|
@@ -521,7 +524,7 @@ export function apply(ctx) {
|
|
|
521
524
|
called open again: the mirror would come back empty in a chat that
|
|
522
525
|
never closed it. The chat that owned the browser asks for its state, so
|
|
523
526
|
answering honestly means picking the browser up first. */
|
|
524
|
-
if (shouldAdoptOnState(said, chat, bridge.owner)) {
|
|
527
|
+
if (shouldAdoptOnState(said, chat, bridge.owner) && mineChat?.expired !== true) {
|
|
525
528
|
try {
|
|
526
529
|
await sidecar.ensure(adoptOptions(chat, sidecar.rememberedTab?.(chat)))
|
|
527
530
|
said = await sidecar.state()
|
|
@@ -737,8 +740,8 @@ export function apply(ctx) {
|
|
|
737
740
|
const who = acting(body)
|
|
738
741
|
if (who === '') return
|
|
739
742
|
const forgot = bridge.forget(who)
|
|
740
|
-
/* Nobody is watching a picture the person
|
|
741
|
-
|
|
743
|
+
/* Nobody is watching a picture the person closed: the browser itself is
|
|
744
|
+
put away a minute later rather than a quarter of an hour, unless an
|
|
742
745
|
agent asks for it again in the meantime. */
|
|
743
746
|
try {
|
|
744
747
|
if (sidecar.watched() === 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-inner-browser-use",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.0",
|
|
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
|
@@ -78,7 +78,7 @@ const QUALITY_FLOOR = 44 // ui-copy-ignore
|
|
|
78
78
|
const QUALITY_CEILING = 76 // ui-copy-ignore
|
|
79
79
|
|
|
80
80
|
/**
|
|
81
|
-
* Decide the next JPEG quality from the weight of the frame
|
|
81
|
+
* Decide the next JPEG quality from the weight of the frame seen. A frame
|
|
82
82
|
* near 90 KB of base64 is a heavy page: lowering quality by 8 costs a little
|
|
83
83
|
* sharpness and saves a lot of transport. A small frame has room to sharpen.
|
|
84
84
|
* @param {number} current - quality now.
|
|
@@ -478,6 +478,30 @@ class Cdp {
|
|
|
478
478
|
* Input is real CDP input: the page sees the same events a person's trackpad
|
|
479
479
|
* produces, which is what makes sites that reject scripted clicks work.
|
|
480
480
|
*/
|
|
481
|
+
/**
|
|
482
|
+
* Whether two addresses show the same place: a fragment never changes the page.
|
|
483
|
+
* @param {string} a - one address.
|
|
484
|
+
* @param {string} b - another address.
|
|
485
|
+
* @returns {string} the shared key, or '' when they differ.
|
|
486
|
+
*/
|
|
487
|
+
function placeKey(value) {
|
|
488
|
+
const said = String(value ?? '')
|
|
489
|
+
if (said === '') return ''
|
|
490
|
+
try {
|
|
491
|
+
const url = new URL(said)
|
|
492
|
+
url.hash = ''
|
|
493
|
+
const path = url.pathname.replace(/\/+$/, '') || '/'
|
|
494
|
+
return `${url.protocol}//${url.host}${path}${url.search}`
|
|
495
|
+
} catch {
|
|
496
|
+
return said
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function samePlace(a, b) {
|
|
501
|
+
const key = placeKey(a)
|
|
502
|
+
return key !== '' && key === placeKey(b)
|
|
503
|
+
}
|
|
504
|
+
|
|
481
505
|
export class SidecarPage {
|
|
482
506
|
/** @param {Cdp} cdp - the page session. @param {string} profile - profile name. */
|
|
483
507
|
constructor(cdp, profile, port, hold = () => 0) { // ui-copy-ignore
|
|
@@ -774,9 +798,10 @@ export class SidecarPage {
|
|
|
774
798
|
.then((response) => (response.ok ? response.json() : []))
|
|
775
799
|
.catch(() => [])
|
|
776
800
|
const current = await this.cdp.send('Target.getTargetInfo').then((info) => String(info?.targetInfo?.targetId ?? '')).catch(() => '')
|
|
777
|
-
|
|
801
|
+
const rows = (Array.isArray(list) ? list : [])
|
|
778
802
|
.filter((item) => item.type === 'page' && item.webSocketDebuggerUrl !== undefined)
|
|
779
803
|
.map((item) => ({ targetId: String(item.id), title: String(item.title ?? ''), url: String(item.url ?? ''), webSocketUrl: String(item.webSocketDebuggerUrl), current: String(item.id) === current }))
|
|
804
|
+
return rows
|
|
780
805
|
}
|
|
781
806
|
|
|
782
807
|
/**
|
|
@@ -1300,7 +1325,7 @@ export class SidecarPage {
|
|
|
1300
1325
|
/**
|
|
1301
1326
|
* Whether this picture repeats the one the pane is already showing.
|
|
1302
1327
|
* @param {string} jpeg - base64 JPEG of the new frame.
|
|
1303
|
-
* @returns {boolean} true when sending it would
|
|
1328
|
+
* @returns {boolean} true when sending it would cost bytes.
|
|
1304
1329
|
*/
|
|
1305
1330
|
repeats(jpeg, gap = 1000) {
|
|
1306
1331
|
const prev = this.lastShot
|
|
@@ -1979,6 +2004,8 @@ export class Sidecar {
|
|
|
1979
2004
|
/* A tab being opened for a chat: a second call for the same chat waits for
|
|
1980
2005
|
it instead of opening a twin. */
|
|
1981
2006
|
this.opening = new Map()
|
|
2007
|
+
/* Chats whose tab was reclaimed: the column is told so it can go away. */
|
|
2008
|
+
this.expired = new Map()
|
|
1982
2009
|
/* A measurement or a test may want a profile of its own; the person's
|
|
1983
2010
|
browser keeps the default one. */
|
|
1984
2011
|
this.profile = String(options.profile ?? 'default')
|
|
@@ -2157,6 +2184,7 @@ export class Sidecar {
|
|
|
2157
2184
|
*/
|
|
2158
2185
|
async tabFor(session, url) {
|
|
2159
2186
|
const id = String(session ?? '')
|
|
2187
|
+
this.expired?.delete(id)
|
|
2160
2188
|
const had = this.pageFor(id)
|
|
2161
2189
|
if (had !== null) {
|
|
2162
2190
|
if (url !== undefined && url !== '') await had.navigate(url)
|
|
@@ -2268,23 +2296,62 @@ export class Sidecar {
|
|
|
2268
2296
|
if (page === this.page) continue
|
|
2269
2297
|
if (now - Number(page.touched ?? 0) < TAB_IDLE_MS) continue
|
|
2270
2298
|
this.pages.delete(id)
|
|
2299
|
+
/* The column is told the tab is gone, so a chat coming back to the front
|
|
2300
|
+
does not re-expand a pane for a page that already went away. */
|
|
2301
|
+
;(this.expired ??= new Map()).set(String(id), Date.now())
|
|
2271
2302
|
await page.stopCast().catch(() => {})
|
|
2272
2303
|
await fetch(`http://127.0.0.1:${String(page.port)}/json/close/${target}`, { signal: AbortSignal.timeout(800) }).catch(() => {}) // ui-copy-ignore
|
|
2273
2304
|
this.note(`closed an idle tab — its address is kept for the chat`)
|
|
2274
2305
|
}
|
|
2275
2306
|
}
|
|
2276
2307
|
|
|
2308
|
+
/**
|
|
2309
|
+
* A page already at an address, so a second open reuses it. A page another
|
|
2310
|
+
* chat is driving is that chat's, and is not offered.
|
|
2311
|
+
* @param {string} url - address wanted.
|
|
2312
|
+
* @param {string} except - chat doing the asking.
|
|
2313
|
+
* @returns {SidecarPage|null} the page, or null.
|
|
2314
|
+
*/
|
|
2315
|
+
tabShowing(url, except) {
|
|
2316
|
+
const wanted = String(url ?? '')
|
|
2317
|
+
if (wanted === '' || wanted === 'about:blank') return null
|
|
2318
|
+
const held = new Set()
|
|
2319
|
+
for (const [who, set] of this.claims) {
|
|
2320
|
+
if (String(who) === String(except)) continue
|
|
2321
|
+
for (const target of set) held.add(String(target))
|
|
2322
|
+
}
|
|
2323
|
+
for (const page of [this.page, ...this.pages.values()]) {
|
|
2324
|
+
if (page === null || page === undefined || page.cdp?.closed === true) continue
|
|
2325
|
+
const target = String(page.targetId ?? '')
|
|
2326
|
+
if (target === '' || held.has(target)) continue
|
|
2327
|
+
if (samePlace(page.url, wanted)) return page
|
|
2328
|
+
}
|
|
2329
|
+
return null
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2277
2332
|
/**
|
|
2278
2333
|
* @param {string} id - chat id.
|
|
2279
2334
|
* @param {string} [url] - address to open in it.
|
|
2280
2335
|
* @returns {Promise<SidecarPage>} a new tab for that chat.
|
|
2281
2336
|
*/
|
|
2282
2337
|
async openTab(id, url) {
|
|
2338
|
+
this.expired?.delete(String(id))
|
|
2283
2339
|
const had = this.pageFor(id)
|
|
2284
2340
|
if (had !== null) {
|
|
2285
2341
|
if (url !== undefined && url !== '') await had.navigate(url)
|
|
2286
2342
|
return had
|
|
2287
2343
|
}
|
|
2344
|
+
/* One address, one page: a browser that is already showing where this chat
|
|
2345
|
+
wants to go hands that page over instead of a second copy of the same
|
|
2346
|
+
address, which nobody would be able to tell apart in the tab list. */
|
|
2347
|
+
const twin = this.tabShowing(url, id)
|
|
2348
|
+
if (twin !== null) {
|
|
2349
|
+
this.pages.set(String(id), twin)
|
|
2350
|
+
this.claim(String(id), twin.targetId)
|
|
2351
|
+
this.touch()
|
|
2352
|
+
this.rememberTab(String(id), String(twin.url ?? url))
|
|
2353
|
+
return twin
|
|
2354
|
+
}
|
|
2288
2355
|
if (this.page === null) await this.ensure()
|
|
2289
2356
|
const port = this.page?.port ?? 0 // ui-copy-ignore
|
|
2290
2357
|
if (port === 0) throw new Error('sidecar: no browser to open a tab in') // ui-copy-ignore
|
|
@@ -2646,7 +2713,25 @@ export class Sidecar {
|
|
|
2646
2713
|
*/
|
|
2647
2714
|
async tabs(session = '') {
|
|
2648
2715
|
const page = this.pageFor(session) ?? this.page
|
|
2649
|
-
|
|
2716
|
+
if (page === null) return []
|
|
2717
|
+
const rows = await page.tabs()
|
|
2718
|
+
/* One address is one row: the base page can be showing the very place a
|
|
2719
|
+
chat's tab already shows, and two identical lines read as two pages. A
|
|
2720
|
+
page a chat holds represents that address; an unheld twin is left out,
|
|
2721
|
+
and the rows that stay keep the browser's order, which is the index
|
|
2722
|
+
space the tab tools address their rows by. */
|
|
2723
|
+
const held = new Set()
|
|
2724
|
+
for (const set of this.claims.values()) for (const target of set) held.add(String(target))
|
|
2725
|
+
const seen = new Set()
|
|
2726
|
+
const kept = []
|
|
2727
|
+
for (const row of [...rows].filter((item) => held.has(item.targetId)).concat(rows.filter((item) => !held.has(item.targetId)))) {
|
|
2728
|
+
const key = placeKey(row.url)
|
|
2729
|
+
if (key !== '' && seen.has(key)) continue
|
|
2730
|
+
if (key !== '') seen.add(key)
|
|
2731
|
+
kept.push(row)
|
|
2732
|
+
}
|
|
2733
|
+
kept.sort((a, b) => rows.indexOf(a) - rows.indexOf(b))
|
|
2734
|
+
return kept
|
|
2650
2735
|
}
|
|
2651
2736
|
|
|
2652
2737
|
/**
|
|
@@ -3074,8 +3159,9 @@ export class Sidecar {
|
|
|
3074
3159
|
question about which site it used to be. */
|
|
3075
3160
|
const remembered = this.rememberedTab(id)
|
|
3076
3161
|
const page = id === '' ? null : this.pageFor(id)
|
|
3077
|
-
|
|
3078
|
-
return { url:
|
|
3162
|
+
const expired = this.expired?.has(id) === true
|
|
3163
|
+
if (page === null || page === undefined) return { url: '', title: '', viewers: 0, alive: false, expired, remembered } // ui-copy-ignore
|
|
3164
|
+
return { url: String(page.url ?? ''), title: String(page.title ?? ''), viewers: page.frames?.size ?? 0, alive: true, expired, remembered } // ui-copy-ignore
|
|
3079
3165
|
}
|
|
3080
3166
|
|
|
3081
3167
|
async state() {
|