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.
- package/extension/manifest.json +1 -1
- package/extension/popup.html +20 -9
- package/extension/popup.js +75 -46
- package/extension/sw.js +10 -2
- package/package.json +1 -1
- package/tools/preview-popup.mjs +11 -0
package/extension/manifest.json
CHANGED
package/extension/popup.html
CHANGED
|
@@ -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"
|
|
152
|
-
<div class="target-url"
|
|
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="
|
|
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>
|
package/extension/popup.js
CHANGED
|
@@ -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
|
-
|
|
97
|
-
|
|
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
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
122
|
-
claim.
|
|
123
|
-
claim.
|
|
124
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
$('
|
|
260
|
-
|
|
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',
|
|
281
|
-
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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",
|
package/tools/preview-popup.mjs
CHANGED
|
@@ -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,
|