dsh-inner-browser-use 0.34.95 → 0.34.97
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 +73 -3
- package/index.js +43 -1
- package/package.json +1 -1
- package/sidecar.js +75 -0
- package/tools.js +1 -1
package/client.js
CHANGED
|
@@ -68,6 +68,10 @@ window.__ModuleLoader__.load({
|
|
|
68
68
|
'.dshib-side-key:hover{background:var(--dsw-alias-bg-subtle);color:var(--dsw-alias-label-primary)}',
|
|
69
69
|
'.dshib-side-url{flex:1 1 auto;min-width:0;border:1px solid transparent;border-radius:6px;background:var(--dsw-alias-bg-subtle);padding:4px 8px;font:inherit;font-size:12px;color:var(--dsw-alias-label-secondary);outline:none}',
|
|
70
70
|
'.dshib-side-url:focus{border-color:var(--dsw-alias-border-strong);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-base)}',
|
|
71
|
+
'.dshib-side-wins{flex:none;display:flex;gap:4px;padding:4px 6px;overflow-x:auto;border-bottom:1px solid var(--dsw-alias-separator)}',
|
|
72
|
+
'.dshib-side-win{flex:0 1 auto;min-width:0;max-width:180px;display:inline-flex;align-items:center;gap:4px;border:0;border-radius:6px;padding:2px 8px;background:transparent;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;cursor:pointer}',
|
|
73
|
+
'.dshib-side-win[data-on="1"]{background:var(--dsw-alias-bg-subtle);color:var(--dsw-alias-label-primary)}',
|
|
74
|
+
'.dshib-side-win > span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
71
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}',
|
|
72
76
|
'.dshib-side-frame{display:block;user-select:none;-webkit-user-drag:none;cursor:default}',
|
|
73
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}',
|
|
@@ -115,7 +119,7 @@ window.__ModuleLoader__.load({
|
|
|
115
119
|
return ''
|
|
116
120
|
}
|
|
117
121
|
}
|
|
118
|
-
const closeHere = (sessionId) => {
|
|
122
|
+
const closeHere = (sessionId, byPerson = false) => {
|
|
119
123
|
/* A tab the person closed is not a tab they want back on return. */
|
|
120
124
|
try {
|
|
121
125
|
void fetch(`${ROUTE}/sidecar/forget?session=${encodeURIComponent(sessionId)}`, { method: 'POST' }) // ui-copy-ignore
|
|
@@ -132,6 +136,11 @@ window.__ModuleLoader__.load({
|
|
|
132
136
|
const kind = String(record?.kind ?? record?.record?.kind ?? '')
|
|
133
137
|
const id = String(record?.id ?? record?.record?.id ?? '')
|
|
134
138
|
if (kind !== SIDE_KIND && id !== SIDE_ID) continue
|
|
139
|
+
/* The person's close arrives from the tab strip: the host is
|
|
140
|
+
already taking that tab down, and closing it again here races
|
|
141
|
+
the host and can leave the tab half closed. Forgetting the ask
|
|
142
|
+
is what this path owes. */
|
|
143
|
+
if (byPerson) continue
|
|
135
144
|
try {
|
|
136
145
|
if (typeof record?.close === 'function') record.close()
|
|
137
146
|
else if (typeof surface?.closeIn === 'function') surface.closeIn(sessionId, id)
|
|
@@ -172,7 +181,7 @@ window.__ModuleLoader__.load({
|
|
|
172
181
|
the keepalive put the mirror back a few seconds later — a tab
|
|
173
182
|
the person closed that would not stay closed. Every close path
|
|
174
183
|
ends here. */
|
|
175
|
-
onClose: (_tab, where) => closeHere(String(where?.sessionId ?? mountedOf())),
|
|
184
|
+
onClose: (_tab, where) => closeHere(String(where?.sessionId ?? mountedOf()), true),
|
|
176
185
|
})))
|
|
177
186
|
disposers.push(scope.slots.inject(SLOT, () => scope.slots.register({
|
|
178
187
|
name: SLOT, // ui-copy-ignore
|
|
@@ -349,6 +358,9 @@ window.__ModuleLoader__.load({
|
|
|
349
358
|
const [draft, setDraft] = React.useState('')
|
|
350
359
|
const [editing, setEditing] = React.useState(false)
|
|
351
360
|
const [size, setSize] = React.useState({ w: 0, h: 0 })
|
|
361
|
+
/* The browser's own windows, this chat's only: a second window a site
|
|
362
|
+
opened is a row here, not a second tab in the column. */
|
|
363
|
+
const [wins, setWins] = React.useState([])
|
|
352
364
|
const stage = React.useRef(null)
|
|
353
365
|
/* Which chat owns the mirror, as the sidecar host records it. // ui-copy-ignore
|
|
354
366
|
Empty until
|
|
@@ -521,6 +533,44 @@ window.__ModuleLoader__.load({
|
|
|
521
533
|
return () => observer.disconnect()
|
|
522
534
|
}, [])
|
|
523
535
|
|
|
536
|
+
/* The picture carries the page, not the window list: ask for the list on
|
|
537
|
+
a slow clock, and after a switch. */
|
|
538
|
+
const [winsTick, setWinsTick] = React.useState(0)
|
|
539
|
+
React.useEffect(() => {
|
|
540
|
+
if (mine !== true) return undefined
|
|
541
|
+
let alive = true
|
|
542
|
+
const chat = String(props.sessionId ?? '')
|
|
543
|
+
const load = () => {
|
|
544
|
+
fetch(`${ROUTE}/sidecar/tabs?session=${encodeURIComponent(chat)}`) // ui-copy-ignore
|
|
545
|
+
.then((reply) => (reply.ok === true ? reply.json() : null))
|
|
546
|
+
.then((info) => {
|
|
547
|
+
if (alive === false || info?.ok !== true || Array.isArray(info.tabs) !== true) return
|
|
548
|
+
setWins(info.tabs)
|
|
549
|
+
})
|
|
550
|
+
.catch(() => {})
|
|
551
|
+
}
|
|
552
|
+
load()
|
|
553
|
+
const clock = setInterval(load, 2500)
|
|
554
|
+
return () => {
|
|
555
|
+
alive = false
|
|
556
|
+
clearInterval(clock)
|
|
557
|
+
}
|
|
558
|
+
}, [mine, props.sessionId, winsTick])
|
|
559
|
+
|
|
560
|
+
const switchWin = (at) => {
|
|
561
|
+
fetch(`${ROUTE}/sidecar/tabs`, { // ui-copy-ignore
|
|
562
|
+
method: 'POST',
|
|
563
|
+
headers: { 'content-type': 'application/json' },
|
|
564
|
+
body: JSON.stringify({ session: String(props.sessionId ?? ''), op: 'use', index: at }), // ui-copy-ignore
|
|
565
|
+
})
|
|
566
|
+
.then((reply) => (reply.ok === true ? reply.json() : null))
|
|
567
|
+
.then((info) => {
|
|
568
|
+
if (info?.ok === true && Array.isArray(info.tabs) === true) setWins(info.tabs)
|
|
569
|
+
setWinsTick((n) => n + 1)
|
|
570
|
+
})
|
|
571
|
+
.catch(() => {})
|
|
572
|
+
}
|
|
573
|
+
|
|
524
574
|
const scale = size.w > 0 && size.h > 0 ? Math.min(size.w / view.w, size.h / view.h) : 0
|
|
525
575
|
const padX = (size.w - view.w * scale) / 2
|
|
526
576
|
const padY = (size.h - view.h * scale) / 2
|
|
@@ -548,7 +598,16 @@ window.__ModuleLoader__.load({
|
|
|
548
598
|
})
|
|
549
599
|
.then((answer) => (answer.ok === true ? answer.json() : { ok: false }))
|
|
550
600
|
.then((said) => {
|
|
551
|
-
if (!live
|
|
601
|
+
if (!live) return
|
|
602
|
+
/* The browser answers with the shape it actually took: drawing
|
|
603
|
+
the picture at that size fills the pane even when the page
|
|
604
|
+
refused the exact one asked for. */
|
|
605
|
+
const took = said?.view ?? null
|
|
606
|
+
if (said?.ok === true && took !== null && Number(took.width) > 0 && Number(took.height) > 0) {
|
|
607
|
+
setView({ w: Math.round(Number(took.width)), h: Math.round(Number(took.height)) })
|
|
608
|
+
return
|
|
609
|
+
}
|
|
610
|
+
if (said?.ok === true || tries >= 5) return
|
|
552
611
|
tries += 1
|
|
553
612
|
timer = setTimeout(send, 400)
|
|
554
613
|
})
|
|
@@ -716,6 +775,17 @@ window.__ModuleLoader__.load({
|
|
|
716
775
|
goto()
|
|
717
776
|
},
|
|
718
777
|
})),
|
|
778
|
+
...(wins.length > 1
|
|
779
|
+
? [h('div', { className: 'dshib-side-wins' },
|
|
780
|
+
...wins.map((win) => h('button', {
|
|
781
|
+
key: `${win.index}:${win.url}`,
|
|
782
|
+
className: 'dshib-side-win',
|
|
783
|
+
type: 'button', // ui-copy-ignore
|
|
784
|
+
'data-on': win.current === true ? '1' : '0',
|
|
785
|
+
title: win.url,
|
|
786
|
+
onClick: () => switchWin(win.index),
|
|
787
|
+
}, h('span', null, String(win.title ?? '') || String(win.url ?? '')))))]
|
|
788
|
+
: []),
|
|
719
789
|
h('div', {
|
|
720
790
|
className: 'dshib-side-stage',
|
|
721
791
|
ref: stage,
|
package/index.js
CHANGED
|
@@ -562,7 +562,10 @@ export function apply(ctx) {
|
|
|
562
562
|
return
|
|
563
563
|
}
|
|
564
564
|
try {
|
|
565
|
-
|
|
565
|
+
/* The shape belongs to the page this chat is actually looking at: a
|
|
566
|
+
chat driving a tab of its own must not resize the browser's base
|
|
567
|
+
page and keep a letterboxed picture. */
|
|
568
|
+
const page = sidecar.pageFor(actor) ?? sidecar.page
|
|
566
569
|
if (page === null || page === undefined) {
|
|
567
570
|
json(409, { ok: false, text: 'the browser is not open' }) // ui-copy-ignore
|
|
568
571
|
return
|
|
@@ -661,6 +664,44 @@ export function apply(ctx) {
|
|
|
661
664
|
return
|
|
662
665
|
}
|
|
663
666
|
|
|
667
|
+
if (path === '/sidecar/tabs' && req.method === 'GET') {
|
|
668
|
+
/* This chat's windows only: another chat's pages never enter this list. */
|
|
669
|
+
const who = acting({ session: url.searchParams.get('session') })
|
|
670
|
+
if (who === '') return
|
|
671
|
+
try {
|
|
672
|
+
const tabs = await sidecar.windowsFor(who)
|
|
673
|
+
json(200, { ok: true, tabs, ...(url.searchParams.get('session') === '' ? { session: who } : {}) })
|
|
674
|
+
} catch (error) {
|
|
675
|
+
json(500, { ok: false, text: String(error?.message ?? error) })
|
|
676
|
+
}
|
|
677
|
+
return
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if (path === '/sidecar/tabs' && req.method === 'POST') {
|
|
681
|
+
let body
|
|
682
|
+
try {
|
|
683
|
+
body = JSON.parse(await readBody(req))
|
|
684
|
+
} catch {
|
|
685
|
+
json(400, { ok: false, text: 'bad frame' })
|
|
686
|
+
return
|
|
687
|
+
}
|
|
688
|
+
const actor = acting(body)
|
|
689
|
+
if (actor === '') return
|
|
690
|
+
try {
|
|
691
|
+
const at = Number(body.index ?? -1)
|
|
692
|
+
if (body.op === 'close') {
|
|
693
|
+
await sidecar.closeTab(at, actor)
|
|
694
|
+
} else {
|
|
695
|
+
await sidecar.useTab(at, actor)
|
|
696
|
+
}
|
|
697
|
+
const tabs = await sidecar.windowsFor(actor)
|
|
698
|
+
json(200, { ok: true, tabs })
|
|
699
|
+
} catch (error) {
|
|
700
|
+
json(500, { ok: false, text: String(error?.message ?? error) })
|
|
701
|
+
}
|
|
702
|
+
return
|
|
703
|
+
}
|
|
704
|
+
|
|
664
705
|
if (path === '/sidecar/forget' && req.method === 'POST') {
|
|
665
706
|
/* Closing the tab is an answer: coming back should not reopen it. Only the
|
|
666
707
|
chat that holds the mirror may withdraw its own ask. */
|
|
@@ -789,6 +830,7 @@ export function apply(ctx) {
|
|
|
789
830
|
'',
|
|
790
831
|
'只有人明确说要看网页/用浏览器时才开始:「用网页端 / 在浏览器里 / 打开某个网站 / 上 ChatGPT 生成 / 帮我在这个页面点…」。人只给了链接、没说要用浏览器时,先用 `web_fetch` 或 envoy 读;读不到(登录墙、空壳、要交互)就说明原因再问一句,不要自己开。',
|
|
791
832
|
'不要用它:读公开文章、单个链接正文、文档、搜索结果摘要、GitHub 仓库或 PR 页面——这些能用 `web_fetch` / envoy 读就不用浏览器;一打开就是一个后台 Chrome 进程加一个页签,会话多了很占内存。',
|
|
833
|
+
'也不要用它做连通性、可达性、服务是否起来的检查:那是一个 HTTP 请求的事,`web_fetch` 或 `curl` 就够,开浏览器只会多一个进程和一栏画面。只有「这一步必须在真实浏览器里点/填/登录」才轮到它。',
|
|
792
834
|
'人点名之后不问、不另起外部 Chrome;画面留在本会话右栏,做完保留,不主动关。',
|
|
793
835
|
'先接着用已有的页:`browser_view`(拿编号控件与可见文字)→ `browser_click` / `browser_type` → 再 `browser_view` 直到出现要的结果;页面要等就重复读,不 sleep。`browser_open` 只用于没有页、要换新地址、或要明确刷新;同一个地址不要反复重开。',
|
|
794
836
|
'页面很长、只想确认某句话/按钮在哪时用 `browser_find`,别整页读。要读长文(文章、文档、issue、论坛帖)用 `browser_read`:它回 Markdown,只花几十分之一的 token,链接可点;要动手点、填表才用 `browser_view` 拿编号。',
|
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.97",
|
|
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
|
@@ -1962,6 +1962,10 @@ export class Sidecar {
|
|
|
1962
1962
|
/* One tab per chat: a second chat opening an address gets a tab of its
|
|
1963
1963
|
own, so neither can navigate the other's page away. */
|
|
1964
1964
|
this.pages = new Map()
|
|
1965
|
+
/* Every tab a chat has driven, so its column can offer its own windows
|
|
1966
|
+
back after it switches away: a tab the chat left is still its own, and a
|
|
1967
|
+
tab nobody claimed belongs to no column at all. */
|
|
1968
|
+
this.claims = new Map()
|
|
1965
1969
|
/* Tabs left behind by an earlier plugin load are closed once, on the first
|
|
1966
1970
|
call that finds this browser: a profile used for weeks otherwise keeps a
|
|
1967
1971
|
renderer per chat that ever opened it. */
|
|
@@ -2048,6 +2052,41 @@ export class Sidecar {
|
|
|
2048
2052
|
* @param {string} session - chat id, empty for the default tab.
|
|
2049
2053
|
* @returns {SidecarPage | null} the tab, if the browser is up.
|
|
2050
2054
|
*/
|
|
2055
|
+
/**
|
|
2056
|
+
* Remember that a tab belongs to a chat: switching away from it keeps it in
|
|
2057
|
+
* that chat's window list, and no other chat can see it.
|
|
2058
|
+
* @param {string} session - chat that drove the tab. // ui-copy-ignore
|
|
2059
|
+
* @param {string} targetId - the browser target.
|
|
2060
|
+
*/
|
|
2061
|
+
claim(session, targetId) {
|
|
2062
|
+
const who = String(session ?? '')
|
|
2063
|
+
const target = String(targetId ?? '')
|
|
2064
|
+
if (who === '' || target === '') return
|
|
2065
|
+
let set = this.claims.get(who)
|
|
2066
|
+
if (set === undefined) {
|
|
2067
|
+
set = new Set()
|
|
2068
|
+
this.claims.set(who, set)
|
|
2069
|
+
}
|
|
2070
|
+
/* A chat cannot hold an unbounded list: the oldest tab it left falls off. */
|
|
2071
|
+
if (set.size >= 12 && set.has(target) !== true) {
|
|
2072
|
+
const first = set.values().next()
|
|
2073
|
+
if (first.done !== true) set.delete(first.value)
|
|
2074
|
+
}
|
|
2075
|
+
set.add(target)
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
/**
|
|
2079
|
+
* Forget a tab a chat no longer has.
|
|
2080
|
+
* @param {string} session - chat that owned it. // ui-copy-ignore
|
|
2081
|
+
* @param {string} targetId - the browser target.
|
|
2082
|
+
*/
|
|
2083
|
+
unclaim(session, targetId) {
|
|
2084
|
+
const set = this.claims.get(String(session ?? ''))
|
|
2085
|
+
if (set === undefined) return
|
|
2086
|
+
set.delete(String(targetId ?? ''))
|
|
2087
|
+
if (set.size === 0) this.claims.delete(String(session ?? ''))
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2051
2090
|
pageFor(session) {
|
|
2052
2091
|
const id = String(session ?? '')
|
|
2053
2092
|
if (id === '') return this.page
|
|
@@ -2265,6 +2304,7 @@ export class Sidecar {
|
|
|
2265
2304
|
await cdp.ready
|
|
2266
2305
|
const page = await this.bind(await new SidecarPage(cdp, this.profile, port, () => this.holding()).start()) // ui-copy-ignore
|
|
2267
2306
|
this.pages.set(id, page)
|
|
2307
|
+
this.claim(id, page.targetId)
|
|
2268
2308
|
this.touch()
|
|
2269
2309
|
this.rememberTab(id, address)
|
|
2270
2310
|
return page
|
|
@@ -2603,6 +2643,39 @@ export class Sidecar {
|
|
|
2603
2643
|
return page === null ? [] : page.tabs()
|
|
2604
2644
|
}
|
|
2605
2645
|
|
|
2646
|
+
/**
|
|
2647
|
+
* The windows this chat may see: the browser's own pages minus the ones other
|
|
2648
|
+
* chats are driving. A chat's column shows its own windows, never a
|
|
2649
|
+
* neighbour's.
|
|
2650
|
+
* @param {string} session - chat asking.
|
|
2651
|
+
* @returns {Promise<Array<{title: string, url: string, current: boolean, index: number}>>} its windows.
|
|
2652
|
+
*/
|
|
2653
|
+
async windowsFor(session) {
|
|
2654
|
+
const list = await this.tabs(session)
|
|
2655
|
+
const page = this.pageFor(session) ?? this.page
|
|
2656
|
+
const foreign = new Set(typeof page?.siblings === 'function' ? page.siblings().map(String) : [])
|
|
2657
|
+
const who = String(session ?? '')
|
|
2658
|
+
const claimed = this.claims.get(who) ?? new Set()
|
|
2659
|
+
/* Tabs the browser no longer has must not linger in a chat's list. */
|
|
2660
|
+
if (claimed.size > 0) {
|
|
2661
|
+
const live = new Set(list.map((row) => String(row.targetId)))
|
|
2662
|
+
for (const id of [...claimed]) if (live.has(id) !== true) claimed.delete(id)
|
|
2663
|
+
if (claimed.size === 0) this.claims.delete(who)
|
|
2664
|
+
}
|
|
2665
|
+
const shown = []
|
|
2666
|
+
for (let at = 0; at < list.length; at += 1) {
|
|
2667
|
+
const row = list[at]
|
|
2668
|
+
if (foreign.has(String(row.targetId))) continue
|
|
2669
|
+
/* A tab nobody in this chat drove is not this chat's window: a page
|
|
2670
|
+
opened behind the browser's back belongs to no column. */
|
|
2671
|
+
if (row.current !== true && claimed.has(String(row.targetId)) !== true) continue
|
|
2672
|
+
/* The index is the browser's own row, not this shorter list's: switching
|
|
2673
|
+
goes back through `tabs()` unfiltered. */
|
|
2674
|
+
shown.push({ title: String(row.title ?? ''), url: String(row.url ?? ''), current: row.current === true, index: at })
|
|
2675
|
+
}
|
|
2676
|
+
return shown
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2606
2679
|
/**
|
|
2607
2680
|
* Move the mirror to another tab, leaving the old one open where it was.
|
|
2608
2681
|
* @param {number} index - a row from `tabs()`.
|
|
@@ -2644,6 +2717,7 @@ export class Sidecar {
|
|
|
2644
2717
|
await cdp.ready
|
|
2645
2718
|
const page = await this.bind(await new SidecarPage(cdp, this.profile, mine.port, () => this.holding()).start()) // ui-copy-ignore
|
|
2646
2719
|
this.pages.set(String(session), page)
|
|
2720
|
+
this.claim(session, tab.targetId)
|
|
2647
2721
|
mine.cdp.send('Page.stopScreencast').catch(() => {})
|
|
2648
2722
|
mine.cdp.close()
|
|
2649
2723
|
await page.cast()
|
|
@@ -2679,6 +2753,7 @@ export class Sidecar {
|
|
|
2679
2753
|
alone = other < 0
|
|
2680
2754
|
if (!alone) await this.useTab(other, session)
|
|
2681
2755
|
}
|
|
2756
|
+
this.unclaim(session, tab.targetId)
|
|
2682
2757
|
if (port > 0) await fetch(`http://127.0.0.1:${port}/json/close/${tab.targetId}`, { signal: AbortSignal.timeout(1000) }).catch(() => {}) // ui-copy-ignore
|
|
2683
2758
|
if (alone && mine !== null) {
|
|
2684
2759
|
/* The chat still needs somewhere to go: a fresh tab in the same browser. */
|
package/tools.js
CHANGED
|
@@ -1776,7 +1776,7 @@ export function sidecarTools(sidecar, refs, host = {}) { // ui-copy-ignore
|
|
|
1776
1776
|
return [
|
|
1777
1777
|
{
|
|
1778
1778
|
name: 'browser_open', // ui-copy-ignore
|
|
1779
|
-
description: 'Start (or reuse) the background browser — a real Chrome with its own persistent profile — and open a URL in it. It is not the host\'s own browser tab: this Chrome runs on its own and only its picture is mirrored into this chat\'s right column, so it keeps working while the person looks elsewhere. Logins survive restarts.',
|
|
1779
|
+
description: 'Start (or reuse) the background browser — a real Chrome with its own persistent profile — and open a URL in it. It is not the host\'s own browser tab: this Chrome runs on its own and only its picture is mirrored into this chat\'s right column, so it keeps working while the person looks elsewhere. Logins survive restarts. Use it only when the person explicitly asked for a browser this turn; reading a URL, a document or checking whether a site answers is `web_fetch`\'s job, not this one.',
|
|
1780
1780
|
parameters: {
|
|
1781
1781
|
type: 'object', // ui-copy-ignore
|
|
1782
1782
|
properties: {
|