dsh-inner-browser-use 0.34.98 → 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 +58 -36
- package/index.js +39 -5
- package/package.json +1 -1
- package/sidecar.js +111 -8
package/client.js
CHANGED
|
@@ -73,7 +73,7 @@ window.__ModuleLoader__.load({
|
|
|
73
73
|
'.dshib-side-win[data-on="1"]{background:var(--dsw-alias-bg-subtle);color:var(--dsw-alias-label-primary)}',
|
|
74
74
|
'.dshib-side-win > span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
75
75
|
'.dshib-side-stage{flex:1 1 auto;position:relative;min-width:0;min-height:0;display:flex;align-items:center;justify-content:center;overflow:hidden;background:var(--dsw-alias-bg-base);outline:none;cursor:default}',
|
|
76
|
-
'.dshib-side-frame{display:block;user-select:none;-webkit-user-drag:none;cursor:default}',
|
|
76
|
+
'.dshib-side-frame{position:absolute;inset:0;display:block;width:100%;height:100%;object-fit:contain;user-select:none;-webkit-user-drag:none;cursor:default}',
|
|
77
77
|
'.dshib-side-ime{position:absolute;width:1px;height:1px;padding:0;margin:0;border:0;resize:none;overflow:hidden;opacity:0;pointer-events:none}',
|
|
78
78
|
/* The black arrow is the agent's pointer, not the person's cursor: both are
|
|
79
79
|
on screen at once, and moving the mouse never takes the arrow over. */
|
|
@@ -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)
|
|
@@ -645,9 +662,27 @@ window.__ModuleLoader__.load({
|
|
|
645
662
|
.catch(() => {})
|
|
646
663
|
}
|
|
647
664
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
665
|
+
/* Where the page actually got drawn inside the column: the frame fills
|
|
666
|
+
the stage and the picture sits inside it, so the answer comes from the
|
|
667
|
+
element that is on screen rather than from the last size remembered.
|
|
668
|
+
A stale measure can then never shrink the picture to a thumbnail. */
|
|
669
|
+
const frameEl = React.useRef(null)
|
|
670
|
+
const picture = () => {
|
|
671
|
+
const at = frameEl.current
|
|
672
|
+
const areaEl = stage.current
|
|
673
|
+
if (at === null || at === undefined || areaEl === null || areaEl === undefined) return null
|
|
674
|
+
if (!(view.w > 0) || !(view.h > 0)) return null
|
|
675
|
+
const a = at.getBoundingClientRect()
|
|
676
|
+
const b = areaEl.getBoundingClientRect()
|
|
677
|
+
if (!(a.width > 1) || !(a.height > 1)) return null
|
|
678
|
+
const scale = Math.min(a.width / view.w, a.height / view.h)
|
|
679
|
+
if (!(scale > 0)) return null
|
|
680
|
+
return {
|
|
681
|
+
scale,
|
|
682
|
+
x: a.left - b.left + (a.width - view.w * scale) / 2,
|
|
683
|
+
y: a.top - b.top + (a.height - view.h * scale) / 2,
|
|
684
|
+
}
|
|
685
|
+
}
|
|
651
686
|
|
|
652
687
|
/* The column tells the page how big it is, a beat after the resize stops:
|
|
653
688
|
a page that relayouts on every pointer step would never settle. */
|
|
@@ -656,15 +691,11 @@ window.__ModuleLoader__.load({
|
|
|
656
691
|
/* The page already has that shape: a resize that lands between two
|
|
657
692
|
viewport sizes would only cost a relayout and another frame. */
|
|
658
693
|
if (Math.abs(Number(view?.w ?? 0) - size.w) < 1 && Math.abs(Number(view?.h ?? 0) - size.h) < 1) return undefined
|
|
659
|
-
/* The page is told the column's size a beat after the resize stops
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
let tries = 0
|
|
665
|
-
let timer = 0
|
|
666
|
-
const send = () => {
|
|
667
|
-
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(() => {
|
|
668
699
|
fetch(`${ROUTE}/sidecar/viewport`, {
|
|
669
700
|
method: 'POST',
|
|
670
701
|
headers: { 'content-type': 'application/json' },
|
|
@@ -672,30 +703,17 @@ window.__ModuleLoader__.load({
|
|
|
672
703
|
})
|
|
673
704
|
.then((answer) => (answer.ok === true ? answer.json() : { ok: false }))
|
|
674
705
|
.then((said) => {
|
|
675
|
-
if (!live) return
|
|
676
706
|
/* The browser answers with the shape it actually took: drawing
|
|
677
707
|
the picture at that size fills the pane even when the page
|
|
678
708
|
refused the exact one asked for. */
|
|
679
709
|
const took = said?.view ?? null
|
|
680
710
|
if (said?.ok === true && took !== null && Number(took.width) > 0 && Number(took.height) > 0) {
|
|
681
711
|
setView({ w: Math.round(Number(took.width)), h: Math.round(Number(took.height)) })
|
|
682
|
-
return
|
|
683
712
|
}
|
|
684
|
-
if (said?.ok === true || tries >= 5) return
|
|
685
|
-
tries += 1
|
|
686
|
-
timer = setTimeout(send, 400)
|
|
687
713
|
})
|
|
688
|
-
.catch(() => {
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
timer = setTimeout(send, 600)
|
|
692
|
-
})
|
|
693
|
-
}
|
|
694
|
-
timer = setTimeout(send, 250)
|
|
695
|
-
return () => {
|
|
696
|
-
live = false
|
|
697
|
-
clearTimeout(timer)
|
|
698
|
-
}
|
|
714
|
+
.catch(() => {})
|
|
715
|
+
}, 250)
|
|
716
|
+
return () => clearTimeout(timer)
|
|
699
717
|
}, [mine, size.w, size.h, view?.w, view?.h])
|
|
700
718
|
|
|
701
719
|
const who = String(props.sessionId ?? '')
|
|
@@ -729,10 +747,12 @@ window.__ModuleLoader__.load({
|
|
|
729
747
|
}
|
|
730
748
|
const toView = (event) => {
|
|
731
749
|
const el = stage.current
|
|
732
|
-
if (el === null
|
|
750
|
+
if (el === null) return null
|
|
751
|
+
const spot = picture()
|
|
752
|
+
if (spot === null) return null
|
|
733
753
|
const box = el.getBoundingClientRect()
|
|
734
|
-
const x = (event.clientX - box.left -
|
|
735
|
-
const y = (event.clientY - box.top -
|
|
754
|
+
const x = (event.clientX - box.left - spot.x) / spot.scale
|
|
755
|
+
const y = (event.clientY - box.top - spot.y) / spot.scale
|
|
736
756
|
if (x < 0 || y < 0 || x > view.w || y > view.h) return null
|
|
737
757
|
return { x: Math.round(x), y: Math.round(y) }
|
|
738
758
|
}
|
|
@@ -828,6 +848,7 @@ window.__ModuleLoader__.load({
|
|
|
828
848
|
h('div', { className: 'dshib-side-note' }, L('side.foreign')))
|
|
829
849
|
}
|
|
830
850
|
const pointer = person === true || hand === true ? null : ptr
|
|
851
|
+
const spot = pointer === null ? null : (picture() ?? { scale: 1, x: 0, y: 0 })
|
|
831
852
|
const noteText = phase === 'failed' ? note || L('side.failed') : phase === 'live' ? L('side.empty') : L('side.connecting')
|
|
832
853
|
return h('div', { className: 'dshib-side' },
|
|
833
854
|
h('div', { className: 'dshib-side-bar' },
|
|
@@ -894,7 +915,7 @@ window.__ModuleLoader__.load({
|
|
|
894
915
|
tabIndex: 0,
|
|
895
916
|
spellCheck: false,
|
|
896
917
|
'aria-label': L('side.type'),
|
|
897
|
-
style: pointer === null ? undefined : { left: `${
|
|
918
|
+
style: pointer === null || spot === null ? undefined : { left: `${spot.x + pointer.x * spot.scale}px`, top: `${spot.y + pointer.y * spot.scale}px` },
|
|
898
919
|
onCompositionStart: () => {
|
|
899
920
|
composing.current = true
|
|
900
921
|
},
|
|
@@ -913,9 +934,10 @@ window.__ModuleLoader__.load({
|
|
|
913
934
|
}),
|
|
914
935
|
frame === null ? null : h('img', {
|
|
915
936
|
className: 'dshib-side-frame',
|
|
937
|
+
ref: frameEl,
|
|
916
938
|
src: `data:image/jpeg;base64,${frame.jpeg}`,
|
|
917
|
-
width: Math.max(1, Math.round(view.w
|
|
918
|
-
height: Math.max(1, Math.round(view.h
|
|
939
|
+
width: Math.max(1, Math.round(view.w)),
|
|
940
|
+
height: Math.max(1, Math.round(view.h)),
|
|
919
941
|
alt: '', // ui-copy-ignore
|
|
920
942
|
draggable: false,
|
|
921
943
|
}),
|
|
@@ -923,7 +945,7 @@ window.__ModuleLoader__.load({
|
|
|
923
945
|
className: 'dshib-side-ptr',
|
|
924
946
|
src: SIDE_POINTER, // ui-copy-ignore
|
|
925
947
|
alt: '', // ui-copy-ignore
|
|
926
|
-
style: { left: `${
|
|
948
|
+
style: spot === null ? undefined : { left: `${spot.x + pointer.x * spot.scale}px`, top: `${spot.y + pointer.y * spot.scale}px` },
|
|
927
949
|
})))
|
|
928
950
|
}
|
|
929
951
|
},
|
package/index.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* learns no port and no secret. // ui-copy-ignore
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
23
|
+
import { appendFileSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
|
|
24
24
|
import { homedir } from 'node:os'
|
|
25
25
|
import { join } from 'node:path'
|
|
26
26
|
import { Sidecar } from './sidecar.js?v=1'
|
|
@@ -50,6 +50,27 @@ function writeMirror(value) {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/** The note stops growing: past this size the oldest half goes, so a long-lived run never fills the disk. */
|
|
54
|
+
const NOTES_MAX = 256 * 1024
|
|
55
|
+
const NOTES_KEEP = 128 * 1024
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Add one line to the mirror's own note, keeping the file bounded.
|
|
59
|
+
* @param {string} line - what happened, one line.
|
|
60
|
+
*/
|
|
61
|
+
export function note(line) {
|
|
62
|
+
const file = join(STATE_DIR, 'asks.log')
|
|
63
|
+
try {
|
|
64
|
+
mkdirSync(STATE_DIR, { recursive: true })
|
|
65
|
+
appendFileSync(file, `${new Date().toISOString()} ${line}\n`)
|
|
66
|
+
if (statSync(file).size <= NOTES_MAX) return
|
|
67
|
+
const whole = readFileSync(file, 'utf8')
|
|
68
|
+
writeFileSync(file, whole.slice(whole.length - NOTES_KEEP).replace(/^[^\n]*\n/, ''))
|
|
69
|
+
} catch {
|
|
70
|
+
/* a note that cannot be written must not take the mirror down */
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
53
74
|
/** Route prefix owned by this plugin. Never under `/api`, which is the Host's own channel. */ // ui-copy-ignore
|
|
54
75
|
export const ROUTE = '/dsh-inner-browser-use'
|
|
55
76
|
|
|
@@ -259,6 +280,9 @@ export function cwdOf(execution) {
|
|
|
259
280
|
|
|
260
281
|
export function needsTab(chatState, chat, owner) {
|
|
261
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
|
|
262
286
|
return chatState?.alive !== true
|
|
263
287
|
}
|
|
264
288
|
|
|
@@ -336,7 +360,7 @@ export function apply(ctx) {
|
|
|
336
360
|
if (due !== true) return
|
|
337
361
|
last = Date.now()
|
|
338
362
|
bridge.want(owner, Date.now(), 'keepalive')
|
|
339
|
-
|
|
363
|
+
note(`asked ${owner.slice(-8)} to draw the mirror again`)
|
|
340
364
|
} catch {}
|
|
341
365
|
}, 4000)
|
|
342
366
|
timer.unref?.()
|
|
@@ -500,7 +524,7 @@ export function apply(ctx) {
|
|
|
500
524
|
called open again: the mirror would come back empty in a chat that
|
|
501
525
|
never closed it. The chat that owned the browser asks for its state, so
|
|
502
526
|
answering honestly means picking the browser up first. */
|
|
503
|
-
if (shouldAdoptOnState(said, chat, bridge.owner)) {
|
|
527
|
+
if (shouldAdoptOnState(said, chat, bridge.owner) && mineChat?.expired !== true) {
|
|
504
528
|
try {
|
|
505
529
|
await sidecar.ensure(adoptOptions(chat, sidecar.rememberedTab?.(chat)))
|
|
506
530
|
said = await sidecar.state()
|
|
@@ -511,7 +535,7 @@ export function apply(ctx) {
|
|
|
511
535
|
if (Date.now() - before > 2000) {
|
|
512
536
|
lastAsk.set(chat, Date.now())
|
|
513
537
|
try {
|
|
514
|
-
|
|
538
|
+
note(`${chat.slice(-8)} alive=${String(said?.alive === true)} viewers=${String(said?.viewers ?? 0)}`)
|
|
515
539
|
} catch {}
|
|
516
540
|
}
|
|
517
541
|
}
|
|
@@ -715,7 +739,17 @@ export function apply(ctx) {
|
|
|
715
739
|
const body = await readBody(req).then((raw) => JSON.parse(raw || '{}')).catch(() => ({}))
|
|
716
740
|
const who = acting(body)
|
|
717
741
|
if (who === '') return
|
|
718
|
-
|
|
742
|
+
const forgot = bridge.forget(who)
|
|
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
|
|
745
|
+
agent asks for it again in the meantime. */
|
|
746
|
+
try {
|
|
747
|
+
if (sidecar.watched() === 0) {
|
|
748
|
+
sidecar.touch(REST_MS)
|
|
749
|
+
note('said ' + who.slice(-8) + ' closed the mirror - browser rests in a minute')
|
|
750
|
+
}
|
|
751
|
+
} catch { /* a browser that never started needs no rest timer */ }
|
|
752
|
+
json(200, { ok: true, forgot })
|
|
719
753
|
return
|
|
720
754
|
}
|
|
721
755
|
|
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.
|
|
@@ -95,6 +95,12 @@ export function nextQuality(current, base64) {
|
|
|
95
95
|
|
|
96
96
|
/** Idle lifetime, so a forgotten sidecar does not sit on a login forever. */
|
|
97
97
|
const IDLE_MS = 15 * 60_000
|
|
98
|
+
/* A pane the person closed is a stronger signal than a pane nobody touched: the
|
|
99
|
+
browser is put away a minute later unless someone asks for it again. A whole
|
|
100
|
+
Chrome with its renderers is a few hundred megabytes, and leaving it up for a
|
|
101
|
+
quarter of an hour after a deliberate close is exactly the cost they did not
|
|
102
|
+
want. */
|
|
103
|
+
const REST_MS = 60_000
|
|
98
104
|
/* One browser serves every chat, but a chat that is not on screen need not keep
|
|
99
105
|
a full renderer alive for ever: past this many chats the oldest unwatched tab
|
|
100
106
|
is closed, and its address brings it back on the next visit. */
|
|
@@ -472,6 +478,30 @@ class Cdp {
|
|
|
472
478
|
* Input is real CDP input: the page sees the same events a person's trackpad
|
|
473
479
|
* produces, which is what makes sites that reject scripted clicks work.
|
|
474
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
|
+
|
|
475
505
|
export class SidecarPage {
|
|
476
506
|
/** @param {Cdp} cdp - the page session. @param {string} profile - profile name. */
|
|
477
507
|
constructor(cdp, profile, port, hold = () => 0) { // ui-copy-ignore
|
|
@@ -768,9 +798,10 @@ export class SidecarPage {
|
|
|
768
798
|
.then((response) => (response.ok ? response.json() : []))
|
|
769
799
|
.catch(() => [])
|
|
770
800
|
const current = await this.cdp.send('Target.getTargetInfo').then((info) => String(info?.targetInfo?.targetId ?? '')).catch(() => '')
|
|
771
|
-
|
|
801
|
+
const rows = (Array.isArray(list) ? list : [])
|
|
772
802
|
.filter((item) => item.type === 'page' && item.webSocketDebuggerUrl !== undefined)
|
|
773
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
|
|
774
805
|
}
|
|
775
806
|
|
|
776
807
|
/**
|
|
@@ -1294,7 +1325,7 @@ export class SidecarPage {
|
|
|
1294
1325
|
/**
|
|
1295
1326
|
* Whether this picture repeats the one the pane is already showing.
|
|
1296
1327
|
* @param {string} jpeg - base64 JPEG of the new frame.
|
|
1297
|
-
* @returns {boolean} true when sending it would
|
|
1328
|
+
* @returns {boolean} true when sending it would cost bytes.
|
|
1298
1329
|
*/
|
|
1299
1330
|
repeats(jpeg, gap = 1000) {
|
|
1300
1331
|
const prev = this.lastShot
|
|
@@ -1973,6 +2004,8 @@ export class Sidecar {
|
|
|
1973
2004
|
/* A tab being opened for a chat: a second call for the same chat waits for
|
|
1974
2005
|
it instead of opening a twin. */
|
|
1975
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()
|
|
1976
2009
|
/* A measurement or a test may want a profile of its own; the person's
|
|
1977
2010
|
browser keeps the default one. */
|
|
1978
2011
|
this.profile = String(options.profile ?? 'default')
|
|
@@ -2151,6 +2184,7 @@ export class Sidecar {
|
|
|
2151
2184
|
*/
|
|
2152
2185
|
async tabFor(session, url) {
|
|
2153
2186
|
const id = String(session ?? '')
|
|
2187
|
+
this.expired?.delete(id)
|
|
2154
2188
|
const had = this.pageFor(id)
|
|
2155
2189
|
if (had !== null) {
|
|
2156
2190
|
if (url !== undefined && url !== '') await had.navigate(url)
|
|
@@ -2262,23 +2296,62 @@ export class Sidecar {
|
|
|
2262
2296
|
if (page === this.page) continue
|
|
2263
2297
|
if (now - Number(page.touched ?? 0) < TAB_IDLE_MS) continue
|
|
2264
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())
|
|
2265
2302
|
await page.stopCast().catch(() => {})
|
|
2266
2303
|
await fetch(`http://127.0.0.1:${String(page.port)}/json/close/${target}`, { signal: AbortSignal.timeout(800) }).catch(() => {}) // ui-copy-ignore
|
|
2267
2304
|
this.note(`closed an idle tab — its address is kept for the chat`)
|
|
2268
2305
|
}
|
|
2269
2306
|
}
|
|
2270
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
|
+
|
|
2271
2332
|
/**
|
|
2272
2333
|
* @param {string} id - chat id.
|
|
2273
2334
|
* @param {string} [url] - address to open in it.
|
|
2274
2335
|
* @returns {Promise<SidecarPage>} a new tab for that chat.
|
|
2275
2336
|
*/
|
|
2276
2337
|
async openTab(id, url) {
|
|
2338
|
+
this.expired?.delete(String(id))
|
|
2277
2339
|
const had = this.pageFor(id)
|
|
2278
2340
|
if (had !== null) {
|
|
2279
2341
|
if (url !== undefined && url !== '') await had.navigate(url)
|
|
2280
2342
|
return had
|
|
2281
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
|
+
}
|
|
2282
2355
|
if (this.page === null) await this.ensure()
|
|
2283
2356
|
const port = this.page?.port ?? 0 // ui-copy-ignore
|
|
2284
2357
|
if (port === 0) throw new Error('sidecar: no browser to open a tab in') // ui-copy-ignore
|
|
@@ -2640,7 +2713,25 @@ export class Sidecar {
|
|
|
2640
2713
|
*/
|
|
2641
2714
|
async tabs(session = '') {
|
|
2642
2715
|
const page = this.pageFor(session) ?? this.page
|
|
2643
|
-
|
|
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
|
|
2644
2735
|
}
|
|
2645
2736
|
|
|
2646
2737
|
/**
|
|
@@ -2836,7 +2927,18 @@ export class Sidecar {
|
|
|
2836
2927
|
}
|
|
2837
2928
|
}
|
|
2838
2929
|
|
|
2839
|
-
|
|
2930
|
+
/** @returns {number} how many panes are watching a picture right now. */
|
|
2931
|
+
watched() {
|
|
2932
|
+
let n = this.page?.frames?.size ?? 0
|
|
2933
|
+
for (const page of this.pages?.values() ?? []) n += page.frames?.size ?? 0
|
|
2934
|
+
return n
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
/**
|
|
2938
|
+
* Starts the idle clock again.
|
|
2939
|
+
* @param {number} [ms] - how long a browser nobody watches may stay up.
|
|
2940
|
+
*/
|
|
2941
|
+
touch(ms = IDLE_MS) {
|
|
2840
2942
|
if (this.idle !== null) clearTimeout(this.idle)
|
|
2841
2943
|
this.idle = setTimeout(() => {
|
|
2842
2944
|
this.idle = null
|
|
@@ -2849,7 +2951,7 @@ export class Sidecar {
|
|
|
2849
2951
|
return
|
|
2850
2952
|
}
|
|
2851
2953
|
void this.shutdown()
|
|
2852
|
-
},
|
|
2954
|
+
}, ms)
|
|
2853
2955
|
this.idle.unref?.()
|
|
2854
2956
|
}
|
|
2855
2957
|
|
|
@@ -3057,8 +3159,9 @@ export class Sidecar {
|
|
|
3057
3159
|
question about which site it used to be. */
|
|
3058
3160
|
const remembered = this.rememberedTab(id)
|
|
3059
3161
|
const page = id === '' ? null : this.pageFor(id)
|
|
3060
|
-
|
|
3061
|
-
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
|
|
3062
3165
|
}
|
|
3063
3166
|
|
|
3064
3167
|
async state() {
|