dsh-inner-browser-use 0.34.97 → 0.34.99
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 +118 -22
- package/index.js +50 -12
- package/package.json +1 -1
- package/sidecar.js +19 -2
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. */
|
|
@@ -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)
|
|
@@ -571,9 +645,27 @@ window.__ModuleLoader__.load({
|
|
|
571
645
|
.catch(() => {})
|
|
572
646
|
}
|
|
573
647
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
648
|
+
/* Where the page actually got drawn inside the column: the frame fills
|
|
649
|
+
the stage and the picture sits inside it, so the answer comes from the
|
|
650
|
+
element that is on screen rather than from the last size remembered.
|
|
651
|
+
A stale measure can then never shrink the picture to a thumbnail. */
|
|
652
|
+
const frameEl = React.useRef(null)
|
|
653
|
+
const picture = () => {
|
|
654
|
+
const at = frameEl.current
|
|
655
|
+
const areaEl = stage.current
|
|
656
|
+
if (at === null || at === undefined || areaEl === null || areaEl === undefined) return null
|
|
657
|
+
if (!(view.w > 0) || !(view.h > 0)) return null
|
|
658
|
+
const a = at.getBoundingClientRect()
|
|
659
|
+
const b = areaEl.getBoundingClientRect()
|
|
660
|
+
if (!(a.width > 1) || !(a.height > 1)) return null
|
|
661
|
+
const scale = Math.min(a.width / view.w, a.height / view.h)
|
|
662
|
+
if (!(scale > 0)) return null
|
|
663
|
+
return {
|
|
664
|
+
scale,
|
|
665
|
+
x: a.left - b.left + (a.width - view.w * scale) / 2,
|
|
666
|
+
y: a.top - b.top + (a.height - view.h * scale) / 2,
|
|
667
|
+
}
|
|
668
|
+
}
|
|
577
669
|
|
|
578
670
|
/* The column tells the page how big it is, a beat after the resize stops:
|
|
579
671
|
a page that relayouts on every pointer step would never settle. */
|
|
@@ -655,10 +747,12 @@ window.__ModuleLoader__.load({
|
|
|
655
747
|
}
|
|
656
748
|
const toView = (event) => {
|
|
657
749
|
const el = stage.current
|
|
658
|
-
if (el === null
|
|
750
|
+
if (el === null) return null
|
|
751
|
+
const spot = picture()
|
|
752
|
+
if (spot === null) return null
|
|
659
753
|
const box = el.getBoundingClientRect()
|
|
660
|
-
const x = (event.clientX - box.left -
|
|
661
|
-
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
|
|
662
756
|
if (x < 0 || y < 0 || x > view.w || y > view.h) return null
|
|
663
757
|
return { x: Math.round(x), y: Math.round(y) }
|
|
664
758
|
}
|
|
@@ -754,6 +848,7 @@ window.__ModuleLoader__.load({
|
|
|
754
848
|
h('div', { className: 'dshib-side-note' }, L('side.foreign')))
|
|
755
849
|
}
|
|
756
850
|
const pointer = person === true || hand === true ? null : ptr
|
|
851
|
+
const spot = pointer === null ? null : (picture() ?? { scale: 1, x: 0, y: 0 })
|
|
757
852
|
const noteText = phase === 'failed' ? note || L('side.failed') : phase === 'live' ? L('side.empty') : L('side.connecting')
|
|
758
853
|
return h('div', { className: 'dshib-side' },
|
|
759
854
|
h('div', { className: 'dshib-side-bar' },
|
|
@@ -820,7 +915,7 @@ window.__ModuleLoader__.load({
|
|
|
820
915
|
tabIndex: 0,
|
|
821
916
|
spellCheck: false,
|
|
822
917
|
'aria-label': L('side.type'),
|
|
823
|
-
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` },
|
|
824
919
|
onCompositionStart: () => {
|
|
825
920
|
composing.current = true
|
|
826
921
|
},
|
|
@@ -839,9 +934,10 @@ window.__ModuleLoader__.load({
|
|
|
839
934
|
}),
|
|
840
935
|
frame === null ? null : h('img', {
|
|
841
936
|
className: 'dshib-side-frame',
|
|
937
|
+
ref: frameEl,
|
|
842
938
|
src: `data:image/jpeg;base64,${frame.jpeg}`,
|
|
843
|
-
width: Math.max(1, Math.round(view.w
|
|
844
|
-
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)),
|
|
845
941
|
alt: '', // ui-copy-ignore
|
|
846
942
|
draggable: false,
|
|
847
943
|
}),
|
|
@@ -849,7 +945,7 @@ window.__ModuleLoader__.load({
|
|
|
849
945
|
className: 'dshib-side-ptr',
|
|
850
946
|
src: SIDE_POINTER, // ui-copy-ignore
|
|
851
947
|
alt: '', // ui-copy-ignore
|
|
852
|
-
style: { left: `${
|
|
948
|
+
style: spot === null ? undefined : { left: `${spot.x + pointer.x * spot.scale}px`, top: `${spot.y + pointer.y * spot.scale}px` },
|
|
853
949
|
})))
|
|
854
950
|
}
|
|
855
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
|
|
|
@@ -131,15 +152,16 @@ export class Bridge {
|
|
|
131
152
|
* Remember that a chat wants the mirror, and tell it now if it is listening.
|
|
132
153
|
* @param {string} sessionId - chat that asked.
|
|
133
154
|
* @param {number} [at] - when it asked; older asks are not replayed.
|
|
155
|
+
* @param {string} [reason] - 'ask' for a real request, 'keepalive' for a nudge.
|
|
134
156
|
* @returns {boolean} whether a live tab took the frame.
|
|
135
157
|
*/
|
|
136
|
-
want(sessionId, at = Date.now()) {
|
|
158
|
+
want(sessionId, at = Date.now(), reason = 'ask') {
|
|
137
159
|
const id = this.canon(sessionId)
|
|
138
160
|
if (id === '') return false
|
|
139
161
|
this.claim(id)
|
|
140
162
|
this.wanted = { session: id, at }
|
|
141
163
|
if (this.persist === true) writeMirror({ owner: this.owner, session: id, at })
|
|
142
|
-
return this.push(id, { type: 'sidecar-wanted', session: id, at })
|
|
164
|
+
return this.push(id, { type: 'sidecar-wanted', session: id, at, reason })
|
|
143
165
|
}
|
|
144
166
|
|
|
145
167
|
/**
|
|
@@ -155,7 +177,7 @@ export class Bridge {
|
|
|
155
177
|
ask older than two minutes is spent and is not replayed. */
|
|
156
178
|
const ask = this.wanted
|
|
157
179
|
if (ask.session === sessionId && Date.now() - ask.at <= 120_000) {
|
|
158
|
-
this.push(sessionId, { type: 'sidecar-wanted', session: sessionId, at: ask.at })
|
|
180
|
+
this.push(sessionId, { type: 'sidecar-wanted', session: sessionId, at: ask.at, reason: 'ask' })
|
|
159
181
|
}
|
|
160
182
|
}
|
|
161
183
|
|
|
@@ -187,14 +209,20 @@ export class Bridge {
|
|
|
187
209
|
}
|
|
188
210
|
|
|
189
211
|
/**
|
|
190
|
-
*
|
|
212
|
+
* Give the mirror up: the person closed the column, so the chat stops owning
|
|
213
|
+
* it and no keepalive may ask for it again. Ownership is what the column
|
|
214
|
+
* reads on reload, so it has to be dropped on disk as well as in memory.
|
|
191
215
|
* @param {string} sessionId - chat that closed its tab.
|
|
192
|
-
* @returns {boolean} whether the
|
|
216
|
+
* @returns {boolean} whether this chat held the mirror or the ask.
|
|
193
217
|
*/
|
|
194
218
|
forget(sessionId) {
|
|
195
|
-
|
|
219
|
+
const id = this.canon(sessionId)
|
|
220
|
+
if (id === '') return false
|
|
221
|
+
const held = this.owner === id || this.wanted.session === id
|
|
196
222
|
this.wanted = { session: '', at: 0 }
|
|
197
|
-
|
|
223
|
+
if (this.owner === id) this.owner = ''
|
|
224
|
+
if (this.persist === true) writeMirror({ owner: this.owner, session: '', at: 0 })
|
|
225
|
+
return held
|
|
198
226
|
}
|
|
199
227
|
|
|
200
228
|
/** Close every stream. */
|
|
@@ -328,8 +356,8 @@ export function apply(ctx) {
|
|
|
328
356
|
const due = nudgeDue({ askedAgo: Date.now() - (lastAsk.get(owner) ?? 0), sinceLast: Date.now() - last }) // ui-copy-ignore
|
|
329
357
|
if (due !== true) return
|
|
330
358
|
last = Date.now()
|
|
331
|
-
bridge.want(owner)
|
|
332
|
-
|
|
359
|
+
bridge.want(owner, Date.now(), 'keepalive')
|
|
360
|
+
note(`asked ${owner.slice(-8)} to draw the mirror again`)
|
|
333
361
|
} catch {}
|
|
334
362
|
}, 4000)
|
|
335
363
|
timer.unref?.()
|
|
@@ -504,7 +532,7 @@ export function apply(ctx) {
|
|
|
504
532
|
if (Date.now() - before > 2000) {
|
|
505
533
|
lastAsk.set(chat, Date.now())
|
|
506
534
|
try {
|
|
507
|
-
|
|
535
|
+
note(`${chat.slice(-8)} alive=${String(said?.alive === true)} viewers=${String(said?.viewers ?? 0)}`)
|
|
508
536
|
} catch {}
|
|
509
537
|
}
|
|
510
538
|
}
|
|
@@ -708,7 +736,17 @@ export function apply(ctx) {
|
|
|
708
736
|
const body = await readBody(req).then((raw) => JSON.parse(raw || '{}')).catch(() => ({}))
|
|
709
737
|
const who = acting(body)
|
|
710
738
|
if (who === '') return
|
|
711
|
-
|
|
739
|
+
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
|
|
742
|
+
agent asks for it again in the meantime. */
|
|
743
|
+
try {
|
|
744
|
+
if (sidecar.watched() === 0) {
|
|
745
|
+
sidecar.touch(REST_MS)
|
|
746
|
+
note('said ' + who.slice(-8) + ' closed the mirror - browser rests in a minute')
|
|
747
|
+
}
|
|
748
|
+
} catch { /* a browser that never started needs no rest timer */ }
|
|
749
|
+
json(200, { ok: true, forgot })
|
|
712
750
|
return
|
|
713
751
|
}
|
|
714
752
|
|
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.99",
|
|
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
|
@@ -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. */
|
|
@@ -2836,7 +2842,18 @@ export class Sidecar {
|
|
|
2836
2842
|
}
|
|
2837
2843
|
}
|
|
2838
2844
|
|
|
2839
|
-
|
|
2845
|
+
/** @returns {number} how many panes are watching a picture right now. */
|
|
2846
|
+
watched() {
|
|
2847
|
+
let n = this.page?.frames?.size ?? 0
|
|
2848
|
+
for (const page of this.pages?.values() ?? []) n += page.frames?.size ?? 0
|
|
2849
|
+
return n
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
/**
|
|
2853
|
+
* Starts the idle clock again.
|
|
2854
|
+
* @param {number} [ms] - how long a browser nobody watches may stay up.
|
|
2855
|
+
*/
|
|
2856
|
+
touch(ms = IDLE_MS) {
|
|
2840
2857
|
if (this.idle !== null) clearTimeout(this.idle)
|
|
2841
2858
|
this.idle = setTimeout(() => {
|
|
2842
2859
|
this.idle = null
|
|
@@ -2849,7 +2866,7 @@ export class Sidecar {
|
|
|
2849
2866
|
return
|
|
2850
2867
|
}
|
|
2851
2868
|
void this.shutdown()
|
|
2852
|
-
},
|
|
2869
|
+
}, ms)
|
|
2853
2870
|
this.idle.unref?.()
|
|
2854
2871
|
}
|
|
2855
2872
|
|