dsh-inner-browser-use 0.34.99 → 0.35.1
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 +102 -17
- package/tools.js +30 -2
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.1",
|
|
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
|
/**
|
|
@@ -801,18 +826,17 @@ export class SidecarPage {
|
|
|
801
826
|
probe is only for the case where the click never made one, so it gets a
|
|
802
827
|
single short look instead of the old half-second of polling. */
|
|
803
828
|
if (target === null) {
|
|
804
|
-
/*
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
829
|
+
/* One look at the tab list answers the common case — a click that opened
|
|
830
|
+
nothing. When it says nothing, the announcement is the cheaper door:
|
|
831
|
+
a tab exists a moment before the browser lists it, so the wait ends on
|
|
832
|
+
the event instead of running out the window. */
|
|
833
|
+
target = await this.findPopup(foreign)
|
|
834
|
+
if (target === null) {
|
|
835
|
+
await this.waitPopup(Math.min(8, POPUP_GRACE_MS))
|
|
836
|
+
if (this.opened !== null && this.opened !== undefined && Date.now() - Number(this.opened.at ?? 0) < 2000) {
|
|
837
|
+
target = { targetId: String(this.opened.targetId), url: String(this.opened.url) }
|
|
812
838
|
}
|
|
813
|
-
|
|
814
|
-
if (left <= 0) break
|
|
815
|
-
await this.waitPopup(Math.min(8, left))
|
|
839
|
+
this.opened = null
|
|
816
840
|
}
|
|
817
841
|
}
|
|
818
842
|
if (target === null) {
|
|
@@ -1300,7 +1324,7 @@ export class SidecarPage {
|
|
|
1300
1324
|
/**
|
|
1301
1325
|
* Whether this picture repeats the one the pane is already showing.
|
|
1302
1326
|
* @param {string} jpeg - base64 JPEG of the new frame.
|
|
1303
|
-
* @returns {boolean} true when sending it would
|
|
1327
|
+
* @returns {boolean} true when sending it would cost bytes.
|
|
1304
1328
|
*/
|
|
1305
1329
|
repeats(jpeg, gap = 1000) {
|
|
1306
1330
|
const prev = this.lastShot
|
|
@@ -1979,6 +2003,8 @@ export class Sidecar {
|
|
|
1979
2003
|
/* A tab being opened for a chat: a second call for the same chat waits for
|
|
1980
2004
|
it instead of opening a twin. */
|
|
1981
2005
|
this.opening = new Map()
|
|
2006
|
+
/* Chats whose tab was reclaimed: the column is told so it can go away. */
|
|
2007
|
+
this.expired = new Map()
|
|
1982
2008
|
/* A measurement or a test may want a profile of its own; the person's
|
|
1983
2009
|
browser keeps the default one. */
|
|
1984
2010
|
this.profile = String(options.profile ?? 'default')
|
|
@@ -2157,6 +2183,7 @@ export class Sidecar {
|
|
|
2157
2183
|
*/
|
|
2158
2184
|
async tabFor(session, url) {
|
|
2159
2185
|
const id = String(session ?? '')
|
|
2186
|
+
this.expired?.delete(id)
|
|
2160
2187
|
const had = this.pageFor(id)
|
|
2161
2188
|
if (had !== null) {
|
|
2162
2189
|
if (url !== undefined && url !== '') await had.navigate(url)
|
|
@@ -2268,23 +2295,62 @@ export class Sidecar {
|
|
|
2268
2295
|
if (page === this.page) continue
|
|
2269
2296
|
if (now - Number(page.touched ?? 0) < TAB_IDLE_MS) continue
|
|
2270
2297
|
this.pages.delete(id)
|
|
2298
|
+
/* The column is told the tab is gone, so a chat coming back to the front
|
|
2299
|
+
does not re-expand a pane for a page that already went away. */
|
|
2300
|
+
;(this.expired ??= new Map()).set(String(id), Date.now())
|
|
2271
2301
|
await page.stopCast().catch(() => {})
|
|
2272
2302
|
await fetch(`http://127.0.0.1:${String(page.port)}/json/close/${target}`, { signal: AbortSignal.timeout(800) }).catch(() => {}) // ui-copy-ignore
|
|
2273
2303
|
this.note(`closed an idle tab — its address is kept for the chat`)
|
|
2274
2304
|
}
|
|
2275
2305
|
}
|
|
2276
2306
|
|
|
2307
|
+
/**
|
|
2308
|
+
* A page already at an address, so a second open reuses it. A page another
|
|
2309
|
+
* chat is driving is that chat's, and is not offered.
|
|
2310
|
+
* @param {string} url - address wanted.
|
|
2311
|
+
* @param {string} except - chat doing the asking.
|
|
2312
|
+
* @returns {SidecarPage|null} the page, or null.
|
|
2313
|
+
*/
|
|
2314
|
+
tabShowing(url, except) {
|
|
2315
|
+
const wanted = String(url ?? '')
|
|
2316
|
+
if (wanted === '' || wanted === 'about:blank') return null
|
|
2317
|
+
const held = new Set()
|
|
2318
|
+
for (const [who, set] of this.claims) {
|
|
2319
|
+
if (String(who) === String(except)) continue
|
|
2320
|
+
for (const target of set) held.add(String(target))
|
|
2321
|
+
}
|
|
2322
|
+
for (const page of [this.page, ...this.pages.values()]) {
|
|
2323
|
+
if (page === null || page === undefined || page.cdp?.closed === true) continue
|
|
2324
|
+
const target = String(page.targetId ?? '')
|
|
2325
|
+
if (target === '' || held.has(target)) continue
|
|
2326
|
+
if (samePlace(page.url, wanted)) return page
|
|
2327
|
+
}
|
|
2328
|
+
return null
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2277
2331
|
/**
|
|
2278
2332
|
* @param {string} id - chat id.
|
|
2279
2333
|
* @param {string} [url] - address to open in it.
|
|
2280
2334
|
* @returns {Promise<SidecarPage>} a new tab for that chat.
|
|
2281
2335
|
*/
|
|
2282
2336
|
async openTab(id, url) {
|
|
2337
|
+
this.expired?.delete(String(id))
|
|
2283
2338
|
const had = this.pageFor(id)
|
|
2284
2339
|
if (had !== null) {
|
|
2285
2340
|
if (url !== undefined && url !== '') await had.navigate(url)
|
|
2286
2341
|
return had
|
|
2287
2342
|
}
|
|
2343
|
+
/* One address, one page: a browser that is already showing where this chat
|
|
2344
|
+
wants to go hands that page over instead of a second copy of the same
|
|
2345
|
+
address, which nobody would be able to tell apart in the tab list. */
|
|
2346
|
+
const twin = this.tabShowing(url, id)
|
|
2347
|
+
if (twin !== null) {
|
|
2348
|
+
this.pages.set(String(id), twin)
|
|
2349
|
+
this.claim(String(id), twin.targetId)
|
|
2350
|
+
this.touch()
|
|
2351
|
+
this.rememberTab(String(id), String(twin.url ?? url))
|
|
2352
|
+
return twin
|
|
2353
|
+
}
|
|
2288
2354
|
if (this.page === null) await this.ensure()
|
|
2289
2355
|
const port = this.page?.port ?? 0 // ui-copy-ignore
|
|
2290
2356
|
if (port === 0) throw new Error('sidecar: no browser to open a tab in') // ui-copy-ignore
|
|
@@ -2646,7 +2712,25 @@ export class Sidecar {
|
|
|
2646
2712
|
*/
|
|
2647
2713
|
async tabs(session = '') {
|
|
2648
2714
|
const page = this.pageFor(session) ?? this.page
|
|
2649
|
-
|
|
2715
|
+
if (page === null) return []
|
|
2716
|
+
const rows = await page.tabs()
|
|
2717
|
+
/* One address is one row: the base page can be showing the very place a
|
|
2718
|
+
chat's tab already shows, and two identical lines read as two pages. A
|
|
2719
|
+
page a chat holds represents that address; an unheld twin is left out,
|
|
2720
|
+
and the rows that stay keep the browser's order, which is the index
|
|
2721
|
+
space the tab tools address their rows by. */
|
|
2722
|
+
const held = new Set()
|
|
2723
|
+
for (const set of this.claims.values()) for (const target of set) held.add(String(target))
|
|
2724
|
+
const seen = new Set()
|
|
2725
|
+
const kept = []
|
|
2726
|
+
for (const row of [...rows].filter((item) => held.has(item.targetId)).concat(rows.filter((item) => !held.has(item.targetId)))) {
|
|
2727
|
+
const key = placeKey(row.url)
|
|
2728
|
+
if (key !== '' && seen.has(key)) continue
|
|
2729
|
+
if (key !== '') seen.add(key)
|
|
2730
|
+
kept.push(row)
|
|
2731
|
+
}
|
|
2732
|
+
kept.sort((a, b) => rows.indexOf(a) - rows.indexOf(b))
|
|
2733
|
+
return kept
|
|
2650
2734
|
}
|
|
2651
2735
|
|
|
2652
2736
|
/**
|
|
@@ -3074,8 +3158,9 @@ export class Sidecar {
|
|
|
3074
3158
|
question about which site it used to be. */
|
|
3075
3159
|
const remembered = this.rememberedTab(id)
|
|
3076
3160
|
const page = id === '' ? null : this.pageFor(id)
|
|
3077
|
-
|
|
3078
|
-
return { url:
|
|
3161
|
+
const expired = this.expired?.has(id) === true
|
|
3162
|
+
if (page === null || page === undefined) return { url: '', title: '', viewers: 0, alive: false, expired, remembered } // ui-copy-ignore
|
|
3163
|
+
return { url: String(page.url ?? ''), title: String(page.title ?? ''), viewers: page.frames?.size ?? 0, alive: true, expired, remembered } // ui-copy-ignore
|
|
3079
3164
|
}
|
|
3080
3165
|
|
|
3081
3166
|
async state() {
|
package/tools.js
CHANGED
|
@@ -310,7 +310,17 @@ export async function outcome(page, before) {
|
|
|
310
310
|
const [wasTitle, wasUrl, wasText, wasNodes] = before.split('|')
|
|
311
311
|
const [nowTitle, nowUrl, nowText, nowNodes] = after.split('|')
|
|
312
312
|
if (after === before) return ' · no visible change'
|
|
313
|
-
if (nowUrl !== wasUrl)
|
|
313
|
+
if (nowUrl !== wasUrl) {
|
|
314
|
+
/* The brief on the same line already names the address this step went to.
|
|
315
|
+
What it cannot say is whether the page moved within itself, so a jump to
|
|
316
|
+
another spot on the same address is said as the spot alone. */
|
|
317
|
+
const was = String(wasUrl ?? '')
|
|
318
|
+
const now = String(nowUrl ?? '')
|
|
319
|
+
const base = (value) => (value.includes('#') ? value.slice(0, value.indexOf('#')) : value)
|
|
320
|
+
const spot = now.includes('#') ? now.slice(now.indexOf('#')) : ''
|
|
321
|
+
if (spot !== '' && base(was) === base(now)) return ` · ${spot.slice(0, 120)}`
|
|
322
|
+
return ' · new page'
|
|
323
|
+
}
|
|
314
324
|
const words = Math.abs(Number(nowText ?? 0) - Number(wasText ?? 0))
|
|
315
325
|
const nodes = Math.abs(Number(nowNodes ?? 0) - Number(wasNodes ?? 0))
|
|
316
326
|
if (nowTitle !== wasTitle) return ' · page moved on'
|
|
@@ -352,9 +362,27 @@ export function withoutSeenAddress(page, url, text) {
|
|
|
352
362
|
return body.slice(first.length + 1)
|
|
353
363
|
}
|
|
354
364
|
|
|
365
|
+
/** The focus line repeats the control the call named: say the name once. */
|
|
366
|
+
export function dropSameFocus(line, args) {
|
|
367
|
+
const squeezed = (value) => String(value ?? '').toLowerCase().replace(/\s+/g, '')
|
|
368
|
+
const named = squeezed(args?.text ?? args?.name ?? args?.ref ?? '')
|
|
369
|
+
if (named === '') return line
|
|
370
|
+
const at = line.indexOf(' · focus ')
|
|
371
|
+
if (at < 0) return line
|
|
372
|
+
const head = line.slice(0, at)
|
|
373
|
+
const parts = line.slice(at + ' · focus '.length).split(' · ')
|
|
374
|
+
const said = String(parts[0] ?? '')
|
|
375
|
+
/* A field carries a value the words do not: that part is news, so it stays. */
|
|
376
|
+
if (said.includes(' = ')) return line
|
|
377
|
+
const label = squeezed(said)
|
|
378
|
+
if (label !== named && !label.includes(named) && !named.includes(label)) return line
|
|
379
|
+
const rest = parts.slice(1)
|
|
380
|
+
return rest.length === 0 ? head : `${head} · ${rest.join(' · ')}`
|
|
381
|
+
}
|
|
382
|
+
|
|
355
383
|
async function acted(page, args, look, label) {
|
|
356
384
|
if (args?.read === true) return await look()
|
|
357
|
-
return `ok ${label} · ${oncePerPage(page, await brief(page))}`
|
|
385
|
+
return `ok ${label} · ${dropSameFocus(oncePerPage(page, await brief(page)), args)}`
|
|
358
386
|
}
|
|
359
387
|
|
|
360
388
|
/**
|