dsh-site-bridge 0.5.3 → 0.6.1

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.3",
5
+ "version": "0.6.1",
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; min-height: 44px;
108
+ }
109
+ .tab-meta { display: inline-flex; align-items: center; gap: 5px; color: var(--muted); font-size: 10.5px; flex: none; }
110
+ .tab-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--ok); }
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,56 @@ 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)
98
+ target.classList.toggle('empty', targets.length === 0)
99
+ target.hidden = targets.length > 0
100
+
101
+ const list = $('tabs-list')
102
+ list.hidden = targets.length === 0
103
+ list.textContent = ''
104
+ for (const item of targets) {
105
+ const row = document.createElement('div')
106
+ row.className = 'tab-item'
107
+ row.dataset.id = String(item.id)
108
+ row.title = item.url || ''
109
+
110
+ const body = document.createElement('div')
111
+ body.className = 'tab-item-text'
112
+ const title = document.createElement('div')
113
+ title.className = 'tab-item-title'
114
+ title.textContent = item.title || item.url || `#${item.id}`
115
+ const address = document.createElement('div')
116
+ address.className = 'tab-item-url'
117
+ address.textContent = item.url || ''
118
+ body.append(title, address)
119
+
120
+ const remove = document.createElement('button')
121
+ remove.className = 'icon'
122
+ remove.dataset.remove = String(item.id)
123
+ remove.title = t('disconnectTab')
124
+ 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>'
125
+
126
+ // Every tab carries its own numbers: nothing here is "the" tab.
127
+ const meta = document.createElement('span')
128
+ meta.className = 'tab-meta'
129
+ if (item.recording) {
130
+ const dot = document.createElement('i')
131
+ dot.className = 'tab-dot'
132
+ meta.append(dot)
115
133
  }
116
- } else {
117
- tabsRow.hidden = true
134
+ meta.append(document.createTextNode(String(item.captured ?? 0)))
135
+ row.append(body, meta, remove)
136
+ list.append(row)
118
137
  }
119
138
 
120
139
  // 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
140
+ const selfAttached = selfTabId != null && targets.some((item) => item.id === selfTabId)
141
+ claim.hidden = targets.length > 0
142
+ claim.disabled = !connected || targets.length > 0
143
+ claim.textContent = t('connectTab')
144
+ $('claim-more').hidden = targets.length === 0 || selfAttached
125
145
 
126
146
  const pick = $('pick')
127
147
  pick.classList.toggle('active', status.picking === true)
@@ -273,9 +293,14 @@ $('capture').addEventListener('click', async () => {
273
293
  await refresh()
274
294
  })
275
295
 
276
- $('release').addEventListener('click', async () => {
277
- await send({ type: 'release' })
278
- await refresh()
296
+ $('tabs-list').addEventListener('click', async (event) => {
297
+ const remove = event.target.closest('[data-remove]')
298
+ if (remove) {
299
+ event.stopPropagation()
300
+ await send({ type: 'release', tabId: Number(remove.dataset.remove) })
301
+ await refresh()
302
+ return
303
+ }
279
304
  })
280
305
 
281
306
  $('pick').addEventListener('click', async () => {
@@ -296,11 +321,6 @@ $('pick').addEventListener('click', async () => {
296
321
 
297
322
  $('claim-more').addEventListener('click', attachCurrentTab)
298
323
 
299
- $('tabs-select').addEventListener('change', async (event) => {
300
- const tabId = Number(event.target.value)
301
- if (!Number.isFinite(tabId)) return
302
- await send({ type: 'use', tabId })
303
- await refresh()
304
- })
324
+
305
325
 
306
326
  void init()
package/extension/sw.js CHANGED
@@ -193,8 +193,10 @@ async function handleMessageInner(message) {
193
193
  recording: session.recording,
194
194
  captured: session.entries.length
195
195
  })),
196
- recording: active?.recording ?? false,
197
- captured: active?.entries.length ?? 0,
196
+ // Aggregates: the popup shows every attached tab, so "capture on" means
197
+ // "at least one is recording" and the counter is the sum.
198
+ recording: [...state.sessions.values()].some((session) => session.recording),
199
+ captured: [...state.sessions.values()].reduce((sum, session) => sum + session.entries.length, 0),
198
200
  picking: active?.picking === true,
199
201
  picks: active?.picks?.length ?? 0
200
202
  }
@@ -216,8 +218,13 @@ async function handleMessageInner(message) {
216
218
  return { ok: true }
217
219
  }
218
220
  if (message?.type === 'toggle-capture') {
221
+ const on = message.on === true
222
+ if (message.tabId == null || message.tabId === '') {
223
+ for (const session of state.sessions.values()) await setRecording(session, on)
224
+ return { ok: true, recording: on, tabs: state.sessions.size }
225
+ }
219
226
  const session = requireSession(message.tabId)
220
- return { ok: true, recording: await setRecording(session, message.on === true) }
227
+ return { ok: true, recording: await setRecording(session, on) }
221
228
  }
222
229
  return { ok: false, error: `unknown message ${message?.type}` }
223
230
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-site-bridge",
3
- "version": "0.5.3",
3
+ "version": "0.6.1",
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,