@flrande/bak-extension 0.6.6 → 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.
@@ -1 +1 @@
1
- 2026-03-13T07:15:48.660Z
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.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",
@@ -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.7",
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.7",
4
4
  "type": "module",
5
5
  "dependencies": {
6
- "@flrande/bak-protocol": "0.6.6"
6
+ "@flrande/bak-protocol": "0.6.7"
7
7
  },
8
8
  "devDependencies": {
9
9
  "@types/chrome": "^0.1.14",
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/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, bad = false): void {
55
+ function setStatus(text: string, tone: 'neutral' | 'success' | 'warning' | 'error' = 'neutral'): void {
54
56
  statusEl.textContent = text;
55
- statusEl.style.color = bad ? '#dc2626' : '#0f172a';
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
- sessionSummaryEl.textContent = `${state.count} sessions, ${state.attachedCount} attached, ${state.tabCount} tabs, ${state.detachedCount} detached`;
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
- portInput.value = String(state.port);
129
- debugRichTextInput.checked = Boolean(state.debugRichText);
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
- if (state.connected) {
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);
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 = Number.parseInt(portInput.value.trim(), 10);
278
+ const port = parsePortValue();
151
279
 
152
280
  if (!token && latestState?.hasToken !== true) {
153
- setStatus('Pair token is required', true);
281
+ setStatus('Pair token is required', 'error');
154
282
  return;
155
283
  }
156
284
 
157
- if (!Number.isInteger(port) || port <= 0) {
158
- setStatus('Port is invalid', true);
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();