ghost-bridge 0.7.1 → 0.9.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 +32 -1
- package/dist/cli.js +266 -140
- package/dist/server.js +288 -69
- package/extension/background.js +692 -224
- package/extension/manifest.json +3 -2
- package/extension/offscreen.js +211 -29
- package/extension/popup.html +72 -54
- package/extension/popup.js +72 -42
- package/package.json +4 -4
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ghost-bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Ghost Bridge: Zero-restart Chrome debugger bridge for Claude MCP. Includes CLI for easy setup.",
|
|
@@ -17,12 +17,12 @@
|
|
|
17
17
|
"author": "<Author>",
|
|
18
18
|
"repository": {
|
|
19
19
|
"type": "git",
|
|
20
|
-
"url": "https://github.com
|
|
20
|
+
"url": "https://github.com/learnStop163/ghost-bridge.git"
|
|
21
21
|
},
|
|
22
22
|
"bugs": {
|
|
23
|
-
"url": "https://github.com
|
|
23
|
+
"url": "https://github.com/learnStop163/ghost-bridge/issues"
|
|
24
24
|
},
|
|
25
|
-
"homepage": "https://github.com
|
|
25
|
+
"homepage": "https://github.com/learnStop163/ghost-bridge#readme",
|
|
26
26
|
"license": "MIT",
|
|
27
27
|
"engines": {
|
|
28
28
|
"node": ">=18.0.0"
|