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.
@@ -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-dot-wrapper" id="dotWrapper">
436
- <div class="status-dot"></div>
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
- <span class="status-text" id="statusText">Checking...</span>
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 clickable" id="errorRow" title="Click to view recent errors">
451
- <span class="detail-label">Rec. Errors</span>
452
- <span class="detail-value warning" id="errorVal">0</span>
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">
@@ -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 errorRow = document.getElementById('errorRow')
11
- const errorVal = document.getElementById('errorVal')
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
- const { status, port, enabled, currentPort, basePort, connectionError, errorCount, recentErrors, tabTitle } = state
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 !== config.text) {
97
+ if (statusText.textContent !== statusLabel) {
79
98
  statusText.style.opacity = '0'
80
99
  setTimeout(() => {
81
- statusText.textContent = config.text
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
- if (tabTitle) {
110
+ const targetTitle = targetTab?.title || tabTitle
111
+ if (targetTitle) {
92
112
  tabRow.classList.remove('hidden')
93
- tabVal.textContent = tabTitle
94
- tabVal.title = tabTitle
113
+ tabVal.textContent = targetTitle
114
+ tabVal.title = targetTab?.url || targetTitle
95
115
  } else {
96
116
  tabRow.classList.add('hidden')
97
117
  }
98
118
 
99
- errorRow.classList.remove('hidden')
100
- errorVal.textContent = errorCount || 0
101
- errorVal.className = errorCount > 0 ? 'detail-value warning' : 'detail-value'
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
- errorList.innerHTML = '<div class="error-item" style="text-align:center;color:#64748b;border:none;">No recent errors recorded.</div>'
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
- errorRow.classList.add('hidden')
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 = 'Launch Claude Code'
137
+ portVal.textContent = 'No Bridge'
132
138
  portVal.className = 'detail-value warning'
133
139
  tabRow.classList.add('hidden')
134
- errorRow.classList.add('hidden')
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
- errorRow.classList.add('hidden')
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 = `Port ${basePort} unavailable. Is your MCP server running on this port?`
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.7.1",
3
+ "version": "0.8.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.",