@flrande/bak-extension 0.6.4 → 0.6.6
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 +147 -55
- package/dist/manifest.json +1 -1
- package/dist/popup.global.js +92 -2
- package/dist/popup.html +72 -1
- package/package.json +2 -2
- package/public/popup.html +72 -1
- package/src/background.ts +282 -131
- package/src/popup.ts +135 -11
- package/src/session-binding.ts +44 -50
package/src/popup.ts
CHANGED
|
@@ -3,28 +3,140 @@ const tokenInput = document.getElementById('token') as HTMLInputElement;
|
|
|
3
3
|
const portInput = document.getElementById('port') as HTMLInputElement;
|
|
4
4
|
const debugRichTextInput = document.getElementById('debugRichText') as HTMLInputElement;
|
|
5
5
|
const saveBtn = document.getElementById('save') as HTMLButtonElement;
|
|
6
|
+
const reconnectBtn = document.getElementById('reconnect') as HTMLButtonElement;
|
|
6
7
|
const disconnectBtn = document.getElementById('disconnect') as HTMLButtonElement;
|
|
8
|
+
const connectionStateEl = document.getElementById('connectionState') as HTMLDivElement;
|
|
9
|
+
const tokenStateEl = document.getElementById('tokenState') as HTMLDivElement;
|
|
10
|
+
const reconnectStateEl = document.getElementById('reconnectState') as HTMLDivElement;
|
|
11
|
+
const connectionUrlEl = document.getElementById('connectionUrl') as HTMLDivElement;
|
|
12
|
+
const lastErrorEl = document.getElementById('lastError') as HTMLDivElement;
|
|
13
|
+
const lastBindingUpdateEl = document.getElementById('lastBindingUpdate') as HTMLDivElement;
|
|
14
|
+
const extensionVersionEl = document.getElementById('extensionVersion') as HTMLDivElement;
|
|
15
|
+
const sessionSummaryEl = document.getElementById('sessionSummary') as HTMLDivElement;
|
|
16
|
+
const sessionListEl = document.getElementById('sessionList') as HTMLUListElement;
|
|
17
|
+
|
|
18
|
+
interface PopupState {
|
|
19
|
+
ok: boolean;
|
|
20
|
+
connected: boolean;
|
|
21
|
+
connectionState: 'connected' | 'connecting' | 'reconnecting' | 'disconnected' | 'manual' | 'missing-token';
|
|
22
|
+
hasToken: boolean;
|
|
23
|
+
port: number;
|
|
24
|
+
wsUrl: string;
|
|
25
|
+
debugRichText: boolean;
|
|
26
|
+
lastError: string | null;
|
|
27
|
+
lastErrorAt: number | null;
|
|
28
|
+
lastErrorContext: string | null;
|
|
29
|
+
reconnectAttempt: number;
|
|
30
|
+
nextReconnectInMs: number | null;
|
|
31
|
+
manualDisconnect: boolean;
|
|
32
|
+
extensionVersion: string;
|
|
33
|
+
lastBindingUpdateAt: number | null;
|
|
34
|
+
lastBindingUpdateReason: string | null;
|
|
35
|
+
sessionBindings: {
|
|
36
|
+
count: number;
|
|
37
|
+
attachedCount: number;
|
|
38
|
+
detachedCount: number;
|
|
39
|
+
tabCount: number;
|
|
40
|
+
items: Array<{
|
|
41
|
+
id: string;
|
|
42
|
+
label: string;
|
|
43
|
+
tabCount: number;
|
|
44
|
+
activeTabId: number | null;
|
|
45
|
+
windowId: number | null;
|
|
46
|
+
groupId: number | null;
|
|
47
|
+
detached: boolean;
|
|
48
|
+
}>;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
let latestState: PopupState | null = null;
|
|
7
52
|
|
|
8
53
|
function setStatus(text: string, bad = false): void {
|
|
9
54
|
statusEl.textContent = text;
|
|
10
55
|
statusEl.style.color = bad ? '#dc2626' : '#0f172a';
|
|
11
56
|
}
|
|
12
57
|
|
|
58
|
+
function formatTimeAgo(at: number | null): string {
|
|
59
|
+
if (typeof at !== 'number') {
|
|
60
|
+
return 'never';
|
|
61
|
+
}
|
|
62
|
+
const deltaSeconds = Math.max(0, Math.round((Date.now() - at) / 1000));
|
|
63
|
+
if (deltaSeconds < 5) {
|
|
64
|
+
return 'just now';
|
|
65
|
+
}
|
|
66
|
+
if (deltaSeconds < 60) {
|
|
67
|
+
return `${deltaSeconds}s ago`;
|
|
68
|
+
}
|
|
69
|
+
const deltaMinutes = Math.round(deltaSeconds / 60);
|
|
70
|
+
if (deltaMinutes < 60) {
|
|
71
|
+
return `${deltaMinutes}m ago`;
|
|
72
|
+
}
|
|
73
|
+
const deltaHours = Math.round(deltaMinutes / 60);
|
|
74
|
+
return `${deltaHours}h ago`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function renderSessionBindings(state: PopupState['sessionBindings']): void {
|
|
78
|
+
sessionSummaryEl.textContent = `${state.count} sessions, ${state.attachedCount} attached, ${state.tabCount} tabs, ${state.detachedCount} detached`;
|
|
79
|
+
sessionListEl.replaceChildren();
|
|
80
|
+
for (const item of state.items) {
|
|
81
|
+
const li = document.createElement('li');
|
|
82
|
+
const location = item.windowId === null ? 'no window' : `window ${item.windowId}`;
|
|
83
|
+
const active = item.activeTabId === null ? 'no active tab' : `active ${item.activeTabId}`;
|
|
84
|
+
li.textContent = `${item.label}: ${item.tabCount} tabs, ${location}, ${active}`;
|
|
85
|
+
if (item.detached) {
|
|
86
|
+
li.style.color = '#b45309';
|
|
87
|
+
}
|
|
88
|
+
sessionListEl.appendChild(li);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function renderConnectionDetails(state: PopupState): void {
|
|
93
|
+
connectionStateEl.textContent = state.connectionState;
|
|
94
|
+
tokenStateEl.textContent = state.hasToken ? 'configured' : 'missing';
|
|
95
|
+
connectionUrlEl.textContent = state.wsUrl;
|
|
96
|
+
extensionVersionEl.textContent = state.extensionVersion;
|
|
97
|
+
|
|
98
|
+
if (state.manualDisconnect) {
|
|
99
|
+
reconnectStateEl.textContent = 'manual disconnect';
|
|
100
|
+
} else if (typeof state.nextReconnectInMs === 'number') {
|
|
101
|
+
const seconds = Math.max(0, Math.ceil(state.nextReconnectInMs / 100) / 10);
|
|
102
|
+
reconnectStateEl.textContent = `attempt ${state.reconnectAttempt}, retry in ${seconds}s`;
|
|
103
|
+
} else if (state.connected) {
|
|
104
|
+
reconnectStateEl.textContent = 'connected';
|
|
105
|
+
} else {
|
|
106
|
+
reconnectStateEl.textContent = 'idle';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (state.lastError) {
|
|
110
|
+
const context = state.lastErrorContext ? `${state.lastErrorContext}: ` : '';
|
|
111
|
+
lastErrorEl.textContent = `${context}${state.lastError} (${formatTimeAgo(state.lastErrorAt)})`;
|
|
112
|
+
} else {
|
|
113
|
+
lastErrorEl.textContent = 'none';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (state.lastBindingUpdateReason) {
|
|
117
|
+
lastBindingUpdateEl.textContent = `${state.lastBindingUpdateReason} (${formatTimeAgo(state.lastBindingUpdateAt)})`;
|
|
118
|
+
} else {
|
|
119
|
+
lastBindingUpdateEl.textContent = 'none';
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
13
123
|
async function refreshState(): Promise<void> {
|
|
14
|
-
const state = (await chrome.runtime.sendMessage({ type: 'bak.getState' })) as
|
|
15
|
-
ok: boolean;
|
|
16
|
-
connected: boolean;
|
|
17
|
-
hasToken: boolean;
|
|
18
|
-
port: number;
|
|
19
|
-
debugRichText: boolean;
|
|
20
|
-
lastError: string | null;
|
|
21
|
-
};
|
|
124
|
+
const state = (await chrome.runtime.sendMessage({ type: 'bak.getState' })) as PopupState;
|
|
22
125
|
|
|
23
126
|
if (state.ok) {
|
|
127
|
+
latestState = state;
|
|
24
128
|
portInput.value = String(state.port);
|
|
25
129
|
debugRichTextInput.checked = Boolean(state.debugRichText);
|
|
130
|
+
renderConnectionDetails(state);
|
|
131
|
+
renderSessionBindings(state.sessionBindings);
|
|
26
132
|
if (state.connected) {
|
|
27
133
|
setStatus('Connected to bak CLI');
|
|
134
|
+
} else if (state.connectionState === 'missing-token') {
|
|
135
|
+
setStatus('Pair token is required', true);
|
|
136
|
+
} else if (state.connectionState === 'manual') {
|
|
137
|
+
setStatus('Disconnected manually');
|
|
138
|
+
} else if (state.connectionState === 'reconnecting') {
|
|
139
|
+
setStatus('Reconnecting to bak CLI', true);
|
|
28
140
|
} else if (state.lastError) {
|
|
29
141
|
setStatus(`Disconnected: ${state.lastError}`, true);
|
|
30
142
|
} else {
|
|
@@ -37,7 +149,7 @@ saveBtn.addEventListener('click', async () => {
|
|
|
37
149
|
const token = tokenInput.value.trim();
|
|
38
150
|
const port = Number.parseInt(portInput.value.trim(), 10);
|
|
39
151
|
|
|
40
|
-
if (!token) {
|
|
152
|
+
if (!token && latestState?.hasToken !== true) {
|
|
41
153
|
setStatus('Pair token is required', true);
|
|
42
154
|
return;
|
|
43
155
|
}
|
|
@@ -49,11 +161,17 @@ saveBtn.addEventListener('click', async () => {
|
|
|
49
161
|
|
|
50
162
|
await chrome.runtime.sendMessage({
|
|
51
163
|
type: 'bak.updateConfig',
|
|
52
|
-
token,
|
|
164
|
+
...(token ? { token } : {}),
|
|
53
165
|
port,
|
|
54
166
|
debugRichText: debugRichTextInput.checked
|
|
55
167
|
});
|
|
56
168
|
|
|
169
|
+
tokenInput.value = '';
|
|
170
|
+
await refreshState();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
reconnectBtn.addEventListener('click', async () => {
|
|
174
|
+
await chrome.runtime.sendMessage({ type: 'bak.reconnectNow' });
|
|
57
175
|
await refreshState();
|
|
58
176
|
});
|
|
59
177
|
|
|
@@ -62,4 +180,10 @@ disconnectBtn.addEventListener('click', async () => {
|
|
|
62
180
|
await refreshState();
|
|
63
181
|
});
|
|
64
182
|
|
|
65
|
-
void refreshState();
|
|
183
|
+
void refreshState();
|
|
184
|
+
const refreshInterval = window.setInterval(() => {
|
|
185
|
+
void refreshState();
|
|
186
|
+
}, 1000);
|
|
187
|
+
window.addEventListener('unload', () => {
|
|
188
|
+
window.clearInterval(refreshInterval);
|
|
189
|
+
});
|
package/src/session-binding.ts
CHANGED
|
@@ -16,10 +16,11 @@ export interface SessionBindingTab {
|
|
|
16
16
|
groupId: number | null;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export interface SessionBindingWindow {
|
|
20
|
-
id: number;
|
|
21
|
-
focused: boolean;
|
|
22
|
-
|
|
19
|
+
export interface SessionBindingWindow {
|
|
20
|
+
id: number;
|
|
21
|
+
focused: boolean;
|
|
22
|
+
initialTabId?: number | null;
|
|
23
|
+
}
|
|
23
24
|
|
|
24
25
|
export interface SessionBindingGroup {
|
|
25
26
|
id: number;
|
|
@@ -143,27 +144,31 @@ class SessionBindingManager {
|
|
|
143
144
|
}
|
|
144
145
|
}
|
|
145
146
|
}
|
|
146
|
-
if (!window) {
|
|
147
|
-
const createdWindow = await this.browser.createWindow({
|
|
148
|
-
url: initialUrl,
|
|
149
|
-
focused: options.focus === true
|
|
150
|
-
});
|
|
147
|
+
if (!window) {
|
|
148
|
+
const createdWindow = await this.browser.createWindow({
|
|
149
|
+
url: initialUrl,
|
|
150
|
+
focused: options.focus === true
|
|
151
|
+
});
|
|
151
152
|
state.windowId = createdWindow.id;
|
|
152
|
-
state.groupId = null;
|
|
153
|
-
state.tabIds = [];
|
|
154
|
-
state.activeTabId = null;
|
|
155
|
-
state.primaryTabId = null;
|
|
156
|
-
window = createdWindow;
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
153
|
+
state.groupId = null;
|
|
154
|
+
state.tabIds = [];
|
|
155
|
+
state.activeTabId = null;
|
|
156
|
+
state.primaryTabId = null;
|
|
157
|
+
window = createdWindow;
|
|
158
|
+
const initialTab =
|
|
159
|
+
typeof createdWindow.initialTabId === 'number'
|
|
160
|
+
? await this.waitForTrackedTab(createdWindow.initialTabId, createdWindow.id)
|
|
161
|
+
: null;
|
|
162
|
+
tabs = initialTab ? [initialTab] : await this.waitForWindowTabs(createdWindow.id);
|
|
163
|
+
state.tabIds = tabs.map((tab) => tab.id);
|
|
164
|
+
if (state.primaryTabId === null) {
|
|
165
|
+
state.primaryTabId = initialTab?.id ?? tabs[0]?.id ?? null;
|
|
166
|
+
}
|
|
167
|
+
if (state.activeTabId === null) {
|
|
168
|
+
state.activeTabId = tabs.find((tab) => tab.active)?.id ?? initialTab?.id ?? tabs[0]?.id ?? null;
|
|
169
|
+
}
|
|
170
|
+
repairActions.push(created ? 'created-window' : 'recreated-window');
|
|
171
|
+
}
|
|
167
172
|
|
|
168
173
|
tabs = tabs.length > 0 ? tabs : await this.readTrackedTabs(state.tabIds, state.windowId);
|
|
169
174
|
const recoveredTabs = await this.recoverBindingTabs(state, tabs);
|
|
@@ -451,22 +456,9 @@ class SessionBindingManager {
|
|
|
451
456
|
const remainingTabIds = ensured.binding.tabIds.filter((candidate) => candidate !== resolvedTabId);
|
|
452
457
|
|
|
453
458
|
if (remainingTabIds.length === 0) {
|
|
454
|
-
|
|
455
|
-
id: ensured.binding.id,
|
|
456
|
-
label: ensured.binding.label,
|
|
457
|
-
color: ensured.binding.color,
|
|
458
|
-
windowId: null,
|
|
459
|
-
groupId: null,
|
|
460
|
-
tabIds: [],
|
|
461
|
-
activeTabId: null,
|
|
462
|
-
primaryTabId: null
|
|
463
|
-
};
|
|
464
|
-
await this.storage.save(emptied);
|
|
459
|
+
await this.storage.delete(ensured.binding.id);
|
|
465
460
|
return {
|
|
466
|
-
binding:
|
|
467
|
-
...emptied,
|
|
468
|
-
tabs: []
|
|
469
|
-
},
|
|
461
|
+
binding: null,
|
|
470
462
|
closedTabId: resolvedTabId
|
|
471
463
|
};
|
|
472
464
|
}
|
|
@@ -738,19 +730,21 @@ class SessionBindingManager {
|
|
|
738
730
|
};
|
|
739
731
|
}
|
|
740
732
|
|
|
741
|
-
private async moveBindingIntoDedicatedWindow(
|
|
742
|
-
state: SessionBindingRecord,
|
|
743
|
-
ownership: SessionBindingWindowOwnership,
|
|
744
|
-
initialUrl: string
|
|
745
|
-
): Promise<{ window: SessionBindingWindow; tabs: SessionBindingTab[] }> {
|
|
733
|
+
private async moveBindingIntoDedicatedWindow(
|
|
734
|
+
state: SessionBindingRecord,
|
|
735
|
+
ownership: SessionBindingWindowOwnership,
|
|
736
|
+
initialUrl: string
|
|
737
|
+
): Promise<{ window: SessionBindingWindow; tabs: SessionBindingTab[] }> {
|
|
746
738
|
const sourceTabs = this.orderSessionBindingTabsForMigration(state, ownership.bindingTabs);
|
|
747
739
|
const seedUrl = sourceTabs[0]?.url ?? initialUrl;
|
|
748
|
-
const window = await this.browser.createWindow({
|
|
749
|
-
url: seedUrl || DEFAULT_SESSION_BINDING_URL,
|
|
750
|
-
focused: false
|
|
751
|
-
});
|
|
752
|
-
|
|
753
|
-
|
|
740
|
+
const window = await this.browser.createWindow({
|
|
741
|
+
url: seedUrl || DEFAULT_SESSION_BINDING_URL,
|
|
742
|
+
focused: false
|
|
743
|
+
});
|
|
744
|
+
const initialTab =
|
|
745
|
+
typeof window.initialTabId === 'number' ? await this.waitForTrackedTab(window.initialTabId, window.id) : null;
|
|
746
|
+
const recreatedTabs = initialTab ? [initialTab] : await this.waitForWindowTabs(window.id);
|
|
747
|
+
const firstTab = recreatedTabs[0] ?? null;
|
|
754
748
|
const tabIdMap = new Map<number, number>();
|
|
755
749
|
if (sourceTabs[0] && firstTab) {
|
|
756
750
|
tabIdMap.set(sourceTabs[0].id, firstTab.id);
|