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 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, and
678
- again until the browser says it took it: a request that arrives while
679
- the page is still starting is refused, and a pane that gives up then
680
- keeps a letterboxed picture for as long as it stays open. */
681
- let live = true
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
- timer = setTimeout(send, 250)
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 just closed: the browser itself
741
- is put away a minute later rather than a quarter of an hour, unless an
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.34.99",
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 just seen. A 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
- return (Array.isArray(list) ? list : [])
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
- /* Look, wait for the announcement, look again: a new tab is announced
805
- before the browser lists it, so one look is not enough. */
806
- const deadline = Date.now() + POPUP_GRACE_MS
807
- while (target === null && Date.now() < deadline) {
808
- const found = await this.findPopup(foreign)
809
- if (found !== null) {
810
- target = found
811
- break
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
- const left = deadline - Date.now()
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 just cost bytes.
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
- return page === null ? [] : page.tabs()
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
- if (page === null || page === undefined) return { url: '', title: '', viewers: 0, alive: false, remembered } // ui-copy-ignore
3078
- return { url: String(page.url ?? ''), title: String(page.title ?? ''), viewers: page.frames?.size ?? 0, alive: true, remembered } // ui-copy-ignore
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) return ` · navigated to ${String(nowUrl).slice(0, 300)}`
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
  /**