@flrande/bak-extension 0.6.6 → 0.6.8

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.
@@ -1 +1 @@
1
- 2026-03-13T07:15:48.660Z
1
+ 2026-03-13T08:51:15.417Z
@@ -62,7 +62,7 @@
62
62
  // package.json
63
63
  var package_default = {
64
64
  name: "@flrande/bak-extension",
65
- version: "0.6.6",
65
+ version: "0.6.8",
66
66
  type: "module",
67
67
  scripts: {
68
68
  build: "tsup src/background.ts src/content.ts src/popup.ts --format iife --out-dir dist --clean && node scripts/copy-assets.mjs",
@@ -563,26 +563,37 @@
563
563
  }
564
564
  }
565
565
  if (!window2) {
566
- const createdWindow = await this.browser.createWindow({
567
- url: initialUrl,
568
- focused: options.focus === true
569
- });
570
- state.windowId = createdWindow.id;
571
- state.groupId = null;
572
- state.tabIds = [];
573
- state.activeTabId = null;
574
- state.primaryTabId = null;
575
- window2 = createdWindow;
576
- const initialTab = typeof createdWindow.initialTabId === "number" ? await this.waitForTrackedTab(createdWindow.initialTabId, createdWindow.id) : null;
577
- tabs = initialTab ? [initialTab] : await this.waitForWindowTabs(createdWindow.id);
578
- state.tabIds = tabs.map((tab) => tab.id);
579
- if (state.primaryTabId === null) {
580
- state.primaryTabId = initialTab?.id ?? tabs[0]?.id ?? null;
581
- }
582
- if (state.activeTabId === null) {
583
- state.activeTabId = tabs.find((tab) => tab.active)?.id ?? initialTab?.id ?? tabs[0]?.id ?? null;
584
- }
585
- repairActions.push(created ? "created-window" : "recreated-window");
566
+ const sharedWindow = await this.findSharedBindingWindow(bindingId);
567
+ if (sharedWindow) {
568
+ state.windowId = sharedWindow.id;
569
+ state.groupId = null;
570
+ state.tabIds = [];
571
+ state.activeTabId = null;
572
+ state.primaryTabId = null;
573
+ window2 = sharedWindow;
574
+ repairActions.push("attached-shared-window");
575
+ } else {
576
+ const createdWindow = await this.browser.createWindow({
577
+ url: initialUrl,
578
+ focused: options.focus === true
579
+ });
580
+ state.windowId = createdWindow.id;
581
+ state.groupId = null;
582
+ state.tabIds = [];
583
+ state.activeTabId = null;
584
+ state.primaryTabId = null;
585
+ window2 = createdWindow;
586
+ const initialTab = typeof createdWindow.initialTabId === "number" ? await this.waitForTrackedTab(createdWindow.initialTabId, createdWindow.id) : null;
587
+ tabs = initialTab ? [initialTab] : await this.waitForWindowTabs(createdWindow.id);
588
+ state.tabIds = tabs.map((tab) => tab.id);
589
+ if (state.primaryTabId === null) {
590
+ state.primaryTabId = initialTab?.id ?? tabs[0]?.id ?? null;
591
+ }
592
+ if (state.activeTabId === null) {
593
+ state.activeTabId = tabs.find((tab) => tab.active)?.id ?? initialTab?.id ?? tabs[0]?.id ?? null;
594
+ }
595
+ repairActions.push(created ? "created-window" : "recreated-window");
596
+ }
586
597
  }
587
598
  tabs = tabs.length > 0 ? tabs : await this.readTrackedTabs(state.tabIds, state.windowId);
588
599
  const recoveredTabs = await this.recoverBindingTabs(state, tabs);
@@ -596,12 +607,12 @@
596
607
  state.tabIds = tabs.map((tab) => tab.id);
597
608
  if (state.windowId !== null) {
598
609
  const ownership = await this.inspectBindingWindowOwnership(state, state.windowId);
599
- if (ownership.foreignTabs.length > 0) {
600
- const migrated = await this.moveBindingIntoDedicatedWindow(state, ownership, initialUrl);
610
+ if (ownership.foreignTabs.length > 0 && ownership.bindingTabs.length > 0) {
611
+ const migrated = await this.moveBindingIntoBakWindow(state, ownership, initialUrl);
601
612
  window2 = migrated.window;
602
613
  tabs = migrated.tabs;
603
614
  state.tabIds = tabs.map((tab) => tab.id);
604
- repairActions.push("migrated-dirty-window");
615
+ repairActions.push("evacuated-foreign-window");
605
616
  }
606
617
  }
607
618
  if (tabs.length === 0) {
@@ -698,7 +709,7 @@
698
709
  const desiredUrl = options.url ?? DEFAULT_SESSION_BINDING_URL;
699
710
  let reusablePrimaryTab = await this.resolveReusablePrimaryTab(
700
711
  state,
701
- ensured.created || ensured.repairActions.includes("recreated-window") || ensured.repairActions.includes("created-primary-tab") || ensured.repairActions.includes("migrated-dirty-window")
712
+ ensured.created || ensured.repairActions.includes("recreated-window") || ensured.repairActions.includes("created-primary-tab")
702
713
  );
703
714
  let createdTab;
704
715
  try {
@@ -831,29 +842,32 @@
831
842
  return { ok: true, binding: refreshed.binding };
832
843
  }
833
844
  async closeTab(bindingId, tabId) {
834
- const ensured = await this.ensureBinding({ bindingId, focus: false });
835
- const resolvedTabId = typeof tabId === "number" ? tabId : ensured.binding.activeTabId ?? ensured.binding.primaryTabId ?? ensured.binding.tabs[0]?.id;
836
- if (typeof resolvedTabId !== "number" || !ensured.binding.tabIds.includes(resolvedTabId)) {
845
+ const binding = await this.inspectBinding(bindingId);
846
+ if (!binding) {
847
+ throw new Error(`Binding ${bindingId} does not exist`);
848
+ }
849
+ const resolvedTabId = typeof tabId === "number" ? tabId : binding.activeTabId ?? binding.primaryTabId ?? binding.tabs[0]?.id;
850
+ if (typeof resolvedTabId !== "number" || !binding.tabIds.includes(resolvedTabId)) {
837
851
  throw new Error(`Tab ${tabId ?? "active"} does not belong to binding ${bindingId}`);
838
852
  }
839
853
  await this.browser.closeTab(resolvedTabId);
840
- const remainingTabIds = ensured.binding.tabIds.filter((candidate) => candidate !== resolvedTabId);
854
+ const remainingTabIds = binding.tabIds.filter((candidate) => candidate !== resolvedTabId);
841
855
  if (remainingTabIds.length === 0) {
842
- await this.storage.delete(ensured.binding.id);
856
+ await this.storage.delete(binding.id);
843
857
  return {
844
858
  binding: null,
845
859
  closedTabId: resolvedTabId
846
860
  };
847
861
  }
848
862
  const tabs = await this.readLooseTrackedTabs(remainingTabIds);
849
- const nextPrimaryTabId = ensured.binding.primaryTabId === resolvedTabId ? tabs[0]?.id ?? null : ensured.binding.primaryTabId;
850
- const nextActiveTabId = ensured.binding.activeTabId === resolvedTabId ? tabs.find((candidate) => candidate.active)?.id ?? nextPrimaryTabId ?? tabs[0]?.id ?? null : ensured.binding.activeTabId;
863
+ const nextPrimaryTabId = binding.primaryTabId === resolvedTabId ? tabs[0]?.id ?? null : binding.primaryTabId;
864
+ const nextActiveTabId = binding.activeTabId === resolvedTabId ? tabs.find((candidate) => candidate.active)?.id ?? nextPrimaryTabId ?? tabs[0]?.id ?? null : binding.activeTabId;
851
865
  const nextState = {
852
- id: ensured.binding.id,
853
- label: ensured.binding.label,
854
- color: ensured.binding.color,
855
- windowId: tabs[0]?.windowId ?? ensured.binding.windowId,
856
- groupId: tabs[0]?.groupId ?? ensured.binding.groupId,
866
+ id: binding.id,
867
+ label: binding.label,
868
+ color: binding.color,
869
+ windowId: tabs[0]?.windowId ?? binding.windowId,
870
+ groupId: tabs[0]?.groupId ?? binding.groupId,
857
871
  tabIds: tabs.map((candidate) => candidate.id),
858
872
  activeTabId: nextActiveTabId,
859
873
  primaryTabId: nextPrimaryTabId
@@ -882,7 +896,7 @@
882
896
  return { ok: true };
883
897
  }
884
898
  await this.storage.delete(bindingId);
885
- const trackedTabs = await this.readLooseTrackedTabs(this.collectCandidateTabIds(state));
899
+ const trackedTabs = await this.collectBindingTabsForClose(state);
886
900
  for (const trackedTab of trackedTabs) {
887
901
  try {
888
902
  await this.browser.closeTab(trackedTab.id);
@@ -1069,24 +1083,59 @@
1069
1083
  }
1070
1084
  async inspectBindingWindowOwnership(state, windowId) {
1071
1085
  const windowTabs = await this.waitForWindowTabs(windowId, 500);
1072
- const trackedIds = new Set(this.collectCandidateTabIds(state));
1086
+ const bindingTabIds = new Set(this.collectCandidateTabIds(state));
1087
+ const peerBindings = (await this.storage.list()).filter((candidate) => candidate.id !== state.id);
1088
+ const peerTabIds = /* @__PURE__ */ new Set();
1089
+ const peerGroupIds = /* @__PURE__ */ new Set();
1090
+ for (const peer of peerBindings) {
1091
+ for (const tabId of this.collectCandidateTabIds(peer)) {
1092
+ peerTabIds.add(tabId);
1093
+ }
1094
+ if (peer.groupId !== null) {
1095
+ peerGroupIds.add(peer.groupId);
1096
+ }
1097
+ }
1098
+ const bindingTabs = [];
1099
+ const sharedBindingTabs = [];
1100
+ const foreignTabs = [];
1101
+ for (const tab of windowTabs) {
1102
+ if (bindingTabIds.has(tab.id) || state.groupId !== null && tab.groupId === state.groupId) {
1103
+ bindingTabs.push(tab);
1104
+ continue;
1105
+ }
1106
+ if (peerTabIds.has(tab.id) || tab.groupId !== null && peerGroupIds.has(tab.groupId)) {
1107
+ sharedBindingTabs.push(tab);
1108
+ continue;
1109
+ }
1110
+ foreignTabs.push(tab);
1111
+ }
1073
1112
  return {
1074
- bindingTabs: windowTabs.filter((tab) => trackedIds.has(tab.id) || state.groupId !== null && tab.groupId === state.groupId),
1075
- foreignTabs: windowTabs.filter((tab) => !trackedIds.has(tab.id) && (state.groupId === null || tab.groupId !== state.groupId))
1113
+ bindingTabs,
1114
+ sharedBindingTabs,
1115
+ foreignTabs
1076
1116
  };
1077
1117
  }
1078
- async moveBindingIntoDedicatedWindow(state, ownership, initialUrl) {
1118
+ async moveBindingIntoBakWindow(state, ownership, initialUrl) {
1079
1119
  const sourceTabs = this.orderSessionBindingTabsForMigration(state, ownership.bindingTabs);
1080
1120
  const seedUrl = sourceTabs[0]?.url ?? initialUrl;
1081
- const window2 = await this.browser.createWindow({
1121
+ const sharedWindow = await this.findSharedBindingWindow(state.id, state.windowId === null ? [] : [state.windowId]);
1122
+ const window2 = sharedWindow ?? await this.browser.createWindow({
1082
1123
  url: seedUrl || DEFAULT_SESSION_BINDING_URL,
1083
1124
  focused: false
1084
1125
  });
1085
- const initialTab = typeof window2.initialTabId === "number" ? await this.waitForTrackedTab(window2.initialTabId, window2.id) : null;
1086
- const recreatedTabs = initialTab ? [initialTab] : await this.waitForWindowTabs(window2.id);
1087
- const firstTab = recreatedTabs[0] ?? null;
1126
+ const initialTab = sharedWindow || typeof window2.initialTabId !== "number" ? null : await this.waitForTrackedTab(window2.initialTabId, window2.id);
1127
+ const recreatedTabs = [];
1088
1128
  const tabIdMap = /* @__PURE__ */ new Map();
1089
- if (sourceTabs[0] && firstTab) {
1129
+ if (sourceTabs[0]) {
1130
+ const firstTab = initialTab ? await this.browser.updateTab(initialTab.id, {
1131
+ url: sourceTabs[0].url,
1132
+ active: false
1133
+ }) : await this.createBindingTab({
1134
+ windowId: window2.id,
1135
+ url: sourceTabs[0].url,
1136
+ active: false
1137
+ });
1138
+ recreatedTabs.push(firstTab);
1090
1139
  tabIdMap.set(sourceTabs[0].id, firstTab.id);
1091
1140
  }
1092
1141
  for (const sourceTab of sourceTabs.slice(1)) {
@@ -1098,7 +1147,7 @@
1098
1147
  recreatedTabs.push(recreated);
1099
1148
  tabIdMap.set(sourceTab.id, recreated.id);
1100
1149
  }
1101
- const nextPrimaryTabId = (state.primaryTabId !== null ? tabIdMap.get(state.primaryTabId) : void 0) ?? firstTab?.id ?? recreatedTabs[0]?.id ?? null;
1150
+ const nextPrimaryTabId = (state.primaryTabId !== null ? tabIdMap.get(state.primaryTabId) : void 0) ?? recreatedTabs[0]?.id ?? null;
1102
1151
  const nextActiveTabId = (state.activeTabId !== null ? tabIdMap.get(state.activeTabId) : void 0) ?? nextPrimaryTabId ?? recreatedTabs[0]?.id ?? null;
1103
1152
  if (nextActiveTabId !== null) {
1104
1153
  await this.browser.updateTab(nextActiveTabId, { active: true });
@@ -1113,7 +1162,10 @@
1113
1162
  tabIds: [...state.tabIds]
1114
1163
  });
1115
1164
  for (const bindingTab of ownership.bindingTabs) {
1116
- await this.browser.closeTab(bindingTab.id);
1165
+ try {
1166
+ await this.browser.closeTab(bindingTab.id);
1167
+ } catch {
1168
+ }
1117
1169
  }
1118
1170
  return {
1119
1171
  window: window2,
@@ -1145,6 +1197,57 @@
1145
1197
  }
1146
1198
  return ordered;
1147
1199
  }
1200
+ async findSharedBindingWindow(bindingId, excludedWindowIds = []) {
1201
+ const peers = (await this.storage.list()).filter((candidate) => candidate.id !== bindingId);
1202
+ const candidateWindowIds = [];
1203
+ const peerTabIds = /* @__PURE__ */ new Set();
1204
+ const peerGroupIds = /* @__PURE__ */ new Set();
1205
+ const pushWindowId = (windowId) => {
1206
+ if (typeof windowId !== "number") {
1207
+ return;
1208
+ }
1209
+ if (excludedWindowIds.includes(windowId) || candidateWindowIds.includes(windowId)) {
1210
+ return;
1211
+ }
1212
+ candidateWindowIds.push(windowId);
1213
+ };
1214
+ for (const peer of peers) {
1215
+ pushWindowId(peer.windowId);
1216
+ if (peer.groupId !== null) {
1217
+ peerGroupIds.add(peer.groupId);
1218
+ const group = await this.waitForGroup(peer.groupId, 300);
1219
+ pushWindowId(group?.windowId);
1220
+ }
1221
+ const trackedTabIds = this.collectCandidateTabIds(peer);
1222
+ for (const trackedTabId of trackedTabIds) {
1223
+ peerTabIds.add(trackedTabId);
1224
+ }
1225
+ const trackedTabs = await this.readLooseTrackedTabs(trackedTabIds);
1226
+ for (const tab of trackedTabs) {
1227
+ pushWindowId(tab.windowId);
1228
+ }
1229
+ }
1230
+ for (const windowId of candidateWindowIds) {
1231
+ const window2 = await this.waitForWindow(windowId, 300);
1232
+ if (window2) {
1233
+ const windowTabs = await this.waitForWindowTabs(window2.id, 300);
1234
+ const ownedTabs = windowTabs.filter(
1235
+ (tab) => peerTabIds.has(tab.id) || tab.groupId !== null && peerGroupIds.has(tab.groupId)
1236
+ );
1237
+ if (ownedTabs.length === 0) {
1238
+ continue;
1239
+ }
1240
+ const foreignTabs = windowTabs.filter(
1241
+ (tab) => !peerTabIds.has(tab.id) && (tab.groupId === null || !peerGroupIds.has(tab.groupId))
1242
+ );
1243
+ if (foreignTabs.length > 0) {
1244
+ continue;
1245
+ }
1246
+ return window2;
1247
+ }
1248
+ }
1249
+ return null;
1250
+ }
1148
1251
  async recoverBindingTabs(state, existingTabs) {
1149
1252
  if (state.windowId === null) {
1150
1253
  return existingTabs;
@@ -1171,6 +1274,19 @@
1171
1274
  }
1172
1275
  return existingTabs;
1173
1276
  }
1277
+ async collectBindingTabsForClose(state) {
1278
+ const trackedTabs = await this.readLooseTrackedTabs(this.collectCandidateTabIds(state));
1279
+ if (state.windowId === null || state.groupId === null) {
1280
+ return trackedTabs;
1281
+ }
1282
+ const windowTabs = await this.waitForWindowTabs(state.windowId, 300);
1283
+ const groupedTabs = windowTabs.filter((tab) => tab.groupId === state.groupId);
1284
+ const merged = /* @__PURE__ */ new Map();
1285
+ for (const tab of [...trackedTabs, ...groupedTabs]) {
1286
+ merged.set(tab.id, tab);
1287
+ }
1288
+ return [...merged.values()];
1289
+ }
1174
1290
  async createBindingTab(options) {
1175
1291
  if (options.windowId === null) {
1176
1292
  throw new Error("Binding window is unavailable");
@@ -1200,6 +1316,7 @@
1200
1316
  return null;
1201
1317
  }
1202
1318
  let tabs = await this.readTrackedTabs(state.tabIds, state.windowId);
1319
+ tabs = await this.recoverBindingTabs(state, tabs);
1203
1320
  const activeTab = state.activeTabId !== null ? await this.waitForTrackedTab(state.activeTabId, state.windowId, 300) : null;
1204
1321
  if (activeTab && !tabs.some((tab) => tab.id === activeTab.id)) {
1205
1322
  tabs = [...tabs, activeTab];
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Browser Agent Kit",
4
- "version": "0.6.6",
4
+ "version": "0.6.8",
5
5
  "action": {
6
6
  "default_popup": "popup.html"
7
7
  },
@@ -2,10 +2,12 @@
2
2
  (() => {
3
3
  // src/popup.ts
4
4
  var statusEl = document.getElementById("status");
5
+ var statusNoteEl = document.getElementById("statusNote");
5
6
  var tokenInput = document.getElementById("token");
6
7
  var portInput = document.getElementById("port");
7
8
  var debugRichTextInput = document.getElementById("debugRichText");
8
9
  var saveBtn = document.getElementById("save");
10
+ var saveRowEl = document.getElementById("saveRow");
9
11
  var reconnectBtn = document.getElementById("reconnect");
10
12
  var disconnectBtn = document.getElementById("disconnect");
11
13
  var connectionStateEl = document.getElementById("connectionState");
@@ -18,9 +20,24 @@
18
20
  var sessionSummaryEl = document.getElementById("sessionSummary");
19
21
  var sessionListEl = document.getElementById("sessionList");
20
22
  var latestState = null;
21
- function setStatus(text, bad = false) {
23
+ function setStatus(text, tone = "neutral") {
22
24
  statusEl.textContent = text;
23
- statusEl.style.color = bad ? "#dc2626" : "#0f172a";
25
+ if (tone === "success") {
26
+ statusEl.style.color = "#166534";
27
+ return;
28
+ }
29
+ if (tone === "warning") {
30
+ statusEl.style.color = "#b45309";
31
+ return;
32
+ }
33
+ if (tone === "error") {
34
+ statusEl.style.color = "#dc2626";
35
+ return;
36
+ }
37
+ statusEl.style.color = "#0f172a";
38
+ }
39
+ function pluralize(count, singular, plural = `${singular}s`) {
40
+ return `${count} ${count === 1 ? singular : plural}`;
24
41
  }
25
42
  function formatTimeAgo(at) {
26
43
  if (typeof at !== "number") {
@@ -41,7 +58,11 @@
41
58
  return `${deltaHours}h ago`;
42
59
  }
43
60
  function renderSessionBindings(state) {
44
- sessionSummaryEl.textContent = `${state.count} sessions, ${state.attachedCount} attached, ${state.tabCount} tabs, ${state.detachedCount} detached`;
61
+ if (state.count === 0) {
62
+ sessionSummaryEl.textContent = "No remembered sessions";
63
+ } else {
64
+ sessionSummaryEl.textContent = `${pluralize(state.count, "session")}, ${pluralize(state.attachedCount, "attached binding")}, ${pluralize(state.tabCount, "tab")}, ${pluralize(state.detachedCount, "detached binding")}`;
65
+ }
45
66
  sessionListEl.replaceChildren();
46
67
  for (const item of state.items) {
47
68
  const li = document.createElement("li");
@@ -54,8 +75,25 @@
54
75
  sessionListEl.appendChild(li);
55
76
  }
56
77
  }
78
+ function describeConnectionState(connectionState) {
79
+ switch (connectionState) {
80
+ case "connected":
81
+ return "connected";
82
+ case "connecting":
83
+ return "waiting for runtime";
84
+ case "reconnecting":
85
+ return "retrying connection";
86
+ case "manual":
87
+ return "manually disconnected";
88
+ case "missing-token":
89
+ return "token required";
90
+ case "disconnected":
91
+ default:
92
+ return "disconnected";
93
+ }
94
+ }
57
95
  function renderConnectionDetails(state) {
58
- connectionStateEl.textContent = state.connectionState;
96
+ connectionStateEl.textContent = describeConnectionState(state.connectionState);
59
97
  tokenStateEl.textContent = state.hasToken ? "configured" : "missing";
60
98
  connectionUrlEl.textContent = state.wsUrl;
61
99
  extensionVersionEl.textContent = state.extensionVersion;
@@ -81,38 +119,110 @@
81
119
  lastBindingUpdateEl.textContent = "none";
82
120
  }
83
121
  }
122
+ function parsePortValue() {
123
+ const port = Number.parseInt(portInput.value.trim(), 10);
124
+ return Number.isInteger(port) && port > 0 ? port : null;
125
+ }
126
+ function isFormDirty(state) {
127
+ if (!state) {
128
+ return tokenInput.value.trim().length > 0;
129
+ }
130
+ return tokenInput.value.trim().length > 0 || portInput.value.trim() !== String(state.port) || debugRichTextInput.checked !== Boolean(state.debugRichText);
131
+ }
132
+ function getConfigValidationMessage(state) {
133
+ if (!tokenInput.value.trim() && state?.hasToken !== true) {
134
+ return "Pair token is required";
135
+ }
136
+ if (parsePortValue() === null) {
137
+ return "Port is invalid";
138
+ }
139
+ return null;
140
+ }
141
+ function updateSaveState(state) {
142
+ const dirty = isFormDirty(state);
143
+ const validationError = getConfigValidationMessage(state);
144
+ saveRowEl.hidden = !dirty;
145
+ saveBtn.disabled = !dirty || validationError !== null;
146
+ saveBtn.textContent = state?.hasToken ? "Save settings" : "Save token";
147
+ }
148
+ function describeStatus(state) {
149
+ const combinedError = `${state.lastErrorContext ?? ""} ${state.lastError ?? ""}`.toLowerCase();
150
+ const runtimeOffline = combinedError.includes("cannot connect to bak cli");
151
+ if (state.connected) {
152
+ return {
153
+ text: "Connected to local bak runtime",
154
+ note: "Use the bak CLI to start browser work. This popup is mainly for status and configuration.",
155
+ tone: "success"
156
+ };
157
+ }
158
+ if (state.connectionState === "missing-token") {
159
+ return {
160
+ text: "Pair token is required",
161
+ note: "Paste a token once, then save it. Future reconnects happen automatically.",
162
+ tone: "error"
163
+ };
164
+ }
165
+ if (state.connectionState === "manual") {
166
+ return {
167
+ text: "Extension bridge is paused",
168
+ note: "Normal browser work starts from the bak CLI. Open Advanced only if you need to reconnect manually.",
169
+ tone: "warning"
170
+ };
171
+ }
172
+ if (runtimeOffline) {
173
+ return {
174
+ text: "Waiting for local bak runtime",
175
+ note: "Run any bak command, such as `bak doctor`, and the extension will reconnect automatically.",
176
+ tone: "warning"
177
+ };
178
+ }
179
+ if (state.connectionState === "reconnecting") {
180
+ return {
181
+ text: "Trying to reconnect",
182
+ note: "The extension is retrying in the background. You usually do not need to press anything here.",
183
+ tone: "warning"
184
+ };
185
+ }
186
+ if (state.lastError) {
187
+ return {
188
+ text: "Connection problem",
189
+ note: "Check the last error below. The extension keeps retrying automatically unless you disconnect it manually.",
190
+ tone: "error"
191
+ };
192
+ }
193
+ return {
194
+ text: "Not connected yet",
195
+ note: "Once the local bak runtime is available, the extension reconnects automatically.",
196
+ tone: "neutral"
197
+ };
198
+ }
84
199
  async function refreshState() {
85
200
  const state = await chrome.runtime.sendMessage({ type: "bak.getState" });
86
201
  if (state.ok) {
202
+ const shouldSyncForm = !isFormDirty(latestState);
87
203
  latestState = state;
88
- portInput.value = String(state.port);
89
- debugRichTextInput.checked = Boolean(state.debugRichText);
204
+ if (shouldSyncForm) {
205
+ portInput.value = String(state.port);
206
+ debugRichTextInput.checked = Boolean(state.debugRichText);
207
+ tokenInput.value = "";
208
+ }
90
209
  renderConnectionDetails(state);
91
210
  renderSessionBindings(state.sessionBindings);
92
- if (state.connected) {
93
- setStatus("Connected to bak CLI");
94
- } else if (state.connectionState === "missing-token") {
95
- setStatus("Pair token is required", true);
96
- } else if (state.connectionState === "manual") {
97
- setStatus("Disconnected manually");
98
- } else if (state.connectionState === "reconnecting") {
99
- setStatus("Reconnecting to bak CLI", true);
100
- } else if (state.lastError) {
101
- setStatus(`Disconnected: ${state.lastError}`, true);
102
- } else {
103
- setStatus("Disconnected");
104
- }
211
+ updateSaveState(state);
212
+ const status = describeStatus(state);
213
+ setStatus(status.text, status.tone);
214
+ statusNoteEl.textContent = status.note;
105
215
  }
106
216
  }
107
217
  saveBtn.addEventListener("click", async () => {
108
218
  const token = tokenInput.value.trim();
109
- const port = Number.parseInt(portInput.value.trim(), 10);
219
+ const port = parsePortValue();
110
220
  if (!token && latestState?.hasToken !== true) {
111
- setStatus("Pair token is required", true);
221
+ setStatus("Pair token is required", "error");
112
222
  return;
113
223
  }
114
- if (!Number.isInteger(port) || port <= 0) {
115
- setStatus("Port is invalid", true);
224
+ if (port === null) {
225
+ setStatus("Port is invalid", "error");
116
226
  return;
117
227
  }
118
228
  await chrome.runtime.sendMessage({
@@ -132,6 +242,14 @@
132
242
  await chrome.runtime.sendMessage({ type: "bak.disconnect" });
133
243
  await refreshState();
134
244
  });
245
+ for (const element of [tokenInput, portInput, debugRichTextInput]) {
246
+ element.addEventListener("input", () => {
247
+ updateSaveState(latestState);
248
+ });
249
+ element.addEventListener("change", () => {
250
+ updateSaveState(latestState);
251
+ });
252
+ }
135
253
  void refreshState();
136
254
  var refreshInterval = window.setInterval(() => {
137
255
  void refreshState();
package/dist/popup.html CHANGED
@@ -54,6 +54,9 @@
54
54
  gap: 8px;
55
55
  margin-top: 12px;
56
56
  }
57
+ .row[hidden] {
58
+ display: none;
59
+ }
57
60
  button {
58
61
  flex: 1;
59
62
  border: none;
@@ -62,6 +65,10 @@
62
65
  font-size: 12px;
63
66
  cursor: pointer;
64
67
  }
68
+ button:disabled {
69
+ opacity: 0.55;
70
+ cursor: default;
71
+ }
65
72
  #save {
66
73
  background: #0f172a;
67
74
  color: #fff;
@@ -79,6 +86,12 @@
79
86
  font-size: 12px;
80
87
  font-weight: 600;
81
88
  }
89
+ #statusNote {
90
+ margin-top: 4px;
91
+ font-size: 11px;
92
+ line-height: 1.45;
93
+ color: #475569;
94
+ }
82
95
  .panel {
83
96
  margin-top: 12px;
84
97
  padding: 10px;
@@ -120,6 +133,30 @@
120
133
  font-size: 11px;
121
134
  color: #334155;
122
135
  }
136
+ .hint.compact {
137
+ margin-top: 8px;
138
+ }
139
+ details.panel {
140
+ padding-bottom: 12px;
141
+ }
142
+ details.panel summary {
143
+ cursor: pointer;
144
+ font-size: 12px;
145
+ font-weight: 600;
146
+ list-style: none;
147
+ }
148
+ details.panel summary::-webkit-details-marker {
149
+ display: none;
150
+ }
151
+ details.panel summary::after {
152
+ content: "Show";
153
+ float: right;
154
+ font-size: 11px;
155
+ color: #64748b;
156
+ }
157
+ details.panel[open] summary::after {
158
+ content: "Hide";
159
+ }
123
160
  </style>
124
161
  </head>
125
162
  <body>
@@ -136,14 +173,11 @@
136
173
  <input id="debugRichText" type="checkbox" />
137
174
  <span class="toggle-text">Allow richer text capture for debugging (still redacted, off by default)</span>
138
175
  </label>
139
- <div class="row">
140
- <button id="save">Save & Connect</button>
141
- <button id="reconnect">Reconnect</button>
142
- </div>
143
- <div class="row">
144
- <button id="disconnect">Disconnect</button>
145
- </div>
146
176
  <div id="status">Checking...</div>
177
+ <div id="statusNote">The extension reconnects automatically when the local bak runtime wakes up.</div>
178
+ <div class="row" id="saveRow" hidden>
179
+ <button id="save">Save settings</button>
180
+ </div>
147
181
  <div class="panel">
148
182
  <h2>Connection</h2>
149
183
  <dl class="meta-grid">
@@ -171,6 +205,14 @@
171
205
  </dl>
172
206
  <ul id="sessionList"></ul>
173
207
  </div>
208
+ <details class="panel" id="advancedPanel">
209
+ <summary>Advanced bridge controls</summary>
210
+ <div class="hint compact">These controls are only for debugging the extension bridge. Normal browser work should start from the bak CLI.</div>
211
+ <div class="row">
212
+ <button id="reconnect">Reconnect bridge</button>
213
+ <button id="disconnect">Disconnect bridge</button>
214
+ </div>
215
+ </details>
174
216
  <div class="hint">Extension only connects to ws://127.0.0.1</div>
175
217
  <script src="./popup.global.js"></script>
176
218
  </body>
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@flrande/bak-extension",
3
- "version": "0.6.6",
3
+ "version": "0.6.8",
4
4
  "type": "module",
5
5
  "dependencies": {
6
- "@flrande/bak-protocol": "0.6.6"
6
+ "@flrande/bak-protocol": "0.6.8"
7
7
  },
8
8
  "devDependencies": {
9
9
  "@types/chrome": "^0.1.14",