@flrande/bak-extension 0.6.5 → 0.6.7
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 +40 -9
- package/dist/manifest.json +1 -1
- package/dist/popup.global.js +141 -23
- package/dist/popup.html +49 -7
- package/package.json +2 -2
- package/public/popup.html +49 -7
- package/src/background.ts +64 -27
- package/src/popup.ts +160 -23
- package/src/session-binding.ts +42 -35
|
@@ -1 +1 @@
|
|
|
1
|
-
2026-03-
|
|
1
|
+
2026-03-13T07:58:24.806Z
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
// package.json
|
|
63
63
|
var package_default = {
|
|
64
64
|
name: "@flrande/bak-extension",
|
|
65
|
-
version: "0.6.
|
|
65
|
+
version: "0.6.7",
|
|
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",
|
|
@@ -573,13 +573,14 @@
|
|
|
573
573
|
state.activeTabId = null;
|
|
574
574
|
state.primaryTabId = null;
|
|
575
575
|
window2 = createdWindow;
|
|
576
|
-
|
|
576
|
+
const initialTab = typeof createdWindow.initialTabId === "number" ? await this.waitForTrackedTab(createdWindow.initialTabId, createdWindow.id) : null;
|
|
577
|
+
tabs = initialTab ? [initialTab] : await this.waitForWindowTabs(createdWindow.id);
|
|
577
578
|
state.tabIds = tabs.map((tab) => tab.id);
|
|
578
579
|
if (state.primaryTabId === null) {
|
|
579
|
-
state.primaryTabId = tabs[0]?.id ?? null;
|
|
580
|
+
state.primaryTabId = initialTab?.id ?? tabs[0]?.id ?? null;
|
|
580
581
|
}
|
|
581
582
|
if (state.activeTabId === null) {
|
|
582
|
-
state.activeTabId = tabs.find((tab) => tab.active)?.id ?? tabs[0]?.id ?? null;
|
|
583
|
+
state.activeTabId = tabs.find((tab) => tab.active)?.id ?? initialTab?.id ?? tabs[0]?.id ?? null;
|
|
583
584
|
}
|
|
584
585
|
repairActions.push(created ? "created-window" : "recreated-window");
|
|
585
586
|
}
|
|
@@ -1081,7 +1082,8 @@
|
|
|
1081
1082
|
url: seedUrl || DEFAULT_SESSION_BINDING_URL,
|
|
1082
1083
|
focused: false
|
|
1083
1084
|
});
|
|
1084
|
-
const
|
|
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);
|
|
1085
1087
|
const firstTab = recreatedTabs[0] ?? null;
|
|
1086
1088
|
const tabIdMap = /* @__PURE__ */ new Map();
|
|
1087
1089
|
if (sourceTabs[0] && firstTab) {
|
|
@@ -1608,14 +1610,42 @@
|
|
|
1608
1610
|
},
|
|
1609
1611
|
async createWindow(options) {
|
|
1610
1612
|
const previouslyFocusedWindow = options.focused === true ? null : (await chrome.windows.getAll()).find((window2) => window2.focused === true && typeof window2.id === "number") ?? null;
|
|
1611
|
-
const
|
|
1612
|
-
const
|
|
1613
|
-
|
|
1613
|
+
const previouslyFocusedTabs = previouslyFocusedWindow?.id !== void 0 ? await chrome.tabs.query({ windowId: previouslyFocusedWindow.id }) : [];
|
|
1614
|
+
const previouslyFocusedTabIds = new Set(
|
|
1615
|
+
previouslyFocusedTabs.flatMap((tab) => typeof tab.id === "number" ? [tab.id] : [])
|
|
1616
|
+
);
|
|
1617
|
+
const previouslyFocusedTab = previouslyFocusedTabs.find((tab) => tab.active === true && typeof tab.id === "number") ?? null;
|
|
1618
|
+
const desiredUrl = options.url ?? "about:blank";
|
|
1619
|
+
let created = await chrome.windows.create({
|
|
1620
|
+
url: desiredUrl,
|
|
1614
1621
|
focused: true
|
|
1615
1622
|
});
|
|
1616
1623
|
if (!created || typeof created.id !== "number") {
|
|
1617
1624
|
throw new Error("Window missing id");
|
|
1618
1625
|
}
|
|
1626
|
+
const pickSeedTab = async (windowId) => {
|
|
1627
|
+
const tabs = await chrome.tabs.query({ windowId });
|
|
1628
|
+
const newlyCreatedTab = windowId === previouslyFocusedWindow?.id ? tabs.find((tab) => typeof tab.id === "number" && !previouslyFocusedTabIds.has(tab.id)) : null;
|
|
1629
|
+
const normalizedDesiredUrl = normalizeComparableTabUrl(desiredUrl);
|
|
1630
|
+
return newlyCreatedTab ?? tabs.find((tab) => {
|
|
1631
|
+
const pendingUrl = "pendingUrl" in tab && typeof tab.pendingUrl === "string" ? tab.pendingUrl : "";
|
|
1632
|
+
return normalizeComparableTabUrl(tab.url ?? pendingUrl) === normalizedDesiredUrl;
|
|
1633
|
+
}) ?? tabs.find((tab) => tab.active === true && typeof tab.id === "number") ?? tabs.find((tab) => typeof tab.id === "number") ?? null;
|
|
1634
|
+
};
|
|
1635
|
+
let seedTab = await pickSeedTab(created.id);
|
|
1636
|
+
const createdWindowTabs = await chrome.tabs.query({ windowId: created.id });
|
|
1637
|
+
const createdWindowReusedFocusedWindow = previouslyFocusedWindow?.id === created.id;
|
|
1638
|
+
const createdWindowLooksDirty = createdWindowTabs.length > 1;
|
|
1639
|
+
if ((createdWindowReusedFocusedWindow || createdWindowLooksDirty) && typeof seedTab?.id === "number") {
|
|
1640
|
+
created = await chrome.windows.create({
|
|
1641
|
+
tabId: seedTab.id,
|
|
1642
|
+
focused: true
|
|
1643
|
+
});
|
|
1644
|
+
if (!created || typeof created.id !== "number") {
|
|
1645
|
+
throw new Error("Lifted window missing id");
|
|
1646
|
+
}
|
|
1647
|
+
seedTab = await pickSeedTab(created.id);
|
|
1648
|
+
}
|
|
1619
1649
|
if (options.focused !== true && previouslyFocusedWindow?.id && previouslyFocusedWindow.id !== created.id) {
|
|
1620
1650
|
await chrome.windows.update(previouslyFocusedWindow.id, { focused: true });
|
|
1621
1651
|
if (typeof previouslyFocusedTab?.id === "number") {
|
|
@@ -1625,7 +1655,8 @@
|
|
|
1625
1655
|
const finalWindow = await chrome.windows.get(created.id);
|
|
1626
1656
|
return {
|
|
1627
1657
|
id: finalWindow.id,
|
|
1628
|
-
focused: Boolean(finalWindow.focused)
|
|
1658
|
+
focused: Boolean(finalWindow.focused),
|
|
1659
|
+
initialTabId: seedTab?.id ?? null
|
|
1629
1660
|
};
|
|
1630
1661
|
},
|
|
1631
1662
|
async updateWindow(windowId, options) {
|
package/dist/manifest.json
CHANGED
package/dist/popup.global.js
CHANGED
|
@@ -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,
|
|
23
|
+
function setStatus(text, tone = "neutral") {
|
|
22
24
|
statusEl.textContent = text;
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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 =
|
|
219
|
+
const port = parsePortValue();
|
|
110
220
|
if (!token && latestState?.hasToken !== true) {
|
|
111
|
-
setStatus("Pair token is required",
|
|
221
|
+
setStatus("Pair token is required", "error");
|
|
112
222
|
return;
|
|
113
223
|
}
|
|
114
|
-
if (
|
|
115
|
-
setStatus("Port is invalid",
|
|
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
package/public/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/src/background.ts
CHANGED
|
@@ -411,7 +411,7 @@ function emitSessionBindingUpdated(
|
|
|
411
411
|
});
|
|
412
412
|
}
|
|
413
413
|
|
|
414
|
-
const sessionBindingBrowser: SessionBindingBrowser = {
|
|
414
|
+
const sessionBindingBrowser: SessionBindingBrowser = {
|
|
415
415
|
async getTab(tabId) {
|
|
416
416
|
try {
|
|
417
417
|
return toTabInfo(await chrome.tabs.get(tabId));
|
|
@@ -468,34 +468,71 @@ const sessionBindingBrowser: SessionBindingBrowser = {
|
|
|
468
468
|
return null;
|
|
469
469
|
}
|
|
470
470
|
},
|
|
471
|
-
async createWindow(options) {
|
|
472
|
-
const previouslyFocusedWindow =
|
|
473
|
-
options.focused === true
|
|
474
|
-
? null
|
|
475
|
-
: (await chrome.windows.getAll()).find((window) => window.focused === true && typeof window.id === 'number') ?? null;
|
|
476
|
-
const
|
|
477
|
-
previouslyFocusedWindow?.id !== undefined
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
471
|
+
async createWindow(options) {
|
|
472
|
+
const previouslyFocusedWindow =
|
|
473
|
+
options.focused === true
|
|
474
|
+
? null
|
|
475
|
+
: (await chrome.windows.getAll()).find((window) => window.focused === true && typeof window.id === 'number') ?? null;
|
|
476
|
+
const previouslyFocusedTabs =
|
|
477
|
+
previouslyFocusedWindow?.id !== undefined ? await chrome.tabs.query({ windowId: previouslyFocusedWindow.id }) : [];
|
|
478
|
+
const previouslyFocusedTabIds = new Set(
|
|
479
|
+
previouslyFocusedTabs.flatMap((tab) => (typeof tab.id === 'number' ? [tab.id] : []))
|
|
480
|
+
);
|
|
481
|
+
const previouslyFocusedTab =
|
|
482
|
+
previouslyFocusedTabs.find((tab) => tab.active === true && typeof tab.id === 'number') ?? null;
|
|
483
|
+
const desiredUrl = options.url ?? 'about:blank';
|
|
484
|
+
let created = await chrome.windows.create({
|
|
485
|
+
url: desiredUrl,
|
|
486
|
+
focused: true
|
|
487
|
+
});
|
|
488
|
+
if (!created || typeof created.id !== 'number') {
|
|
489
|
+
throw new Error('Window missing id');
|
|
490
|
+
}
|
|
491
|
+
const pickSeedTab = async (windowId: number): Promise<chrome.tabs.Tab | null> => {
|
|
492
|
+
const tabs = await chrome.tabs.query({ windowId });
|
|
493
|
+
const newlyCreatedTab =
|
|
494
|
+
windowId === previouslyFocusedWindow?.id
|
|
495
|
+
? tabs.find((tab) => typeof tab.id === 'number' && !previouslyFocusedTabIds.has(tab.id))
|
|
496
|
+
: null;
|
|
497
|
+
const normalizedDesiredUrl = normalizeComparableTabUrl(desiredUrl);
|
|
498
|
+
return (
|
|
499
|
+
newlyCreatedTab ??
|
|
500
|
+
tabs.find((tab) => {
|
|
501
|
+
const pendingUrl = 'pendingUrl' in tab && typeof tab.pendingUrl === 'string' ? tab.pendingUrl : '';
|
|
502
|
+
return normalizeComparableTabUrl(tab.url ?? pendingUrl) === normalizedDesiredUrl;
|
|
503
|
+
}) ??
|
|
504
|
+
tabs.find((tab) => tab.active === true && typeof tab.id === 'number') ??
|
|
505
|
+
tabs.find((tab) => typeof tab.id === 'number') ??
|
|
506
|
+
null
|
|
507
|
+
);
|
|
508
|
+
};
|
|
509
|
+
let seedTab = await pickSeedTab(created.id);
|
|
510
|
+
const createdWindowTabs = await chrome.tabs.query({ windowId: created.id });
|
|
511
|
+
const createdWindowReusedFocusedWindow = previouslyFocusedWindow?.id === created.id;
|
|
512
|
+
const createdWindowLooksDirty = createdWindowTabs.length > 1;
|
|
513
|
+
if ((createdWindowReusedFocusedWindow || createdWindowLooksDirty) && typeof seedTab?.id === 'number') {
|
|
514
|
+
created = await chrome.windows.create({
|
|
515
|
+
tabId: seedTab.id,
|
|
516
|
+
focused: true
|
|
517
|
+
});
|
|
518
|
+
if (!created || typeof created.id !== 'number') {
|
|
519
|
+
throw new Error('Lifted window missing id');
|
|
520
|
+
}
|
|
521
|
+
seedTab = await pickSeedTab(created.id);
|
|
522
|
+
}
|
|
523
|
+
if (options.focused !== true && previouslyFocusedWindow?.id && previouslyFocusedWindow.id !== created.id) {
|
|
524
|
+
await chrome.windows.update(previouslyFocusedWindow.id, { focused: true });
|
|
525
|
+
if (typeof previouslyFocusedTab?.id === 'number') {
|
|
526
|
+
await chrome.tabs.update(previouslyFocusedTab.id, { active: true });
|
|
527
|
+
}
|
|
492
528
|
}
|
|
493
529
|
const finalWindow = await chrome.windows.get(created.id);
|
|
494
|
-
return {
|
|
495
|
-
id: finalWindow.id!,
|
|
496
|
-
focused: Boolean(finalWindow.focused)
|
|
497
|
-
|
|
498
|
-
|
|
530
|
+
return {
|
|
531
|
+
id: finalWindow.id!,
|
|
532
|
+
focused: Boolean(finalWindow.focused),
|
|
533
|
+
initialTabId: seedTab?.id ?? null
|
|
534
|
+
};
|
|
535
|
+
},
|
|
499
536
|
async updateWindow(windowId, options) {
|
|
500
537
|
const updated = await chrome.windows.update(windowId, {
|
|
501
538
|
focused: options.focused
|
package/src/popup.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
const statusEl = document.getElementById('status') as HTMLDivElement;
|
|
2
|
+
const statusNoteEl = document.getElementById('statusNote') as HTMLDivElement;
|
|
2
3
|
const tokenInput = document.getElementById('token') as HTMLInputElement;
|
|
3
4
|
const portInput = document.getElementById('port') as HTMLInputElement;
|
|
4
5
|
const debugRichTextInput = document.getElementById('debugRichText') as HTMLInputElement;
|
|
5
6
|
const saveBtn = document.getElementById('save') as HTMLButtonElement;
|
|
7
|
+
const saveRowEl = document.getElementById('saveRow') as HTMLDivElement;
|
|
6
8
|
const reconnectBtn = document.getElementById('reconnect') as HTMLButtonElement;
|
|
7
9
|
const disconnectBtn = document.getElementById('disconnect') as HTMLButtonElement;
|
|
8
10
|
const connectionStateEl = document.getElementById('connectionState') as HTMLDivElement;
|
|
@@ -50,9 +52,25 @@ interface PopupState {
|
|
|
50
52
|
}
|
|
51
53
|
let latestState: PopupState | null = null;
|
|
52
54
|
|
|
53
|
-
function setStatus(text: string,
|
|
55
|
+
function setStatus(text: string, tone: 'neutral' | 'success' | 'warning' | 'error' = 'neutral'): void {
|
|
54
56
|
statusEl.textContent = text;
|
|
55
|
-
|
|
57
|
+
if (tone === 'success') {
|
|
58
|
+
statusEl.style.color = '#166534';
|
|
59
|
+
return;
|
|
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';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function pluralize(count: number, singular: string, plural = `${singular}s`): string {
|
|
73
|
+
return `${count} ${count === 1 ? singular : plural}`;
|
|
56
74
|
}
|
|
57
75
|
|
|
58
76
|
function formatTimeAgo(at: number | null): string {
|
|
@@ -75,7 +93,11 @@ function formatTimeAgo(at: number | null): string {
|
|
|
75
93
|
}
|
|
76
94
|
|
|
77
95
|
function renderSessionBindings(state: PopupState['sessionBindings']): void {
|
|
78
|
-
|
|
96
|
+
if (state.count === 0) {
|
|
97
|
+
sessionSummaryEl.textContent = 'No remembered sessions';
|
|
98
|
+
} else {
|
|
99
|
+
sessionSummaryEl.textContent = `${pluralize(state.count, 'session')}, ${pluralize(state.attachedCount, 'attached binding')}, ${pluralize(state.tabCount, 'tab')}, ${pluralize(state.detachedCount, 'detached binding')}`;
|
|
100
|
+
}
|
|
79
101
|
sessionListEl.replaceChildren();
|
|
80
102
|
for (const item of state.items) {
|
|
81
103
|
const li = document.createElement('li');
|
|
@@ -89,8 +111,26 @@ function renderSessionBindings(state: PopupState['sessionBindings']): void {
|
|
|
89
111
|
}
|
|
90
112
|
}
|
|
91
113
|
|
|
114
|
+
function describeConnectionState(connectionState: PopupState['connectionState']): string {
|
|
115
|
+
switch (connectionState) {
|
|
116
|
+
case 'connected':
|
|
117
|
+
return 'connected';
|
|
118
|
+
case 'connecting':
|
|
119
|
+
return 'waiting for runtime';
|
|
120
|
+
case 'reconnecting':
|
|
121
|
+
return 'retrying connection';
|
|
122
|
+
case 'manual':
|
|
123
|
+
return 'manually disconnected';
|
|
124
|
+
case 'missing-token':
|
|
125
|
+
return 'token required';
|
|
126
|
+
case 'disconnected':
|
|
127
|
+
default:
|
|
128
|
+
return 'disconnected';
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
92
132
|
function renderConnectionDetails(state: PopupState): void {
|
|
93
|
-
connectionStateEl.textContent = state.connectionState;
|
|
133
|
+
connectionStateEl.textContent = describeConnectionState(state.connectionState);
|
|
94
134
|
tokenStateEl.textContent = state.hasToken ? 'configured' : 'missing';
|
|
95
135
|
connectionUrlEl.textContent = state.wsUrl;
|
|
96
136
|
extensionVersionEl.textContent = state.extensionVersion;
|
|
@@ -120,42 +160,130 @@ function renderConnectionDetails(state: PopupState): void {
|
|
|
120
160
|
}
|
|
121
161
|
}
|
|
122
162
|
|
|
163
|
+
function parsePortValue(): number | null {
|
|
164
|
+
const port = Number.parseInt(portInput.value.trim(), 10);
|
|
165
|
+
return Number.isInteger(port) && port > 0 ? port : null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function isFormDirty(state: PopupState | null): boolean {
|
|
169
|
+
if (!state) {
|
|
170
|
+
return tokenInput.value.trim().length > 0;
|
|
171
|
+
}
|
|
172
|
+
return (
|
|
173
|
+
tokenInput.value.trim().length > 0 ||
|
|
174
|
+
portInput.value.trim() !== String(state.port) ||
|
|
175
|
+
debugRichTextInput.checked !== Boolean(state.debugRichText)
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function getConfigValidationMessage(state: PopupState | null): string | null {
|
|
180
|
+
if (!tokenInput.value.trim() && state?.hasToken !== true) {
|
|
181
|
+
return 'Pair token is required';
|
|
182
|
+
}
|
|
183
|
+
if (parsePortValue() === null) {
|
|
184
|
+
return 'Port is invalid';
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function updateSaveState(state: PopupState | null): void {
|
|
190
|
+
const dirty = isFormDirty(state);
|
|
191
|
+
const validationError = getConfigValidationMessage(state);
|
|
192
|
+
saveRowEl.hidden = !dirty;
|
|
193
|
+
saveBtn.disabled = !dirty || validationError !== null;
|
|
194
|
+
saveBtn.textContent = state?.hasToken ? 'Save settings' : 'Save token';
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function describeStatus(state: PopupState): { text: string; note: string; tone: 'neutral' | 'success' | 'warning' | 'error' } {
|
|
198
|
+
const combinedError = `${state.lastErrorContext ?? ''} ${state.lastError ?? ''}`.toLowerCase();
|
|
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
|
+
}
|
|
232
|
+
|
|
233
|
+
if (state.connectionState === 'reconnecting') {
|
|
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
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (state.lastError) {
|
|
242
|
+
return {
|
|
243
|
+
text: 'Connection problem',
|
|
244
|
+
note: 'Check the last error below. The extension keeps retrying automatically unless you disconnect it manually.',
|
|
245
|
+
tone: 'error'
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
text: 'Not connected yet',
|
|
251
|
+
note: 'Once the local bak runtime is available, the extension reconnects automatically.',
|
|
252
|
+
tone: 'neutral'
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
123
256
|
async function refreshState(): Promise<void> {
|
|
124
257
|
const state = (await chrome.runtime.sendMessage({ type: 'bak.getState' })) as PopupState;
|
|
125
258
|
|
|
126
259
|
if (state.ok) {
|
|
260
|
+
const shouldSyncForm = !isFormDirty(latestState);
|
|
127
261
|
latestState = state;
|
|
128
|
-
|
|
129
|
-
|
|
262
|
+
if (shouldSyncForm) {
|
|
263
|
+
portInput.value = String(state.port);
|
|
264
|
+
debugRichTextInput.checked = Boolean(state.debugRichText);
|
|
265
|
+
tokenInput.value = '';
|
|
266
|
+
}
|
|
130
267
|
renderConnectionDetails(state);
|
|
131
268
|
renderSessionBindings(state.sessionBindings);
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
} else if (state.connectionState === 'manual') {
|
|
137
|
-
setStatus('Disconnected manually');
|
|
138
|
-
} else if (state.connectionState === 'reconnecting') {
|
|
139
|
-
setStatus('Reconnecting to bak CLI', true);
|
|
140
|
-
} else if (state.lastError) {
|
|
141
|
-
setStatus(`Disconnected: ${state.lastError}`, true);
|
|
142
|
-
} else {
|
|
143
|
-
setStatus('Disconnected');
|
|
144
|
-
}
|
|
269
|
+
updateSaveState(state);
|
|
270
|
+
const status = describeStatus(state);
|
|
271
|
+
setStatus(status.text, status.tone);
|
|
272
|
+
statusNoteEl.textContent = status.note;
|
|
145
273
|
}
|
|
146
274
|
}
|
|
147
275
|
|
|
148
276
|
saveBtn.addEventListener('click', async () => {
|
|
149
277
|
const token = tokenInput.value.trim();
|
|
150
|
-
const port =
|
|
278
|
+
const port = parsePortValue();
|
|
151
279
|
|
|
152
280
|
if (!token && latestState?.hasToken !== true) {
|
|
153
|
-
setStatus('Pair token is required',
|
|
281
|
+
setStatus('Pair token is required', 'error');
|
|
154
282
|
return;
|
|
155
283
|
}
|
|
156
284
|
|
|
157
|
-
if (
|
|
158
|
-
setStatus('Port is invalid',
|
|
285
|
+
if (port === null) {
|
|
286
|
+
setStatus('Port is invalid', 'error');
|
|
159
287
|
return;
|
|
160
288
|
}
|
|
161
289
|
|
|
@@ -180,6 +308,15 @@ disconnectBtn.addEventListener('click', async () => {
|
|
|
180
308
|
await refreshState();
|
|
181
309
|
});
|
|
182
310
|
|
|
311
|
+
for (const element of [tokenInput, portInput, debugRichTextInput]) {
|
|
312
|
+
element.addEventListener('input', () => {
|
|
313
|
+
updateSaveState(latestState);
|
|
314
|
+
});
|
|
315
|
+
element.addEventListener('change', () => {
|
|
316
|
+
updateSaveState(latestState);
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
183
320
|
void refreshState();
|
|
184
321
|
const refreshInterval = window.setInterval(() => {
|
|
185
322
|
void refreshState();
|
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);
|
|
@@ -725,19 +730,21 @@ class SessionBindingManager {
|
|
|
725
730
|
};
|
|
726
731
|
}
|
|
727
732
|
|
|
728
|
-
private async moveBindingIntoDedicatedWindow(
|
|
729
|
-
state: SessionBindingRecord,
|
|
730
|
-
ownership: SessionBindingWindowOwnership,
|
|
731
|
-
initialUrl: string
|
|
732
|
-
): Promise<{ window: SessionBindingWindow; tabs: SessionBindingTab[] }> {
|
|
733
|
+
private async moveBindingIntoDedicatedWindow(
|
|
734
|
+
state: SessionBindingRecord,
|
|
735
|
+
ownership: SessionBindingWindowOwnership,
|
|
736
|
+
initialUrl: string
|
|
737
|
+
): Promise<{ window: SessionBindingWindow; tabs: SessionBindingTab[] }> {
|
|
733
738
|
const sourceTabs = this.orderSessionBindingTabsForMigration(state, ownership.bindingTabs);
|
|
734
739
|
const seedUrl = sourceTabs[0]?.url ?? initialUrl;
|
|
735
|
-
const window = await this.browser.createWindow({
|
|
736
|
-
url: seedUrl || DEFAULT_SESSION_BINDING_URL,
|
|
737
|
-
focused: false
|
|
738
|
-
});
|
|
739
|
-
|
|
740
|
-
|
|
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;
|
|
741
748
|
const tabIdMap = new Map<number, number>();
|
|
742
749
|
if (sourceTabs[0] && firstTab) {
|
|
743
750
|
tabIdMap.set(sourceTabs[0].id, firstTab.id);
|