dsh-site-bridge 0.5.2 → 0.6.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.
@@ -2,7 +2,7 @@
2
2
  "manifest_version": 3,
3
3
  "default_locale": "en",
4
4
  "name": "__MSG_appName__",
5
- "version": "0.5.2",
5
+ "version": "0.6.0",
6
6
  "description": "__MSG_appDesc__",
7
7
  "minimum_chrome_version": "116",
8
8
  "permissions": [
@@ -94,6 +94,23 @@
94
94
  button.link:hover:not(:disabled) { color: #7e98ff; background: none; }
95
95
 
96
96
  .target-text { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 2px; }
97
+ .tabs-list {
98
+ display: flex; flex-direction: column; gap: 6px; max-height: 150px; overflow-y: auto;
99
+ margin-top: 8px; scrollbar-width: thin; scrollbar-color: #3a4150 transparent;
100
+ }
101
+ .tabs-list::-webkit-scrollbar { width: 6px; }
102
+ .tabs-list::-webkit-scrollbar-thumb { background: #3a4150; border-radius: 3px; }
103
+ .tabs-list::-webkit-scrollbar-track { background: transparent; }
104
+ .tab-item {
105
+ display: flex; align-items: center; justify-content: space-between; gap: 8px;
106
+ padding: 6px 8px; background: #15181e; border: 1px solid var(--line);
107
+ border-radius: 7px; cursor: pointer; min-height: 44px;
108
+ }
109
+ .tab-item:hover { border-color: #3a4150; }
110
+ .tab-item.active { border-color: var(--accent); background: #1a1f2c; }
111
+ .tab-item-text { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 2px; }
112
+ .tab-item-title { font-size: 12px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
113
+ .tab-item-url { color: var(--muted); font-size: 10.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
97
114
  .capture-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--line); }
98
115
  .capture-row button { padding: 5px 9px; font-size: 11.5px; }
99
116
  select {
@@ -148,12 +165,9 @@
148
165
 
149
166
  <div class="target empty" id="target-block">
150
167
  <div class="target-text">
151
- <div class="target-title" id="target-title" data-i18n="targetNoneTitle">Вкладка не подключена</div>
152
- <div class="target-url" id="target-url" data-i18n="targetNoneUrl">Откройте нужную страницу и подключите её</div>
168
+ <div class="target-title" data-i18n="targetNoneTitle">Вкладка не подключена</div>
169
+ <div class="target-url" data-i18n="targetNoneUrl">Откройте нужную страницу и подключите её</div>
153
170
  </div>
154
- <button class="icon" id="release" hidden data-i18n-title="disconnectTab" title="Отключить вкладку">
155
- <svg width="12" height="12" viewBox="0 0 12 12" aria-hidden="true"><path d="M2 2 L10 10 M10 2 L2 10" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
156
- </button>
157
171
  </div>
158
172
 
159
173
  <button class="primary" id="claim" data-i18n="connectTab">Подключить эту вкладку</button>
@@ -161,10 +175,7 @@
161
175
 
162
176
  <button class="primary" id="claim-more" hidden data-i18n="attachAnother">Подключить ещё вкладку</button>
163
177
 
164
- <div class="capture-row" id="tabs-row" hidden>
165
- <span class="muted" data-i18n="tabsLabel">Вкладки</span>
166
- <select id="tabs-select"></select>
167
- </div>
178
+ <div class="tabs-list" id="tabs-list" hidden></div>
168
179
 
169
180
  <div class="capture-row">
170
181
  <span class="muted" id="captured">0</span>
@@ -59,7 +59,6 @@ function render(status) {
59
59
  const dot = $('dot')
60
60
  const text = $('connection-text')
61
61
  const claim = $('claim')
62
- const release = $('release')
63
62
  const capture = $('capture')
64
63
  const target = $('target-block')
65
64
 
@@ -93,35 +92,48 @@ function render(status) {
93
92
  text.textContent = t('statusNotConfigured')
94
93
  }
95
94
 
96
- target.classList.toggle('empty', !hasTarget)
97
- $('target-title').textContent = hasTarget ? status.target.title || status.target.url : t('targetNoneTitle')
98
- $('target-url').textContent = hasTarget ? String(status.target.url || '') : t('targetNoneUrl')
99
-
100
- // Several attached tabs: a compact switcher, nothing else changes.
95
+ // One block per attached tab: title, address and its own detach cross. The
96
+ // list scrolls after three blocks so the popup keeps its height.
101
97
  const targets = Array.isArray(status.targets) ? status.targets : []
102
- const tabsRow = $('tabs-row')
103
- const tabsSelect = $('tabs-select')
104
- const selfAttached = selfTabId != null && targets.some((item) => item.id === selfTabId)
105
- $('claim-more').hidden = !hasTarget || selfAttached
106
- if (targets.length > 1) {
107
- tabsRow.hidden = false
108
- tabsSelect.textContent = ''
109
- for (const item of targets) {
110
- const option = document.createElement('option')
111
- option.value = String(item.id)
112
- option.textContent = item.title || item.url || `#${item.id}`
113
- option.selected = item.id === status.activeTabId
114
- tabsSelect.appendChild(option)
115
- }
116
- } else {
117
- tabsRow.hidden = true
98
+ const activeId = status.activeTabId ?? status.target?.id ?? null
99
+ target.classList.toggle('empty', targets.length === 0)
100
+ target.hidden = targets.length > 0
101
+
102
+ const list = $('tabs-list')
103
+ list.hidden = targets.length === 0
104
+ list.textContent = ''
105
+ for (const item of targets) {
106
+ const row = document.createElement('div')
107
+ row.className = item.id === activeId ? 'tab-item active' : 'tab-item'
108
+ row.dataset.id = String(item.id)
109
+ row.title = item.url || ''
110
+
111
+ const body = document.createElement('div')
112
+ body.className = 'tab-item-text'
113
+ const title = document.createElement('div')
114
+ title.className = 'tab-item-title'
115
+ title.textContent = item.title || item.url || `#${item.id}`
116
+ const address = document.createElement('div')
117
+ address.className = 'tab-item-url'
118
+ address.textContent = item.url || ''
119
+ body.append(title, address)
120
+
121
+ const remove = document.createElement('button')
122
+ remove.className = 'icon'
123
+ remove.dataset.remove = String(item.id)
124
+ remove.title = t('disconnectTab')
125
+ remove.innerHTML = '<svg width="12" height="12" viewBox="0 0 12 12" aria-hidden="true"><path d="M2 2 L10 10 M10 2 L2 10" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>'
126
+
127
+ row.append(body, remove)
128
+ list.append(row)
118
129
  }
119
130
 
120
131
  // Nothing to claim once a tab is bound: the action would be a no-op.
121
- claim.hidden = hasTarget
122
- claim.disabled = !connected || hasTarget
123
- claim.textContent = hasTarget ? t('tabConnected') : t('connectTab')
124
- release.hidden = !hasTarget
132
+ const selfAttached = selfTabId != null && targets.some((item) => item.id === selfTabId)
133
+ claim.hidden = targets.length > 0
134
+ claim.disabled = !connected || targets.length > 0
135
+ claim.textContent = t('connectTab')
136
+ $('claim-more').hidden = targets.length === 0 || selfAttached
125
137
 
126
138
  const pick = $('pick')
127
139
  pick.classList.toggle('active', status.picking === true)
@@ -239,16 +251,33 @@ $('import-file').addEventListener('change', async (event) => {
239
251
  }
240
252
  })
241
253
 
242
- $('claim').addEventListener('click', async () => {
243
- const [tab] = await chrome.tabs.query({ active: true, currentWindow: true })
244
- if (!tab?.id) return
245
- const result = await send({ type: 'claim', tabId: tab.id })
246
- if (!result?.ok) {
247
- setNote(t('claimFailed', [String(result?.error ?? t('claimFailedUnknown'))]), true)
254
+ async function attachCurrentTab() {
255
+ let tabId = null
256
+ try {
257
+ const [tab] = await chrome.tabs.query({ active: true, currentWindow: true })
258
+ tabId = tab?.id ?? null
259
+ } catch {
260
+ tabId = null
261
+ }
262
+ if (tabId == null) {
263
+ setNote(t('claimFailed', [t('claimFailedUnknown')]), true)
264
+ return
265
+ }
266
+ try {
267
+ const result = await send({ type: 'claim', tabId })
268
+ if (!result?.ok) {
269
+ setNote(t('claimFailed', [String(result?.error ?? t('claimFailedUnknown'))]), true)
270
+ return
271
+ }
272
+ setNote('')
273
+ } catch (error) {
274
+ setNote(t('claimFailed', [String(error?.message ?? error)]), true)
248
275
  return
249
276
  }
250
277
  await refresh()
251
- })
278
+ }
279
+
280
+ $('claim').addEventListener('click', attachCurrentTab)
252
281
 
253
282
  $('capture').addEventListener('click', async () => {
254
283
  const status = await send({ type: 'status' })
@@ -256,8 +285,17 @@ $('capture').addEventListener('click', async () => {
256
285
  await refresh()
257
286
  })
258
287
 
259
- $('release').addEventListener('click', async () => {
260
- await send({ type: 'release' })
288
+ $('tabs-list').addEventListener('click', async (event) => {
289
+ const remove = event.target.closest('[data-remove]')
290
+ if (remove) {
291
+ event.stopPropagation()
292
+ await send({ type: 'release', tabId: Number(remove.dataset.remove) })
293
+ await refresh()
294
+ return
295
+ }
296
+ const row = event.target.closest('.tab-item')
297
+ if (!row || row.classList.contains('active')) return
298
+ await send({ type: 'use', tabId: Number(row.dataset.id) })
261
299
  await refresh()
262
300
  })
263
301
 
@@ -277,17 +315,8 @@ $('pick').addEventListener('click', async () => {
277
315
  await refresh()
278
316
  })
279
317
 
280
- $('claim-more').addEventListener('click', async () => {
281
- const result = await send({ type: 'claim' })
282
- if (!result?.ok && result?.error) setNote(String(result.error), true)
283
- await refresh()
284
- })
318
+ $('claim-more').addEventListener('click', attachCurrentTab)
319
+
285
320
 
286
- $('tabs-select').addEventListener('change', async (event) => {
287
- const tabId = Number(event.target.value)
288
- if (!Number.isFinite(tabId)) return
289
- await send({ type: 'use', tabId })
290
- await refresh()
291
- })
292
321
 
293
322
  void init()
package/extension/sw.js CHANGED
@@ -272,11 +272,19 @@ async function attach(tabId) {
272
272
  }
273
273
 
274
274
  async function claimTab(tabId) {
275
- const tab = await chrome.tabs.get(tabId)
275
+ // Without an explicit id, claim whatever tab the user is looking at, so a
276
+ // caller that only knows "this tab" cannot silently pass NaN.
277
+ let id = Number(tabId)
278
+ if (!Number.isFinite(id)) {
279
+ const [active] = await chrome.tabs.query({ active: true, lastFocusedWindow: true })
280
+ id = active?.id
281
+ }
282
+ if (!Number.isFinite(id)) throw new Error('no tab to claim: pass tabId or open a page first')
283
+ const tab = await chrome.tabs.get(id)
276
284
  if (!/^https?:|^file:/.test(tab.url ?? '')) {
277
285
  throw new Error(`cannot drive ${tab.url || 'this tab'}: only http(s) and file:// pages are supported`)
278
286
  }
279
- return attach(tabId)
287
+ return attach(id)
280
288
  }
281
289
 
282
290
  chrome.debugger.onDetach.addListener((source, reason) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-site-bridge",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "Chrome/Edge MV3 bridge for DeepSeek Harness: capture page markup (DOM, styles, selectors) and BFF traffic (xhr/fetch with headers and bodies) into a workspace snapshot bundle, drive the page with trusted CDP input, and verify injected scripts. Authorization never leaves the browser and is never written to disk.",
5
5
  "icon": "./icon.svg",
6
6
  "type": "module",
@@ -21,6 +21,17 @@ const chromePath = process.env.CHROME_PATH ?? 'C:\\Program Files\\Google\\Chrome
21
21
  const sleep = (ms) => new Promise((done) => setTimeout(done, ms))
22
22
 
23
23
  const statuses = {
24
+ many: {
25
+ ok: true, configured: true, connected: true, port: 47321, recording: true, captured: 7, selfTabId: 9,
26
+ activeTabId: 1,
27
+ target: { id: 1, title: 'Example Store — Products', url: 'https://shop.example/products' },
28
+ targets: [
29
+ { id: 1, title: 'Example Store — Products', url: 'https://shop.example/products', recording: true, captured: 7 },
30
+ { id: 2, title: 'Example Store — Cart', url: 'https://shop.example/cart', recording: false, captured: 0 },
31
+ { id: 3, title: 'Example Store — Checkout', url: 'https://shop.example/checkout', recording: false, captured: 0 },
32
+ { id: 4, title: 'Example Store — Orders', url: 'https://shop.example/orders', recording: false, captured: 0 }
33
+ ]
34
+ },
24
35
  'connected-one': {
25
36
  ok: true, configured: true, connected: true, port: 47321, recording: false, captured: 0, selfTabId: 1,
26
37
  activeTabId: 1,