ghost-bridge 0.7.1 → 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 +17 -0
- package/dist/cli.js +266 -140
- package/dist/server.js +132 -33
- package/extension/background.js +236 -81
- package/extension/manifest.json +1 -1
- 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/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
|
-
})
|