dsh-inner-browser-use 0.18.0 → 0.34.28

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
@@ -32,7 +32,8 @@ window.__ModuleLoader__.load({
32
32
  const DICT = {
33
33
  zh: {
34
34
  'side.label': '后台浏览器',
35
- 'side.empty': '还没有打开的页面',
35
+ 'side.empty': '还没有打开的页面,在上方地址栏输入网址',
36
+ 'side.restore': '恢复上次的页面',
36
37
  'side.connecting': '正在连接…',
37
38
  'side.failed': '画面没有连上',
38
39
  'side.foreign': '这个浏览器属于发起它的会话,回到那个会话继续。',
@@ -44,7 +45,8 @@ window.__ModuleLoader__.load({
44
45
  },
45
46
  en: {
46
47
  'side.label': 'Background browser',
47
- 'side.empty': 'No page open',
48
+ 'side.empty': 'No page open; type an address above',
49
+ 'side.restore': 'Reopen the last page',
48
50
  'side.connecting': 'Connecting…',
49
51
  'side.failed': 'The picture did not connect',
50
52
  'side.foreign': 'This browser belongs to the chat that opened it.',
@@ -73,6 +75,8 @@ window.__ModuleLoader__.load({
73
75
  on screen at once, and moving the mouse never takes the arrow over. */
74
76
  '.dshib-side-ptr{position:absolute;width:20px;height:20px;pointer-events:none;z-index:2;transform:translate(-2px,-2px);transition:left 60ms linear,top 60ms linear;filter:drop-shadow(0 1px 2px rgba(0,0,0,.45))}',
75
77
  '.dshib-side-note{font-size:12px;color:var(--dsw-alias-label-tertiary);padding:0 16px;text-align:center}',
78
+ '.dshib-side-back{border:0;border-radius:8px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;padding:6px 12px;cursor:pointer}',
79
+ '.dshib-side-back:hover{background:var(--dsw-alias-bg-subtle)}',
76
80
  ].join('')
77
81
 
78
82
  return {
@@ -84,6 +88,9 @@ window.__ModuleLoader__.load({
84
88
  /* one locale service or none */
85
89
  }
86
90
  ctx.effect(() => {
91
+ /* A reload keeps one sheet, not one per lifetime: the same id is
92
+ replaced instead of stacked. */
93
+ document.getElementById?.(STYLE_ID)?.remove()
87
94
  const style = document.createElement('style')
88
95
  style.id = STYLE_ID
89
96
  /* A sheet is keyed by the plugin that brought it. */
@@ -109,6 +116,12 @@ window.__ModuleLoader__.load({
109
116
  }
110
117
  }
111
118
  const closeHere = (sessionId) => {
119
+ /* A tab the person closed is not a tab they want back on return. */
120
+ try {
121
+ void fetch(`${ROUTE}/sidecar/forget?session=${encodeURIComponent(sessionId)}`, { method: 'POST' }) // ui-copy-ignore
122
+ } catch {
123
+ /* a page that cannot hear it keeps its note */
124
+ }
112
125
  let records = []
113
126
  try {
114
127
  records = surface?.tabsIn?.(sessionId) ?? []
@@ -138,14 +151,23 @@ window.__ModuleLoader__.load({
138
151
  /* a column that refuses keeps its own tabs */
139
152
  }
140
153
  }
141
- disposers.push(tabs.register({
154
+ /* The same rule on this side: a second mount of the same tab is a
155
+ no-op, not a thrown error that takes the pane down. */
156
+ const safely = (register) => {
157
+ try {
158
+ return register()
159
+ } catch (error) {
160
+ return () => {}
161
+ }
162
+ }
163
+ disposers.push(safely(() => tabs.register({
142
164
  id: SIDE_ID, // ui-copy-ignore
143
165
  kind: SIDE_KIND, // ui-copy-ignore
144
166
  keepMounted: true,
145
167
  single: true,
146
168
  priority: 'extension',
147
169
  title: () => L('side.label'),
148
- }))
170
+ })))
149
171
  disposers.push(scope.slots.inject(SLOT, () => scope.slots.register({
150
172
  name: SLOT, // ui-copy-ignore
151
173
  key: SIDE_ID, // ui-copy-ignore
@@ -177,7 +199,10 @@ window.__ModuleLoader__.load({
177
199
  }
178
200
  const syncAsk = () => {
179
201
  const who = mountedOf()
180
- if (who === askWho) return
202
+ /* A stream the browser gave up on keeps its address here, so the
203
+ next tick has to notice it is dead and open a fresh one, or this
204
+ chat never hears an ask again. */
205
+ if (who === askWho && askSource !== null && Number(askSource.readyState) !== 2) return
181
206
  closeAsk()
182
207
  if (who === '') return
183
208
  try {
@@ -251,6 +276,9 @@ window.__ModuleLoader__.load({
251
276
  const [ptr, setPtr] = React.useState(null)
252
277
  const [phase, setPhase] = React.useState('connecting')
253
278
  const [note, setNote] = React.useState('')
279
+ /* The address this chat had before its tab went away: the pane reopens
280
+ that address, so a lost picture costs one click. */
281
+ const [back, setBack] = React.useState('')
254
282
  const [url, setUrl] = React.useState('')
255
283
  const [draft, setDraft] = React.useState('')
256
284
  const [editing, setEditing] = React.useState(false)
@@ -263,13 +291,48 @@ window.__ModuleLoader__.load({
263
291
  React.useEffect(() => {
264
292
  let alive = true
265
293
  let dropped = false
294
+ /* The tab is still in this chat's column, so this chat may say the
295
+ mirror is its own: a reload forgets the owner, and a pane that only
296
+ waited would stay dark until some tool ran. */
297
+ const reclaim = () => {
298
+ const id = String(props.sessionId ?? '')
299
+ if (id === '') return
300
+ void fetch(`${ROUTE}/sidecar/claim`, { // ui-copy-ignore
301
+ method: 'POST',
302
+ headers: { 'content-type': 'application/json' },
303
+ body: JSON.stringify({ session: id }),
304
+ })
305
+ .then((reply) => (reply.ok === true ? reply.json() : null))
306
+ .then((info) => {
307
+ if (alive !== true) return
308
+ if (info?.ok === true) {
309
+ setOwner(id)
310
+ /* The owner was just named, so the next poll may adopt the
311
+ browser: ask now instead of up to a poll later. */
312
+ setTimeout(read, 0)
313
+ }
314
+ })
315
+ .catch(() => {})
316
+ }
266
317
  const read = () => {
267
- void fetch(`${ROUTE}/sidecar/state`, { cache: 'no-store' }) // ui-copy-ignore
318
+ const who = String(props.sessionId ?? '')
319
+ /* Naming the chat lets a browser that outlived a reload be picked
320
+ up for us; an anonymous poll cannot, and the column would sit on
321
+ an empty stage with the browser running behind it. */
322
+ void fetch(`${ROUTE}/sidecar/state?session=${encodeURIComponent(who)}`, { cache: 'no-store' }) // ui-copy-ignore
268
323
  .then((reply) => (reply.ok === true ? reply.json() : null))
269
324
  .then((info) => {
270
325
  if (alive !== true) return
271
326
  const next = String(info?.owner ?? '')
327
+ const asked = String(props.sessionId ?? '')
328
+ /* Nobody owns it, or this chat does but the note was lost: ask
329
+ back instead of showing a note for a browser this column shows. */
330
+ if (asked !== '' && next !== asked) reclaim()
272
331
  setOwner((prev) => (prev === next ? prev : next))
332
+ /* A browser that is gone still remembers where this chat was:
333
+ keep the address so the pane can offer to reopen it. */
334
+ const was = String(info?.chat?.remembered ?? '')
335
+ if (was !== '') setBack((prev) => (prev === was ? prev : was))
273
336
  /* Another chat holds the browser now: this tab goes away instead
274
337
  of sitting here as a note. */
275
338
  if (dropped !== true && next !== '' && next !== String(props.sessionId ?? '')) {
@@ -297,9 +360,15 @@ window.__ModuleLoader__.load({
297
360
  this chat keeps only a note. // ui-copy-ignore */
298
361
  const unknown = owner === null
299
362
  const mine = unknown !== true && owner === asked
363
+ /* A picture that stops arriving has to come back by itself: the pane
364
+ re-opens its stream instead of sitting on an empty stage until the
365
+ person reloads the page. */
366
+ const [attempt, setAttempt] = React.useState(0)
367
+ const drawnAt = React.useRef(0)
300
368
 
301
369
  React.useEffect(() => {
302
370
  if (mine !== true) return undefined
371
+ drawnAt.current = Date.now()
303
372
  const who = encodeURIComponent(String(props.sessionId ?? ''))
304
373
  const source = new EventSource(`${ROUTE}/sidecar/frames?session=${who}`)
305
374
  /* Bursts arrive faster than a picture can decode: only the newest frame
@@ -316,6 +385,7 @@ window.__ModuleLoader__.load({
316
385
  setFrame(message)
317
386
  setPtr(message.pointer ?? null)
318
387
  setPhase('live')
388
+ drawnAt.current = Date.now()
319
389
  if (typeof message.url === 'string' && message.url !== '') setUrl(message.url)
320
390
  }
321
391
  const draw = (message) => {
@@ -324,6 +394,11 @@ window.__ModuleLoader__.load({
324
394
  if (typeof requestAnimationFrame === 'function') raf = requestAnimationFrame(paint)
325
395
  else paint()
326
396
  }
397
+ const stuck = setInterval(() => {
398
+ /* Hello alone is not a picture: six quiet seconds means the stream is
399
+ dead, so ask for a new one. */
400
+ if (Date.now() - drawnAt.current > 6000) setAttempt((n) => n + 1)
401
+ }, 3000)
327
402
  source.onmessage = (event) => {
328
403
  let message
329
404
  try {
@@ -333,7 +408,9 @@ window.__ModuleLoader__.load({
333
408
  }
334
409
  if (message.type === 'hello') {
335
410
  setPhase('live')
411
+ drawnAt.current = Date.now()
336
412
  setUrl(String(message.url ?? ''))
413
+ setBack(String(message.chat?.remembered ?? ''))
337
414
  } else if (message.type === 'failed') {
338
415
  setPhase('failed')
339
416
  setNote(String(message.text ?? ''))
@@ -347,10 +424,11 @@ window.__ModuleLoader__.load({
347
424
  }
348
425
  source.onerror = () => setPhase('lost')
349
426
  return () => {
427
+ clearInterval(stuck)
350
428
  if (raf !== 0 && typeof cancelAnimationFrame === 'function') cancelAnimationFrame(raf)
351
429
  source.close()
352
430
  }
353
- }, [props.sessionId, mine])
431
+ }, [props.sessionId, mine, attempt])
354
432
 
355
433
  /* The address field follows the page except while the person is editing it. */
356
434
  React.useEffect(() => {
@@ -510,7 +588,24 @@ window.__ModuleLoader__.load({
510
588
  onWheel,
511
589
  onKeyDown,
512
590
  },
513
- frame === null ? h('div', { className: 'dshib-side-note' }, noteText) : null,
591
+ frame === null && back !== ''
592
+ ? h('button', {
593
+ className: 'dshib-side-back',
594
+ type: 'button', // ui-copy-ignore
595
+ onClick: () => { // ui-copy-ignore
596
+ void fetch(`${ROUTE}/sidecar/open`, { // ui-copy-ignore
597
+ method: 'POST',
598
+ headers: { 'content-type': 'application/json' },
599
+ body: JSON.stringify({ session: who, url: back }), // ui-copy-ignore
600
+ })
601
+ .then((reply) => (reply.ok === true ? reply.json() : null))
602
+ .then((info) => {
603
+ if (info?.ok === true) setAttempt((n) => n + 1)
604
+ })
605
+ .catch(() => {})
606
+ },
607
+ }, L('side.restore'))
608
+ : frame === null ? h('div', { className: 'dshib-side-note' }, noteText) : null,
514
609
  h('textarea', {
515
610
  className: 'dshib-side-ime',
516
611
  ref: ime,
@@ -0,0 +1,19 @@
1
+ # 文件落在哪里
2
+
3
+ 浏览器产生的东西——截图、抓下来的图、打印的 PDF——不落在项目根目录,也不散在 home 下。固定规则:
4
+
5
+ ```
6
+ ~/.dsh-inner-browser-use/artifacts/<项目>/<会话>/<种类>/
7
+ ```
8
+
9
+ - **项目**:会话工作目录的文件夹名,加路径哈希前 6 位,例如 `dsh-inner-browser-use-a1b2c3`。同名文件夹、不同路径不会混。
10
+ - **会话**:会话 id 去掉 `session-` 后的前 8 位,例如 `72557d6e`。同一项目里两个会话各存各的。
11
+ - **种类**:`shots/` 是 `sidecar_shot` 的截图,`pictures/` 是 `sidecar_grab` 抓的图,`pdf/` 是 `sidecar_pdf` 打印的文档。
12
+
13
+ 文件名默认带时间戳(`shot-20261028-124500.png`),给了 `name` 就用它;名字里的分隔符、连续点、开头点都会被去掉,跳不出这个目录。
14
+
15
+ 每个种类保留最新 40 个文件,旧的自动删,免得跑一天的会话留下一个没人翻得动的文件夹。开着的文件不会被删掉(尽力删除,失败就留着)。
16
+
17
+ 调用环境里没有项目或会话信息时,落到 `no-project/no-chat/<种类>/`,不猜。
18
+
19
+ 测试里用 `DSH_INNER_BROWSER_ARTIFACTS` 指向临时目录,不写真实 home。