@flrande/bak-extension 0.6.11 → 0.6.13
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/dist/.bak-e2e-build-stamp +1 -1
- package/dist/background.global.js +588 -90
- package/dist/content.global.js +530 -12
- package/dist/manifest.json +1 -1
- package/dist/popup.global.js +233 -101
- package/dist/popup.html +260 -37
- package/package.json +2 -2
- package/public/popup.html +260 -37
- package/src/background.ts +344 -296
- package/src/content.ts +390 -22
- package/src/dynamic-data-tools.ts +790 -0
- package/src/popup.ts +291 -108
package/src/popup.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
const statusPanelEl = document.getElementById('statusPanel') as HTMLDivElement;
|
|
2
|
+
const statusBadgeEl = document.getElementById('statusBadge') as HTMLSpanElement;
|
|
1
3
|
const statusEl = document.getElementById('status') as HTMLDivElement;
|
|
2
|
-
const
|
|
4
|
+
const statusBodyEl = document.getElementById('statusBody') as HTMLDivElement;
|
|
5
|
+
const recoveryListEl = document.getElementById('recoveryList') as HTMLUListElement;
|
|
3
6
|
const tokenInput = document.getElementById('token') as HTMLInputElement;
|
|
4
7
|
const portInput = document.getElementById('port') as HTMLInputElement;
|
|
5
8
|
const debugRichTextInput = document.getElementById('debugRichText') as HTMLInputElement;
|
|
@@ -15,7 +18,9 @@ const lastErrorEl = document.getElementById('lastError') as HTMLDivElement;
|
|
|
15
18
|
const lastBindingUpdateEl = document.getElementById('lastBindingUpdate') as HTMLDivElement;
|
|
16
19
|
const extensionVersionEl = document.getElementById('extensionVersion') as HTMLDivElement;
|
|
17
20
|
const sessionSummaryEl = document.getElementById('sessionSummary') as HTMLDivElement;
|
|
18
|
-
const
|
|
21
|
+
const sessionCardsEl = document.getElementById('sessionCards') as HTMLDivElement;
|
|
22
|
+
|
|
23
|
+
type PopupTone = 'neutral' | 'success' | 'warning' | 'error';
|
|
19
24
|
|
|
20
25
|
interface PopupState {
|
|
21
26
|
ok: boolean;
|
|
@@ -44,31 +49,27 @@ interface PopupState {
|
|
|
44
49
|
label: string;
|
|
45
50
|
tabCount: number;
|
|
46
51
|
activeTabId: number | null;
|
|
52
|
+
activeTabTitle: string | null;
|
|
53
|
+
activeTabUrl: string | null;
|
|
47
54
|
windowId: number | null;
|
|
48
55
|
groupId: number | null;
|
|
49
56
|
detached: boolean;
|
|
57
|
+
lastBindingUpdateAt: number | null;
|
|
58
|
+
lastBindingUpdateReason: string | null;
|
|
50
59
|
}>;
|
|
51
60
|
};
|
|
52
61
|
}
|
|
53
|
-
let latestState: PopupState | null = null;
|
|
54
62
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (tone === 'warning') {
|
|
62
|
-
statusEl.style.color = '#b45309';
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
if (tone === 'error') {
|
|
66
|
-
statusEl.style.color = '#dc2626';
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
statusEl.style.color = '#0f172a';
|
|
63
|
+
interface StatusDescriptor {
|
|
64
|
+
badge: string;
|
|
65
|
+
title: string;
|
|
66
|
+
body: string;
|
|
67
|
+
tone: PopupTone;
|
|
68
|
+
recoverySteps: string[];
|
|
70
69
|
}
|
|
71
70
|
|
|
71
|
+
let latestState: PopupState | null = null;
|
|
72
|
+
|
|
72
73
|
function pluralize(count: number, singular: string, plural = `${singular}s`): string {
|
|
73
74
|
return `${count} ${count === 1 ? singular : plural}`;
|
|
74
75
|
}
|
|
@@ -89,25 +90,31 @@ function formatTimeAgo(at: number | null): string {
|
|
|
89
90
|
return `${deltaMinutes}m ago`;
|
|
90
91
|
}
|
|
91
92
|
const deltaHours = Math.round(deltaMinutes / 60);
|
|
92
|
-
|
|
93
|
+
if (deltaHours < 48) {
|
|
94
|
+
return `${deltaHours}h ago`;
|
|
95
|
+
}
|
|
96
|
+
const deltaDays = Math.round(deltaHours / 24);
|
|
97
|
+
return `${deltaDays}d ago`;
|
|
93
98
|
}
|
|
94
99
|
|
|
95
|
-
function
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
} else {
|
|
99
|
-
sessionSummaryEl.textContent = `${pluralize(state.count, 'session')}, ${pluralize(state.attachedCount, 'attached binding')}, ${pluralize(state.tabCount, 'tab')}, ${pluralize(state.detachedCount, 'detached binding')}`;
|
|
100
|
+
function truncate(value: string, maxLength: number): string {
|
|
101
|
+
if (value.length <= maxLength) {
|
|
102
|
+
return value;
|
|
100
103
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
104
|
+
return `${value.slice(0, Math.max(0, maxLength - 1))}...`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function formatUrl(url: string | null): string {
|
|
108
|
+
if (!url) {
|
|
109
|
+
return 'No active tab URL';
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
const parsed = new URL(url);
|
|
113
|
+
const trimmedPath = parsed.pathname === '/' ? '' : parsed.pathname;
|
|
114
|
+
const trimmedQuery = parsed.search.length > 0 ? parsed.search : '';
|
|
115
|
+
return truncate(`${parsed.host}${trimmedPath}${trimmedQuery}`, 64);
|
|
116
|
+
} catch {
|
|
117
|
+
return truncate(url, 64);
|
|
111
118
|
}
|
|
112
119
|
}
|
|
113
120
|
|
|
@@ -129,6 +136,127 @@ function describeConnectionState(connectionState: PopupState['connectionState'])
|
|
|
129
136
|
}
|
|
130
137
|
}
|
|
131
138
|
|
|
139
|
+
function setStatus(descriptor: StatusDescriptor): void {
|
|
140
|
+
statusPanelEl.dataset.tone = descriptor.tone;
|
|
141
|
+
statusBadgeEl.dataset.tone = descriptor.tone;
|
|
142
|
+
statusBadgeEl.textContent = descriptor.badge;
|
|
143
|
+
statusEl.textContent = descriptor.title;
|
|
144
|
+
statusBodyEl.textContent = descriptor.body;
|
|
145
|
+
recoveryListEl.replaceChildren();
|
|
146
|
+
for (const step of descriptor.recoverySteps) {
|
|
147
|
+
const li = document.createElement('li');
|
|
148
|
+
li.textContent = step;
|
|
149
|
+
recoveryListEl.appendChild(li);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function describeStatus(state: PopupState): StatusDescriptor {
|
|
154
|
+
const lastError = `${state.lastErrorContext ?? ''} ${state.lastError ?? ''}`.toLowerCase();
|
|
155
|
+
const runtimeOffline = lastError.includes('cannot connect to bak cli');
|
|
156
|
+
|
|
157
|
+
if (state.connected) {
|
|
158
|
+
const body =
|
|
159
|
+
state.sessionBindings.detachedCount > 0
|
|
160
|
+
? `${pluralize(state.sessionBindings.detachedCount, 'remembered session')} are detached. Check the cards below before you continue browser work.`
|
|
161
|
+
: 'The extension bridge is healthy and ready for CLI-driven browser work.';
|
|
162
|
+
return {
|
|
163
|
+
badge: 'Ready',
|
|
164
|
+
title: 'Connected to the local bak runtime',
|
|
165
|
+
body,
|
|
166
|
+
tone: 'success',
|
|
167
|
+
recoverySteps: state.sessionBindings.detachedCount > 0
|
|
168
|
+
? [
|
|
169
|
+
'Resume or recreate detached work from the bak CLI before sending new page commands.',
|
|
170
|
+
'Use the Sessions panel below to confirm which remembered session lost its owned tabs.'
|
|
171
|
+
]
|
|
172
|
+
: [
|
|
173
|
+
'Start browser work from the bak CLI.',
|
|
174
|
+
'Use Reconnect bridge only when you intentionally changed token or port settings.'
|
|
175
|
+
]
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (state.connectionState === 'missing-token') {
|
|
180
|
+
return {
|
|
181
|
+
badge: 'Action needed',
|
|
182
|
+
title: 'Pair token is required',
|
|
183
|
+
body: 'This browser profile does not have a saved token yet, so the extension cannot pair with bak.',
|
|
184
|
+
tone: 'error',
|
|
185
|
+
recoverySteps: [
|
|
186
|
+
'Run `bak setup` if you need a fresh token.',
|
|
187
|
+
`Paste the token above, keep CLI port ${state.port}, and click Save settings.`,
|
|
188
|
+
'If the bridge still stays disconnected after saving, click Reconnect bridge below.'
|
|
189
|
+
]
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (state.connectionState === 'manual') {
|
|
194
|
+
return {
|
|
195
|
+
badge: 'Paused',
|
|
196
|
+
title: 'Extension bridge is paused',
|
|
197
|
+
body: 'The bridge was manually disconnected. It will stay idle until you reconnect it.',
|
|
198
|
+
tone: 'warning',
|
|
199
|
+
recoverySteps: [
|
|
200
|
+
'Click Reconnect bridge below when you want the extension live again.',
|
|
201
|
+
'If you changed the token or port, save the new settings first.'
|
|
202
|
+
]
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (runtimeOffline) {
|
|
207
|
+
return {
|
|
208
|
+
badge: 'Runtime offline',
|
|
209
|
+
title: 'The local bak runtime is not reachable',
|
|
210
|
+
body: `The extension cannot reach ${state.wsUrl}, so browser work cannot start yet.`,
|
|
211
|
+
tone: 'warning',
|
|
212
|
+
recoverySteps: [
|
|
213
|
+
`Run \`bak doctor --port ${state.port}\` in PowerShell 7.`,
|
|
214
|
+
'If you just upgraded bak, reload the unpacked extension in chrome://extensions or edge://extensions.',
|
|
215
|
+
'Leave this popup open for a moment after doctor so the bridge can retry.'
|
|
216
|
+
]
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (state.connectionState === 'reconnecting') {
|
|
221
|
+
return {
|
|
222
|
+
badge: 'Retrying',
|
|
223
|
+
title: 'The extension is trying to reconnect',
|
|
224
|
+
body: 'The bridge is retrying in the background. You only need to intervene if the retry loop keeps failing.',
|
|
225
|
+
tone: 'warning',
|
|
226
|
+
recoverySteps: [
|
|
227
|
+
'Wait for the current retry window to finish.',
|
|
228
|
+
'If you changed the token or port, click Save settings.',
|
|
229
|
+
'Use Reconnect bridge below if you want to retry immediately.'
|
|
230
|
+
]
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (state.lastError) {
|
|
235
|
+
return {
|
|
236
|
+
badge: 'Check setup',
|
|
237
|
+
title: 'The bridge needs attention',
|
|
238
|
+
body: `Last error: ${state.lastError}`,
|
|
239
|
+
tone: 'error',
|
|
240
|
+
recoverySteps: [
|
|
241
|
+
`Confirm the saved token and CLI port ${state.port}.`,
|
|
242
|
+
`Run \`bak doctor --port ${state.port}\` from the bak CLI.`,
|
|
243
|
+
'Reload the unpacked extension if the runtime and token both look correct.'
|
|
244
|
+
]
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
badge: 'Waiting',
|
|
250
|
+
title: 'Not connected yet',
|
|
251
|
+
body: 'The extension is ready to pair, but it is still waiting for the local bak runtime to come online.',
|
|
252
|
+
tone: 'neutral',
|
|
253
|
+
recoverySteps: [
|
|
254
|
+
`Run \`bak doctor --port ${state.port}\` or another bak CLI command to wake the runtime.`,
|
|
255
|
+
'Return here and confirm the bridge reconnects automatically.'
|
|
256
|
+
]
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
132
260
|
function renderConnectionDetails(state: PopupState): void {
|
|
133
261
|
connectionStateEl.textContent = describeConnectionState(state.connectionState);
|
|
134
262
|
tokenStateEl.textContent = state.hasToken ? 'configured' : 'missing';
|
|
@@ -160,6 +288,107 @@ function renderConnectionDetails(state: PopupState): void {
|
|
|
160
288
|
}
|
|
161
289
|
}
|
|
162
290
|
|
|
291
|
+
function createMetaRow(label: string, value: string): HTMLDivElement {
|
|
292
|
+
const row = document.createElement('div');
|
|
293
|
+
row.className = 'session-meta-row';
|
|
294
|
+
|
|
295
|
+
const labelEl = document.createElement('span');
|
|
296
|
+
labelEl.className = 'session-meta-label';
|
|
297
|
+
labelEl.textContent = label;
|
|
298
|
+
|
|
299
|
+
const valueEl = document.createElement('span');
|
|
300
|
+
valueEl.className = 'session-meta-value';
|
|
301
|
+
valueEl.textContent = value;
|
|
302
|
+
|
|
303
|
+
row.append(labelEl, valueEl);
|
|
304
|
+
return row;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function renderSessionBindings(state: PopupState['sessionBindings']): void {
|
|
308
|
+
if (state.count === 0) {
|
|
309
|
+
sessionSummaryEl.textContent = 'No remembered sessions';
|
|
310
|
+
} else {
|
|
311
|
+
sessionSummaryEl.textContent =
|
|
312
|
+
`${pluralize(state.count, 'session')}, ` +
|
|
313
|
+
`${pluralize(state.attachedCount, 'attached binding')}, ` +
|
|
314
|
+
`${pluralize(state.detachedCount, 'detached binding')}, ` +
|
|
315
|
+
`${pluralize(state.tabCount, 'tracked tab')}`;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
sessionCardsEl.replaceChildren();
|
|
319
|
+
|
|
320
|
+
if (state.items.length === 0) {
|
|
321
|
+
const empty = document.createElement('div');
|
|
322
|
+
empty.className = 'session-empty';
|
|
323
|
+
empty.textContent = 'No tracked sessions yet. Resolve a session from the bak CLI and it will appear here.';
|
|
324
|
+
sessionCardsEl.appendChild(empty);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
for (const item of state.items) {
|
|
329
|
+
const card = document.createElement('section');
|
|
330
|
+
card.className = 'session-card';
|
|
331
|
+
card.dataset.detached = item.detached ? 'true' : 'false';
|
|
332
|
+
|
|
333
|
+
const header = document.createElement('div');
|
|
334
|
+
header.className = 'session-card-header';
|
|
335
|
+
|
|
336
|
+
const titleWrap = document.createElement('div');
|
|
337
|
+
titleWrap.className = 'session-card-title-wrap';
|
|
338
|
+
|
|
339
|
+
const title = document.createElement('div');
|
|
340
|
+
title.className = 'session-card-title';
|
|
341
|
+
title.textContent = item.label;
|
|
342
|
+
|
|
343
|
+
const subtitle = document.createElement('div');
|
|
344
|
+
subtitle.className = 'session-card-subtitle';
|
|
345
|
+
subtitle.textContent = item.id;
|
|
346
|
+
|
|
347
|
+
titleWrap.append(title, subtitle);
|
|
348
|
+
|
|
349
|
+
const badge = document.createElement('span');
|
|
350
|
+
badge.className = 'session-badge';
|
|
351
|
+
badge.dataset.detached = item.detached ? 'true' : 'false';
|
|
352
|
+
badge.textContent = item.detached ? 'Detached' : 'Attached';
|
|
353
|
+
|
|
354
|
+
header.append(titleWrap, badge);
|
|
355
|
+
|
|
356
|
+
const activeTitle = document.createElement('div');
|
|
357
|
+
activeTitle.className = 'session-active-title';
|
|
358
|
+
activeTitle.textContent = item.activeTabTitle ? truncate(item.activeTabTitle, 72) : 'No active tab title';
|
|
359
|
+
activeTitle.title = item.activeTabTitle ?? '';
|
|
360
|
+
|
|
361
|
+
const activeUrl = document.createElement('div');
|
|
362
|
+
activeUrl.className = 'session-active-url';
|
|
363
|
+
activeUrl.textContent = formatUrl(item.activeTabUrl);
|
|
364
|
+
activeUrl.title = item.activeTabUrl ?? '';
|
|
365
|
+
|
|
366
|
+
const meta = document.createElement('div');
|
|
367
|
+
meta.className = 'session-meta-grid';
|
|
368
|
+
meta.append(
|
|
369
|
+
createMetaRow('Active tab', item.activeTabId === null ? 'none' : `${item.activeTabId}`),
|
|
370
|
+
createMetaRow('Tabs', `${item.tabCount}`),
|
|
371
|
+
createMetaRow('Window', item.windowId === null ? 'none' : `${item.windowId}`),
|
|
372
|
+
createMetaRow('Group', item.groupId === null ? 'none' : `${item.groupId}`),
|
|
373
|
+
createMetaRow(
|
|
374
|
+
'Last binding',
|
|
375
|
+
item.lastBindingUpdateReason
|
|
376
|
+
? `${item.lastBindingUpdateReason} (${formatTimeAgo(item.lastBindingUpdateAt)})`
|
|
377
|
+
: 'none this session'
|
|
378
|
+
)
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
const footer = document.createElement('div');
|
|
382
|
+
footer.className = 'session-card-footer';
|
|
383
|
+
footer.textContent = item.detached
|
|
384
|
+
? 'bak still remembers this session, but its owned tabs or window are missing.'
|
|
385
|
+
: 'The saved binding still points at live browser tabs.';
|
|
386
|
+
|
|
387
|
+
card.append(header, activeTitle, activeUrl, meta, footer);
|
|
388
|
+
sessionCardsEl.appendChild(card);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
163
392
|
function parsePortValue(): number | null {
|
|
164
393
|
const port = Number.parseInt(portInput.value.trim(), 10);
|
|
165
394
|
return Number.isInteger(port) && port > 0 ? port : null;
|
|
@@ -194,83 +423,25 @@ function updateSaveState(state: PopupState | null): void {
|
|
|
194
423
|
saveBtn.textContent = state?.hasToken ? 'Save settings' : 'Save token';
|
|
195
424
|
}
|
|
196
425
|
|
|
197
|
-
function
|
|
198
|
-
const
|
|
199
|
-
const runtimeOffline = combinedError.includes('cannot connect to bak cli');
|
|
200
|
-
|
|
201
|
-
if (state.connected) {
|
|
202
|
-
return {
|
|
203
|
-
text: 'Connected to local bak runtime',
|
|
204
|
-
note: 'Use the bak CLI to start browser work. This popup is mainly for status and configuration.',
|
|
205
|
-
tone: 'success'
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
if (state.connectionState === 'missing-token') {
|
|
210
|
-
return {
|
|
211
|
-
text: 'Pair token is required',
|
|
212
|
-
note: 'Paste a token once, then save it. Future reconnects happen automatically.',
|
|
213
|
-
tone: 'error'
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
if (state.connectionState === 'manual') {
|
|
218
|
-
return {
|
|
219
|
-
text: 'Extension bridge is paused',
|
|
220
|
-
note: 'Normal browser work starts from the bak CLI. Open Advanced only if you need to reconnect manually.',
|
|
221
|
-
tone: 'warning'
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if (runtimeOffline) {
|
|
226
|
-
return {
|
|
227
|
-
text: 'Waiting for local bak runtime',
|
|
228
|
-
note: 'Run any bak command, such as `bak doctor`, and the extension will reconnect automatically.',
|
|
229
|
-
tone: 'warning'
|
|
230
|
-
};
|
|
231
|
-
}
|
|
426
|
+
async function refreshState(): Promise<void> {
|
|
427
|
+
const state = (await chrome.runtime.sendMessage({ type: 'bak.getState' })) as PopupState;
|
|
232
428
|
|
|
233
|
-
if (state.
|
|
234
|
-
return
|
|
235
|
-
text: 'Trying to reconnect',
|
|
236
|
-
note: 'The extension is retrying in the background. You usually do not need to press anything here.',
|
|
237
|
-
tone: 'warning'
|
|
238
|
-
};
|
|
429
|
+
if (!state.ok) {
|
|
430
|
+
return;
|
|
239
431
|
}
|
|
240
432
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
433
|
+
const shouldSyncForm = !isFormDirty(latestState);
|
|
434
|
+
latestState = state;
|
|
435
|
+
if (shouldSyncForm) {
|
|
436
|
+
portInput.value = String(state.port);
|
|
437
|
+
debugRichTextInput.checked = Boolean(state.debugRichText);
|
|
438
|
+
tokenInput.value = '';
|
|
247
439
|
}
|
|
248
440
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
async function refreshState(): Promise<void> {
|
|
257
|
-
const state = (await chrome.runtime.sendMessage({ type: 'bak.getState' })) as PopupState;
|
|
258
|
-
|
|
259
|
-
if (state.ok) {
|
|
260
|
-
const shouldSyncForm = !isFormDirty(latestState);
|
|
261
|
-
latestState = state;
|
|
262
|
-
if (shouldSyncForm) {
|
|
263
|
-
portInput.value = String(state.port);
|
|
264
|
-
debugRichTextInput.checked = Boolean(state.debugRichText);
|
|
265
|
-
tokenInput.value = '';
|
|
266
|
-
}
|
|
267
|
-
renderConnectionDetails(state);
|
|
268
|
-
renderSessionBindings(state.sessionBindings);
|
|
269
|
-
updateSaveState(state);
|
|
270
|
-
const status = describeStatus(state);
|
|
271
|
-
setStatus(status.text, status.tone);
|
|
272
|
-
statusNoteEl.textContent = status.note;
|
|
273
|
-
}
|
|
441
|
+
renderConnectionDetails(state);
|
|
442
|
+
renderSessionBindings(state.sessionBindings);
|
|
443
|
+
updateSaveState(state);
|
|
444
|
+
setStatus(describeStatus(state));
|
|
274
445
|
}
|
|
275
446
|
|
|
276
447
|
saveBtn.addEventListener('click', async () => {
|
|
@@ -278,12 +449,24 @@ saveBtn.addEventListener('click', async () => {
|
|
|
278
449
|
const port = parsePortValue();
|
|
279
450
|
|
|
280
451
|
if (!token && latestState?.hasToken !== true) {
|
|
281
|
-
setStatus(
|
|
452
|
+
setStatus({
|
|
453
|
+
badge: 'Action needed',
|
|
454
|
+
title: 'Pair token is required',
|
|
455
|
+
body: 'Save a token before the extension can reconnect.',
|
|
456
|
+
tone: 'error',
|
|
457
|
+
recoverySteps: ['Paste a valid token above, then click Save settings.']
|
|
458
|
+
});
|
|
282
459
|
return;
|
|
283
460
|
}
|
|
284
461
|
|
|
285
462
|
if (port === null) {
|
|
286
|
-
setStatus(
|
|
463
|
+
setStatus({
|
|
464
|
+
badge: 'Invalid input',
|
|
465
|
+
title: 'CLI port is invalid',
|
|
466
|
+
body: 'Use a positive integer port before saving settings.',
|
|
467
|
+
tone: 'error',
|
|
468
|
+
recoverySteps: ['Correct the port value above and try again.']
|
|
469
|
+
});
|
|
287
470
|
return;
|
|
288
471
|
}
|
|
289
472
|
|