ghost-bridge 0.7.0 → 0.8.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/README.md +20 -18
- package/dist/cli.js +266 -140
- package/dist/server.js +137 -38
- package/extension/background.js +246 -628
- package/extension/bg-dom.js +441 -0
- package/extension/bg-network.js +194 -0
- package/extension/manifest.json +1 -3
- package/extension/offscreen.js +64 -21
- package/extension/popup.html +72 -54
- package/extension/popup.js +72 -42
- package/package.json +1 -1
- package/extension/icon-32.png +0 -0
package/extension/offscreen.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
let ws = null
|
|
5
5
|
let reconnectTimer = null
|
|
6
6
|
let manualDisconnect = false // 用户主动断开标志,防止 onclose 触发重连
|
|
7
|
+
let connectionGeneration = 0
|
|
7
8
|
let config = {
|
|
8
9
|
basePort: 33333,
|
|
9
10
|
token: '',
|
|
@@ -17,12 +18,42 @@ function log(msg) {
|
|
|
17
18
|
|
|
18
19
|
const DEFAULT_TOKEN = 'ghost-bridge-local'
|
|
19
20
|
|
|
21
|
+
function clearReconnectTimer() {
|
|
22
|
+
if (reconnectTimer) {
|
|
23
|
+
clearTimeout(reconnectTimer)
|
|
24
|
+
reconnectTimer = null
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isCurrentConnection(generation, socket) {
|
|
29
|
+
return generation === connectionGeneration && socket === ws
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function scheduleReconnect(generation, delay) {
|
|
33
|
+
if (generation !== connectionGeneration) return
|
|
34
|
+
clearReconnectTimer()
|
|
35
|
+
reconnectTimer = setTimeout(() => {
|
|
36
|
+
if (generation === connectionGeneration) connect()
|
|
37
|
+
}, delay)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function closeCurrentSocket() {
|
|
41
|
+
if (!ws) return
|
|
42
|
+
try {
|
|
43
|
+
ws.close()
|
|
44
|
+
} catch {}
|
|
45
|
+
ws = null
|
|
46
|
+
}
|
|
47
|
+
|
|
20
48
|
// 连接到服务器
|
|
21
49
|
function connect() {
|
|
22
50
|
// 如果已手动断开,不再尝试连接
|
|
23
51
|
if (manualDisconnect) return
|
|
52
|
+
clearReconnectTimer()
|
|
53
|
+
closeCurrentSocket()
|
|
24
54
|
|
|
25
55
|
const port = config.basePort
|
|
56
|
+
const generation = ++connectionGeneration
|
|
26
57
|
const url = new URL(`ws://localhost:${port}`)
|
|
27
58
|
url.searchParams.set('token', config.token)
|
|
28
59
|
log(`尝试连接固定端口 ${port}...`)
|
|
@@ -32,12 +63,13 @@ function connect() {
|
|
|
32
63
|
currentPort: port,
|
|
33
64
|
}).catch(() => {})
|
|
34
65
|
|
|
35
|
-
|
|
36
|
-
ws
|
|
66
|
+
const socket = new WebSocket(url.toString())
|
|
67
|
+
ws = socket
|
|
68
|
+
socket.binaryType = 'blob' // 明确设置
|
|
37
69
|
|
|
38
70
|
const connectionTimeout = setTimeout(() => {
|
|
39
|
-
if (
|
|
40
|
-
|
|
71
|
+
if (isCurrentConnection(generation, socket) && socket.readyState === WebSocket.CONNECTING) {
|
|
72
|
+
socket.close()
|
|
41
73
|
}
|
|
42
74
|
}, 2000) // 增加到 2 秒
|
|
43
75
|
|
|
@@ -45,19 +77,22 @@ function connect() {
|
|
|
45
77
|
let socketOpened = false
|
|
46
78
|
let terminalErrorMessage = ''
|
|
47
79
|
|
|
48
|
-
|
|
80
|
+
socket.onopen = () => {
|
|
81
|
+
if (!isCurrentConnection(generation, socket)) return
|
|
49
82
|
socketOpened = true
|
|
50
83
|
clearTimeout(connectionTimeout)
|
|
51
84
|
log(`WebSocket 已连接端口 ${port},等待身份验证...`)
|
|
52
85
|
}
|
|
53
86
|
|
|
54
|
-
|
|
87
|
+
socket.onmessage = async (event) => {
|
|
55
88
|
try {
|
|
89
|
+
if (!isCurrentConnection(generation, socket)) return
|
|
56
90
|
// 处理 Blob 类型的消息
|
|
57
91
|
let data = event.data
|
|
58
92
|
if (data instanceof Blob) {
|
|
59
93
|
data = await data.text()
|
|
60
94
|
}
|
|
95
|
+
if (!isCurrentConnection(generation, socket)) return
|
|
61
96
|
const msg = JSON.parse(data)
|
|
62
97
|
|
|
63
98
|
if (msg.type === 'identity') {
|
|
@@ -68,6 +103,13 @@ function connect() {
|
|
|
68
103
|
type: 'status',
|
|
69
104
|
status: 'connected',
|
|
70
105
|
port: port,
|
|
106
|
+
serverInfo: {
|
|
107
|
+
version: msg.version,
|
|
108
|
+
pid: msg.pid,
|
|
109
|
+
port: msg.port,
|
|
110
|
+
startedAt: msg.startedAt,
|
|
111
|
+
serverPath: msg.serverPath,
|
|
112
|
+
},
|
|
71
113
|
}).catch(() => {})
|
|
72
114
|
} else {
|
|
73
115
|
terminalErrorMessage = msg.service === 'ghost-bridge'
|
|
@@ -94,7 +136,8 @@ function connect() {
|
|
|
94
136
|
}
|
|
95
137
|
}
|
|
96
138
|
|
|
97
|
-
|
|
139
|
+
socket.onclose = (event) => {
|
|
140
|
+
if (!isCurrentConnection(generation, socket)) return
|
|
98
141
|
clearTimeout(connectionTimeout)
|
|
99
142
|
|
|
100
143
|
// 用户主动断开,不重连
|
|
@@ -110,22 +153,28 @@ function connect() {
|
|
|
110
153
|
currentPort: port,
|
|
111
154
|
errorMessage,
|
|
112
155
|
}).catch(() => {})
|
|
113
|
-
|
|
156
|
+
scheduleReconnect(generation, 2000)
|
|
114
157
|
return
|
|
115
158
|
}
|
|
116
159
|
log(`固定端口 ${port} 未发现可用服务,2秒后重试...`)
|
|
117
|
-
chrome.runtime.sendMessage({
|
|
118
|
-
|
|
160
|
+
chrome.runtime.sendMessage({
|
|
161
|
+
type: 'status',
|
|
162
|
+
status: 'not_found',
|
|
163
|
+
currentPort: port,
|
|
164
|
+
errorMessage: `No ghost-bridge WebSocket service was found on port ${port}.`,
|
|
165
|
+
}).catch(() => {})
|
|
166
|
+
scheduleReconnect(generation, 2000)
|
|
119
167
|
return
|
|
120
168
|
}
|
|
121
169
|
|
|
122
170
|
// 连接断开,重试
|
|
123
171
|
log(`端口 ${port} 连接断开,尝试重连...`)
|
|
124
172
|
chrome.runtime.sendMessage({ type: 'status', status: 'disconnected' }).catch(() => {})
|
|
125
|
-
|
|
173
|
+
scheduleReconnect(generation, 1000)
|
|
126
174
|
}
|
|
127
175
|
|
|
128
|
-
|
|
176
|
+
socket.onerror = () => {
|
|
177
|
+
if (!isCurrentConnection(generation, socket)) return
|
|
129
178
|
clearTimeout(connectionTimeout)
|
|
130
179
|
}
|
|
131
180
|
}
|
|
@@ -142,14 +191,9 @@ function sendToServer(data) {
|
|
|
142
191
|
// 断开连接
|
|
143
192
|
function disconnect() {
|
|
144
193
|
manualDisconnect = true // 标记为手动断开,阻止 onclose 重连
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
if (ws) {
|
|
150
|
-
ws.close()
|
|
151
|
-
ws = null
|
|
152
|
-
}
|
|
194
|
+
connectionGeneration++
|
|
195
|
+
clearReconnectTimer()
|
|
196
|
+
closeCurrentSocket()
|
|
153
197
|
log('已断开连接')
|
|
154
198
|
}
|
|
155
199
|
|
|
@@ -158,7 +202,6 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
|
158
202
|
if (message.type === 'connect') {
|
|
159
203
|
config.basePort = message.basePort || 33333
|
|
160
204
|
config.token = message.token || DEFAULT_TOKEN
|
|
161
|
-
disconnect()
|
|
162
205
|
manualDisconnect = false // 用户重新连接,清除断开标志
|
|
163
206
|
connect()
|
|
164
207
|
sendResponse({ ok: true })
|
package/extension/popup.html
CHANGED
|
@@ -163,9 +163,17 @@
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
.status-row {
|
|
166
|
+
display: flex;
|
|
167
|
+
align-items: center;
|
|
168
|
+
justify-content: space-between;
|
|
169
|
+
gap: 12px;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.status-main {
|
|
166
173
|
display: flex;
|
|
167
174
|
align-items: center;
|
|
168
175
|
gap: 12px;
|
|
176
|
+
min-width: 0;
|
|
169
177
|
}
|
|
170
178
|
|
|
171
179
|
/* Modern Ripple/Pulse Animations */
|
|
@@ -200,6 +208,58 @@
|
|
|
200
208
|
font-weight: 600;
|
|
201
209
|
letter-spacing: 0.3px;
|
|
202
210
|
transition: color 0.3s ease;
|
|
211
|
+
white-space: nowrap;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.target-toggle {
|
|
215
|
+
flex: 0 0 auto;
|
|
216
|
+
min-width: 72px;
|
|
217
|
+
height: 28px;
|
|
218
|
+
padding: 0 10px;
|
|
219
|
+
border: 1px solid rgba(0, 210, 255, 0.28);
|
|
220
|
+
border-radius: 999px;
|
|
221
|
+
background: rgba(0, 210, 255, 0.08);
|
|
222
|
+
color: #9deaff;
|
|
223
|
+
font-size: 12px;
|
|
224
|
+
font-weight: 700;
|
|
225
|
+
letter-spacing: 0.2px;
|
|
226
|
+
cursor: pointer;
|
|
227
|
+
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
228
|
+
display: inline-flex;
|
|
229
|
+
align-items: center;
|
|
230
|
+
justify-content: center;
|
|
231
|
+
gap: 6px;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.target-toggle:hover:not(:disabled) {
|
|
235
|
+
background: rgba(0, 210, 255, 0.16);
|
|
236
|
+
border-color: rgba(0, 210, 255, 0.5);
|
|
237
|
+
color: #ffffff;
|
|
238
|
+
box-shadow: 0 0 14px rgba(0, 210, 255, 0.16);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.target-toggle:disabled {
|
|
242
|
+
opacity: 0.45;
|
|
243
|
+
cursor: not-allowed;
|
|
244
|
+
box-shadow: none;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.target-toggle.pinned {
|
|
248
|
+
background: rgba(255, 159, 10, 0.13);
|
|
249
|
+
border-color: rgba(255, 159, 10, 0.5);
|
|
250
|
+
color: #ffc56b;
|
|
251
|
+
box-shadow: 0 0 16px rgba(255, 159, 10, 0.12);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.target-toggle.pinned:hover:not(:disabled) {
|
|
255
|
+
background: rgba(255, 159, 10, 0.2);
|
|
256
|
+
border-color: rgba(255, 159, 10, 0.72);
|
|
257
|
+
color: #fff3d6;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.target-toggle .target-icon {
|
|
261
|
+
font-size: 12px;
|
|
262
|
+
line-height: 1;
|
|
203
263
|
}
|
|
204
264
|
|
|
205
265
|
.status-detail-container {
|
|
@@ -262,50 +322,6 @@
|
|
|
262
322
|
color: #ff9f0a;
|
|
263
323
|
}
|
|
264
324
|
|
|
265
|
-
.error-list {
|
|
266
|
-
max-height: 180px;
|
|
267
|
-
overflow-y: auto;
|
|
268
|
-
margin-top: 12px;
|
|
269
|
-
padding: 8px;
|
|
270
|
-
background: rgba(0, 0, 0, 0.4);
|
|
271
|
-
border-radius: 8px;
|
|
272
|
-
border: 1px solid rgba(255, 59, 48, 0.2);
|
|
273
|
-
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
274
|
-
opacity: 1;
|
|
275
|
-
}
|
|
276
|
-
.error-list.collapsed {
|
|
277
|
-
max-height: 0;
|
|
278
|
-
opacity: 0;
|
|
279
|
-
margin-top: 0;
|
|
280
|
-
padding-top: 0;
|
|
281
|
-
padding-bottom: 0;
|
|
282
|
-
border-color: transparent;
|
|
283
|
-
overflow: hidden;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
.error-item {
|
|
287
|
-
font-size: 11px;
|
|
288
|
-
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
|
|
289
|
-
padding: 6px;
|
|
290
|
-
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
|
291
|
-
word-break: break-work;
|
|
292
|
-
color: #e2e8f0;
|
|
293
|
-
display: flex;
|
|
294
|
-
flex-direction: column;
|
|
295
|
-
gap: 4px;
|
|
296
|
-
}
|
|
297
|
-
.error-item:last-child {
|
|
298
|
-
border-bottom: none;
|
|
299
|
-
}
|
|
300
|
-
.error-item .err-msg {
|
|
301
|
-
color: #ff8a8a;
|
|
302
|
-
white-space: pre-wrap;
|
|
303
|
-
}
|
|
304
|
-
.error-item .err-loc {
|
|
305
|
-
color: #64748b;
|
|
306
|
-
font-size: 10px;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
325
|
.btn-row {
|
|
310
326
|
display: flex;
|
|
311
327
|
gap: 10px;
|
|
@@ -432,10 +448,16 @@
|
|
|
432
448
|
|
|
433
449
|
<div class="status-card" id="statusCard">
|
|
434
450
|
<div class="status-row">
|
|
435
|
-
<div class="status-
|
|
436
|
-
<div class="status-dot"
|
|
451
|
+
<div class="status-main">
|
|
452
|
+
<div class="status-dot-wrapper" id="dotWrapper">
|
|
453
|
+
<div class="status-dot"></div>
|
|
454
|
+
</div>
|
|
455
|
+
<span class="status-text" id="statusText">Checking...</span>
|
|
437
456
|
</div>
|
|
438
|
-
<
|
|
457
|
+
<button class="target-toggle" id="targetBtn" type="button" title="Pin current tab">
|
|
458
|
+
<span class="target-icon" id="targetIcon">○</span>
|
|
459
|
+
<span id="targetBtnText">Pin</span>
|
|
460
|
+
</button>
|
|
439
461
|
</div>
|
|
440
462
|
|
|
441
463
|
<div class="status-detail-container" id="detailContainer">
|
|
@@ -447,15 +469,11 @@
|
|
|
447
469
|
<span class="detail-label">Target Tab</span>
|
|
448
470
|
<span class="detail-value" id="tabVal" title="">-</span>
|
|
449
471
|
</div>
|
|
450
|
-
<div class="detail-row
|
|
451
|
-
<span class="detail-label">
|
|
452
|
-
<span class="detail-value
|
|
472
|
+
<div class="detail-row hidden" id="viewingRow">
|
|
473
|
+
<span class="detail-label">Viewing Now</span>
|
|
474
|
+
<span class="detail-value" id="viewingVal" title="">-</span>
|
|
453
475
|
</div>
|
|
454
476
|
</div>
|
|
455
|
-
|
|
456
|
-
<div id="errorList" class="error-list collapsed">
|
|
457
|
-
<!-- JS dynamically injects errors here -->
|
|
458
|
-
</div>
|
|
459
477
|
</div>
|
|
460
478
|
|
|
461
479
|
<div class="btn-row">
|
package/extension/popup.js
CHANGED
|
@@ -7,11 +7,13 @@ const detailContainer = document.getElementById('detailContainer')
|
|
|
7
7
|
const portVal = document.getElementById('portVal')
|
|
8
8
|
const tabRow = document.getElementById('tabRow')
|
|
9
9
|
const tabVal = document.getElementById('tabVal')
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const errorList = document.getElementById('errorList')
|
|
10
|
+
const viewingRow = document.getElementById('viewingRow')
|
|
11
|
+
const viewingVal = document.getElementById('viewingVal')
|
|
13
12
|
const headerGhost = document.getElementById('headerGhost')
|
|
14
13
|
|
|
14
|
+
const targetBtn = document.getElementById('targetBtn')
|
|
15
|
+
const targetBtnText = document.getElementById('targetBtnText')
|
|
16
|
+
const targetIcon = document.getElementById('targetIcon')
|
|
15
17
|
const connectBtn = document.getElementById('connectBtn')
|
|
16
18
|
const disconnectBtn = document.getElementById('disconnectBtn')
|
|
17
19
|
const scanInfo = document.getElementById('scanInfo')
|
|
@@ -19,6 +21,7 @@ const scanInfo = document.getElementById('scanInfo')
|
|
|
19
21
|
let lastStableStatus = null
|
|
20
22
|
let pendingStatus = null
|
|
21
23
|
let statusChangeTimer = null
|
|
24
|
+
let latestState = null
|
|
22
25
|
const STATUS_DEBOUNCE_MS = 300
|
|
23
26
|
|
|
24
27
|
const STATUS_MAP = {
|
|
@@ -53,8 +56,24 @@ const STATUS_MAP = {
|
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
function renderUI(state) {
|
|
56
|
-
|
|
59
|
+
latestState = state
|
|
60
|
+
const {
|
|
61
|
+
status,
|
|
62
|
+
port,
|
|
63
|
+
enabled,
|
|
64
|
+
currentPort,
|
|
65
|
+
basePort,
|
|
66
|
+
connectionError,
|
|
67
|
+
targetMode = 'focused',
|
|
68
|
+
targetTab,
|
|
69
|
+
viewingTab,
|
|
70
|
+
tabTitle,
|
|
71
|
+
} = state
|
|
72
|
+
const isPinned = targetMode === 'pinned'
|
|
57
73
|
const config = STATUS_MAP[status] || STATUS_MAP.disconnected
|
|
74
|
+
const statusLabel = status === 'connected'
|
|
75
|
+
? isPinned ? 'ON / Pinned' : 'ON / Following'
|
|
76
|
+
: config.text
|
|
58
77
|
|
|
59
78
|
// Update classes for color & animations
|
|
60
79
|
dotWrapper.className = `status-dot-wrapper ${config.statusClass}`
|
|
@@ -75,10 +94,10 @@ function renderUI(state) {
|
|
|
75
94
|
}
|
|
76
95
|
|
|
77
96
|
// Animate text change
|
|
78
|
-
if (statusText.textContent !==
|
|
97
|
+
if (statusText.textContent !== statusLabel) {
|
|
79
98
|
statusText.style.opacity = '0'
|
|
80
99
|
setTimeout(() => {
|
|
81
|
-
statusText.textContent =
|
|
100
|
+
statusText.textContent = statusLabel
|
|
82
101
|
statusText.style.opacity = '1'
|
|
83
102
|
}, 150)
|
|
84
103
|
}
|
|
@@ -88,34 +107,21 @@ function renderUI(state) {
|
|
|
88
107
|
portVal.textContent = port
|
|
89
108
|
portVal.className = 'detail-value highlight'
|
|
90
109
|
|
|
91
|
-
|
|
110
|
+
const targetTitle = targetTab?.title || tabTitle
|
|
111
|
+
if (targetTitle) {
|
|
92
112
|
tabRow.classList.remove('hidden')
|
|
93
|
-
tabVal.textContent =
|
|
94
|
-
tabVal.title =
|
|
113
|
+
tabVal.textContent = targetTitle
|
|
114
|
+
tabVal.title = targetTab?.url || targetTitle
|
|
95
115
|
} else {
|
|
96
116
|
tabRow.classList.add('hidden')
|
|
97
117
|
}
|
|
98
118
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
// Render error list
|
|
104
|
-
if (recentErrors && recentErrors.length > 0) {
|
|
105
|
-
errorList.innerHTML = recentErrors.map(err => {
|
|
106
|
-
const text = err.text ? err.text.substring(0, 100) : 'Unknown Error'
|
|
107
|
-
const file = err.url ? err.url.split('/').pop() : 'inline'
|
|
108
|
-
const loc = err.line ? `${file}:${err.line}` : file
|
|
109
|
-
return `
|
|
110
|
-
<div class="error-item" title="${err.text || ''}">
|
|
111
|
-
<div class="err-msg">${text}${err.text && err.text.length > 100 ? '...' : ''}</div>
|
|
112
|
-
<div class="err-loc">${loc}</div>
|
|
113
|
-
</div>
|
|
114
|
-
`
|
|
115
|
-
}).join('')
|
|
119
|
+
if (isPinned && viewingTab && targetTab && viewingTab.id !== targetTab.id) {
|
|
120
|
+
viewingRow.classList.remove('hidden')
|
|
121
|
+
viewingVal.textContent = viewingTab.title || 'Untitled'
|
|
122
|
+
viewingVal.title = viewingTab.url || viewingTab.title || ''
|
|
116
123
|
} else {
|
|
117
|
-
|
|
118
|
-
errorList.classList.add('collapsed')
|
|
124
|
+
viewingRow.classList.add('hidden')
|
|
119
125
|
}
|
|
120
126
|
|
|
121
127
|
detailContainer.classList.remove('collapsed')
|
|
@@ -123,21 +129,21 @@ function renderUI(state) {
|
|
|
123
129
|
portVal.textContent = `Connecting: ${currentPort}`
|
|
124
130
|
portVal.className = 'detail-value highlight'
|
|
125
131
|
tabRow.classList.add('hidden')
|
|
126
|
-
|
|
132
|
+
viewingRow.classList.add('hidden')
|
|
127
133
|
detailContainer.classList.remove('collapsed')
|
|
128
134
|
} else if (status === 'disconnected') {
|
|
129
135
|
detailContainer.classList.add('collapsed')
|
|
130
136
|
} else if (status === 'not_found') {
|
|
131
|
-
portVal.textContent = '
|
|
137
|
+
portVal.textContent = 'No Bridge'
|
|
132
138
|
portVal.className = 'detail-value warning'
|
|
133
139
|
tabRow.classList.add('hidden')
|
|
134
|
-
|
|
140
|
+
viewingRow.classList.add('hidden')
|
|
135
141
|
detailContainer.classList.remove('collapsed')
|
|
136
142
|
} else if (status === 'error') {
|
|
137
143
|
portVal.textContent = `Port ${currentPort || basePort || '-'} blocked`
|
|
138
144
|
portVal.className = 'detail-value warning'
|
|
139
145
|
tabRow.classList.add('hidden')
|
|
140
|
-
|
|
146
|
+
viewingRow.classList.add('hidden')
|
|
141
147
|
detailContainer.classList.remove('collapsed')
|
|
142
148
|
} else {
|
|
143
149
|
detailContainer.classList.add('collapsed')
|
|
@@ -152,12 +158,24 @@ function renderUI(state) {
|
|
|
152
158
|
connectBtn.disabled = false
|
|
153
159
|
}
|
|
154
160
|
|
|
161
|
+
targetBtn.classList.toggle('pinned', isPinned)
|
|
162
|
+
targetBtnText.textContent = isPinned ? 'Locked' : 'Pin'
|
|
163
|
+
targetIcon.textContent = isPinned ? '●' : '○'
|
|
164
|
+
targetBtn.title = isPinned
|
|
165
|
+
? 'Unpin target tab and follow the focused tab'
|
|
166
|
+
: 'Pin current tab as the Ghost Bridge target'
|
|
167
|
+
targetBtn.setAttribute('aria-label', targetBtn.title)
|
|
168
|
+
targetBtn.disabled = status !== 'connected' || !port
|
|
169
|
+
|
|
155
170
|
// Scan info text
|
|
156
171
|
if (status === 'error' && connectionError) {
|
|
157
172
|
scanInfo.textContent = connectionError
|
|
158
173
|
scanInfo.classList.remove('collapsed')
|
|
174
|
+
} else if (status === 'connected' && state.targetError) {
|
|
175
|
+
scanInfo.textContent = state.targetError
|
|
176
|
+
scanInfo.classList.remove('collapsed')
|
|
159
177
|
} else if (status === 'not_found' && basePort) {
|
|
160
|
-
scanInfo.textContent = `
|
|
178
|
+
scanInfo.textContent = connectionError || `No ghost-bridge WebSocket service was found on port ${basePort}. Start a configured MCP client, then reconnect.`
|
|
161
179
|
scanInfo.classList.remove('collapsed')
|
|
162
180
|
} else {
|
|
163
181
|
scanInfo.classList.add('collapsed')
|
|
@@ -212,6 +230,27 @@ async function fetchStatus() {
|
|
|
212
230
|
}
|
|
213
231
|
}
|
|
214
232
|
|
|
233
|
+
targetBtn.addEventListener('click', async () => {
|
|
234
|
+
try {
|
|
235
|
+
const isPinned = latestState?.targetMode === 'pinned'
|
|
236
|
+
targetBtnText.textContent = isPinned ? 'Unlocking' : 'Pinning'
|
|
237
|
+
targetBtn.disabled = true
|
|
238
|
+
const response = await chrome.runtime.sendMessage({
|
|
239
|
+
type: isPinned ? 'unpinTab' : 'pinCurrentTab',
|
|
240
|
+
})
|
|
241
|
+
if (response && !response.ok) {
|
|
242
|
+
scanInfo.textContent = response.error || 'Target action failed'
|
|
243
|
+
scanInfo.classList.remove('collapsed')
|
|
244
|
+
}
|
|
245
|
+
} catch (e) {
|
|
246
|
+
console.error('Target action failed:', e)
|
|
247
|
+
scanInfo.textContent = e.message
|
|
248
|
+
scanInfo.classList.remove('collapsed')
|
|
249
|
+
} finally {
|
|
250
|
+
setTimeout(fetchStatus, 100)
|
|
251
|
+
}
|
|
252
|
+
})
|
|
253
|
+
|
|
215
254
|
connectBtn.addEventListener('click', async () => {
|
|
216
255
|
try {
|
|
217
256
|
// Add visual click feedback
|
|
@@ -240,12 +279,3 @@ chrome.runtime.onMessage.addListener((message) => {
|
|
|
240
279
|
updateUI(message.state)
|
|
241
280
|
}
|
|
242
281
|
})
|
|
243
|
-
|
|
244
|
-
// Error List Toggle Logic
|
|
245
|
-
errorRow.addEventListener('click', () => {
|
|
246
|
-
if (errorList.classList.contains('collapsed')) {
|
|
247
|
-
errorList.classList.remove('collapsed')
|
|
248
|
-
} else {
|
|
249
|
-
errorList.classList.add('collapsed')
|
|
250
|
-
}
|
|
251
|
-
})
|
package/package.json
CHANGED
package/extension/icon-32.png
DELETED
|
Binary file
|