@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/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
+ });
@@ -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
- tabs = await this.waitForWindowTabs(createdWindow.id);
158
- state.tabIds = tabs.map((tab) => tab.id);
159
- if (state.primaryTabId === null) {
160
- state.primaryTabId = tabs[0]?.id ?? null;
161
- }
162
- if (state.activeTabId === null) {
163
- state.activeTabId = tabs.find((tab) => tab.active)?.id ?? tabs[0]?.id ?? null;
164
- }
165
- repairActions.push(created ? 'created-window' : 'recreated-window');
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
- const emptied: SessionBindingRecord = {
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
- const recreatedTabs = await this.waitForWindowTabs(window.id);
753
- const firstTab = recreatedTabs[0] ?? null;
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);