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 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 || said?.ok === true || tries >= 5) return
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
- const page = sidecar.page
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.95",
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: {