clankerbend 0.1.0 → 0.1.2

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.
Files changed (38) hide show
  1. package/DEVELOPERS.md +6 -6
  2. package/README.md +31 -14
  3. package/apps/README.md +3 -3
  4. package/apps/sticky-notes/README.md +2 -2
  5. package/apps/sticky-notes/{onewhack.manifest.json → clankerbend.manifest.json} +1 -1
  6. package/apps/sticky-notes/package.json +3 -3
  7. package/apps/sticky-notes/public/app.js +42 -9
  8. package/apps/sticky-notes/src/sticky-notes-app.js +17 -21
  9. package/apps/vim-nav/README.md +22 -22
  10. package/apps/vim-nav/{onewhack.manifest.json → clankerbend.manifest.json} +4 -4
  11. package/apps/vim-nav/package.json +3 -3
  12. package/apps/vim-nav/public/app.js +44 -11
  13. package/apps/vim-nav/public/index.html +2 -2
  14. package/apps/vim-nav/server.mjs +2 -2
  15. package/apps/vim-nav/src/vim-nav-app.js +5 -5
  16. package/assets/clankerbend-banner.webp +0 -0
  17. package/assets/clankerbend-demo-thumbnail.webp +0 -0
  18. package/assets/clankerbend.svg +26 -0
  19. package/cli.mjs +22 -11
  20. package/docs/app-lifecycle.md +9 -9
  21. package/docs/app-manifest.md +4 -4
  22. package/docs/author-guide.md +5 -5
  23. package/docs/launcher-profiles.md +45 -10
  24. package/docs/protocol.md +248 -241
  25. package/host/README.md +4 -4
  26. package/host/src/app-registry.js +8 -6
  27. package/host/src/codex-desktop-cdp-adapter.js +85 -65
  28. package/host/src/codex-desktop-renderer-bridge.js +839 -270
  29. package/host/src/index.js +186 -34
  30. package/launch/accounts.mjs +360 -0
  31. package/launch/codex-mux-adapter.mjs +175 -0
  32. package/launch/profiles.mjs +40 -16
  33. package/launch/runtime-paths.mjs +9 -6
  34. package/launch/test-accounts.mjs +71 -0
  35. package/package.json +12 -10
  36. package/scripts/release-npm.mjs +1 -2
  37. package/server.mjs +21 -14
  38. package/assets/onewhack.jpg +0 -0
@@ -1,7 +1,7 @@
1
1
  (function installCodexVimNavigator() {
2
2
  const VIM_NAV_APP_ID = "onewill.vim-nav";
3
- const BRIDGE_VERSION = 112;
4
- const RUNTIME_KEY = "__oneWhackRuntime";
3
+ const BRIDGE_VERSION = 115;
4
+ const RUNTIME_KEY = "__clankerbendRuntime";
5
5
  const STYLE_ID = "codex-vim-nav-style";
6
6
  const ANNOTATION_CLASS = "codex-vim-nav-annotation";
7
7
  const ANCHOR_CLASS = "codex-vim-nav-anchor";
@@ -9,10 +9,11 @@
9
9
  const HIGHLIGHT_CLASS = "codex-vim-nav-highlight";
10
10
  const STATUS_CHROME_CLASS = "codex-vim-nav-status-chrome";
11
11
  const MODE_BADGE_ID = "codex-vim-nav-mode-badge";
12
- const HOST_UI_CLASS = "onewhack-host-ui";
13
- const SELECTION_MENU_ID = "onewhack-selection-menu";
14
- const OVERLAY_ID = "onewhack-anchored-overlay";
15
- const COMPOSER_CHIPS_ID = "onewhack-composer-chips";
12
+ const HOST_UI_CLASS = "clankerbend-host-ui";
13
+ const SELECTION_MENU_ID = "clankerbend-selection-menu";
14
+ const ACCOUNT_SWITCHER_ID = "clankerbend-account-switcher";
15
+ const OVERLAY_ID = "clankerbend-anchored-overlay";
16
+ const COMPOSER_CHIPS_ID = "clankerbend-composer-chips";
16
17
  const SELECTOR = [
17
18
  "[data-content-search-unit-key]",
18
19
  "[data-turn-key]",
@@ -20,7 +21,7 @@
20
21
  "[data-thread-user-message-navigation-item-id]"
21
22
  ].join(",");
22
23
 
23
- const existingBridge = oneWhackRuntime().getBridge(VIM_NAV_APP_ID);
24
+ const existingBridge = clankerbendRuntime().getBridge(VIM_NAV_APP_ID);
24
25
  if (existingBridge?.name === "vim-nav" && existingBridge?.version === BRIDGE_VERSION) {
25
26
  return existingBridge.snapshot();
26
27
  }
@@ -36,9 +37,9 @@
36
37
  vimMode: false,
37
38
  helpOpen: false,
38
39
  badgeHidden: readBadgeHidden(),
39
- ctrlDown: false,
40
+ metaDown: false,
40
41
  altDown: false,
41
- ctrlAltToggleDown: false,
42
+ metaAltToggleDown: false,
42
43
  pendingKeys: "",
43
44
  countPrefix: "",
44
45
  anchorOrder: [],
@@ -60,11 +61,19 @@
60
61
  lastBottomJump: null,
61
62
  lastOrderFallback: null,
62
63
  commandChain: Promise.resolve(),
64
+ relativeMoveInFlight: false,
65
+ pendingRelativeDelta: 0,
63
66
  hostState: null,
64
67
  hostEvents: [],
65
68
  activeTextSelection: null,
66
69
  lastSelectionMenuAt: 0,
67
70
  renderedOverlaySignature: null,
71
+ accountUiStatus: "",
72
+ accountAddOpen: false,
73
+ accountAddLabel: "",
74
+ accountManageOpen: false,
75
+ accountPendingAdoptId: null,
76
+ accountPendingDeleteId: null,
68
77
  pendingComposerSubmission: null,
69
78
  submittedComposerSubmissions: []
70
79
  };
@@ -119,6 +128,7 @@
119
128
  color: #241f12 !important;
120
129
  }
121
130
  #${OVERLAY_ID} button,
131
+ #${ACCOUNT_SWITCHER_ID} button,
122
132
  #${COMPOSER_CHIPS_ID} button {
123
133
  appearance: none !important;
124
134
  border: 0 !important;
@@ -129,6 +139,202 @@
129
139
  font: 650 12px/1 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important;
130
140
  padding: 7px 8px !important;
131
141
  }
142
+ #${ACCOUNT_SWITCHER_ID} {
143
+ display: grid !important;
144
+ gap: 7px !important;
145
+ min-width: 220px !important;
146
+ max-width: 320px !important;
147
+ border-top: 1px solid rgba(148, 163, 184, .24) !important;
148
+ border-bottom: 1px solid rgba(148, 163, 184, .18) !important;
149
+ margin: 5px 0 !important;
150
+ padding: 8px 4px !important;
151
+ color: inherit !important;
152
+ }
153
+ #${ACCOUNT_SWITCHER_ID} .clankerbend-account-head {
154
+ display: flex !important;
155
+ align-items: center !important;
156
+ justify-content: space-between !important;
157
+ gap: 8px !important;
158
+ padding: 0 6px !important;
159
+ }
160
+ #${ACCOUNT_SWITCHER_ID} .clankerbend-account-title {
161
+ color: currentColor !important;
162
+ font: 700 11px/1.2 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important;
163
+ letter-spacing: 0 !important;
164
+ }
165
+ #${ACCOUNT_SWITCHER_ID} .clankerbend-account-status {
166
+ color: color-mix(in srgb, currentColor 62%, transparent) !important;
167
+ font: 500 11px/1.2 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important;
168
+ overflow: hidden !important;
169
+ text-overflow: ellipsis !important;
170
+ white-space: nowrap !important;
171
+ }
172
+ #${ACCOUNT_SWITCHER_ID} .clankerbend-account-list {
173
+ display: grid !important;
174
+ gap: 2px !important;
175
+ }
176
+ #${ACCOUNT_SWITCHER_ID} .clankerbend-account-row {
177
+ display: grid !important;
178
+ grid-template-columns: minmax(0, 1fr) auto !important;
179
+ align-items: center !important;
180
+ gap: 5px !important;
181
+ border-radius: 6px !important;
182
+ padding: 2px !important;
183
+ }
184
+ #${ACCOUNT_SWITCHER_ID} .clankerbend-account-row.is-active {
185
+ background: rgba(143, 199, 212, .14) !important;
186
+ }
187
+ #${ACCOUNT_SWITCHER_ID} .clankerbend-account-main {
188
+ display: grid !important;
189
+ min-width: 0 !important;
190
+ border-radius: 5px !important;
191
+ padding: 6px !important;
192
+ text-align: left !important;
193
+ color: inherit !important;
194
+ }
195
+ #${ACCOUNT_SWITCHER_ID} .clankerbend-account-main:hover,
196
+ #${ACCOUNT_SWITCHER_ID} .clankerbend-account-icon:hover {
197
+ background: rgba(148, 163, 184, .16) !important;
198
+ }
199
+ #${ACCOUNT_SWITCHER_ID} .clankerbend-account-label,
200
+ #${ACCOUNT_SWITCHER_ID} .clankerbend-account-meta {
201
+ overflow: hidden !important;
202
+ text-overflow: ellipsis !important;
203
+ white-space: nowrap !important;
204
+ }
205
+ #${ACCOUNT_SWITCHER_ID} .clankerbend-account-label {
206
+ font: 650 12px/1.2 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important;
207
+ }
208
+ #${ACCOUNT_SWITCHER_ID} .clankerbend-account-meta {
209
+ color: color-mix(in srgb, currentColor 58%, transparent) !important;
210
+ font: 500 11px/1.2 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important;
211
+ }
212
+ #${ACCOUNT_SWITCHER_ID} .clankerbend-account-icon {
213
+ width: 28px !important;
214
+ height: 28px !important;
215
+ display: inline-grid !important;
216
+ place-items: center !important;
217
+ border-radius: 5px !important;
218
+ color: color-mix(in srgb, currentColor 76%, transparent) !important;
219
+ padding: 0 !important;
220
+ }
221
+ #${ACCOUNT_SWITCHER_ID} .clankerbend-account-actions {
222
+ display: grid !important;
223
+ grid-template-columns: 1fr 1fr !important;
224
+ gap: 4px !important;
225
+ }
226
+ #${ACCOUNT_SWITCHER_ID} .clankerbend-account-actions button {
227
+ border-radius: 5px !important;
228
+ background: rgba(148, 163, 184, .12) !important;
229
+ color: inherit !important;
230
+ min-width: 0 !important;
231
+ }
232
+ #${ACCOUNT_SWITCHER_ID} .clankerbend-account-add-form {
233
+ display: grid !important;
234
+ gap: 6px !important;
235
+ border-radius: 6px !important;
236
+ background: rgba(148, 163, 184, .10) !important;
237
+ padding: 6px !important;
238
+ }
239
+ #${ACCOUNT_SWITCHER_ID} .clankerbend-account-add-form input {
240
+ width: 100% !important;
241
+ min-width: 0 !important;
242
+ border: 1px solid rgba(148, 163, 184, .32) !important;
243
+ border-radius: 5px !important;
244
+ background: rgba(0, 0, 0, .18) !important;
245
+ color: inherit !important;
246
+ font: 500 12px/1.2 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important;
247
+ outline: none !important;
248
+ padding: 7px 8px !important;
249
+ }
250
+ #${ACCOUNT_SWITCHER_ID} .clankerbend-account-add-form input:focus {
251
+ border-color: rgba(143, 199, 212, .76) !important;
252
+ box-shadow: 0 0 0 2px rgba(143, 199, 212, .18) !important;
253
+ }
254
+ #${ACCOUNT_SWITCHER_ID} .clankerbend-account-add-actions {
255
+ display: grid !important;
256
+ grid-template-columns: 1fr 1fr !important;
257
+ gap: 4px !important;
258
+ }
259
+ #${ACCOUNT_SWITCHER_ID} .clankerbend-account-add-actions button {
260
+ background: rgba(148, 163, 184, .14) !important;
261
+ }
262
+ #${ACCOUNT_SWITCHER_ID} .clankerbend-account-manage {
263
+ display: grid !important;
264
+ gap: 7px !important;
265
+ border-top: 1px solid rgba(148, 163, 184, .18) !important;
266
+ padding-top: 7px !important;
267
+ }
268
+ #${ACCOUNT_SWITCHER_ID} .clankerbend-account-manage-head {
269
+ display: flex !important;
270
+ align-items: baseline !important;
271
+ justify-content: space-between !important;
272
+ gap: 8px !important;
273
+ padding: 0 6px !important;
274
+ }
275
+ #${ACCOUNT_SWITCHER_ID} .clankerbend-account-manage-head strong {
276
+ font: 700 11px/1.2 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important;
277
+ }
278
+ #${ACCOUNT_SWITCHER_ID} .clankerbend-account-manage-head span {
279
+ color: color-mix(in srgb, currentColor 58%, transparent) !important;
280
+ font: 500 11px/1.2 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important;
281
+ }
282
+ #${ACCOUNT_SWITCHER_ID} .clankerbend-account-manage-list {
283
+ display: grid !important;
284
+ gap: 6px !important;
285
+ max-height: 204px !important;
286
+ overflow-y: auto !important;
287
+ overscroll-behavior: contain !important;
288
+ padding-right: 2px !important;
289
+ }
290
+ #${ACCOUNT_SWITCHER_ID} .clankerbend-account-manage-list::-webkit-scrollbar {
291
+ width: 8px !important;
292
+ }
293
+ #${ACCOUNT_SWITCHER_ID} .clankerbend-account-manage-list::-webkit-scrollbar-thumb {
294
+ border-radius: 999px !important;
295
+ background: rgba(148, 163, 184, .34) !important;
296
+ }
297
+ #${ACCOUNT_SWITCHER_ID} .clankerbend-account-manage-row {
298
+ display: grid !important;
299
+ grid-template-columns: minmax(0, 1fr) auto !important;
300
+ align-items: center !important;
301
+ gap: 7px !important;
302
+ border-radius: 6px !important;
303
+ background: rgba(148, 163, 184, .10) !important;
304
+ padding: 6px !important;
305
+ }
306
+ #${ACCOUNT_SWITCHER_ID} .clankerbend-account-manage-title {
307
+ display: grid !important;
308
+ min-width: 0 !important;
309
+ }
310
+ #${ACCOUNT_SWITCHER_ID} .clankerbend-account-manage-title strong,
311
+ #${ACCOUNT_SWITCHER_ID} .clankerbend-account-manage-title span {
312
+ overflow: hidden !important;
313
+ text-overflow: ellipsis !important;
314
+ white-space: nowrap !important;
315
+ }
316
+ #${ACCOUNT_SWITCHER_ID} .clankerbend-account-manage-title strong {
317
+ font: 650 12px/1.2 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important;
318
+ }
319
+ #${ACCOUNT_SWITCHER_ID} .clankerbend-account-manage-title span {
320
+ color: color-mix(in srgb, currentColor 58%, transparent) !important;
321
+ font: 500 11px/1.2 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important;
322
+ }
323
+ #${ACCOUNT_SWITCHER_ID} .clankerbend-account-manage-actions {
324
+ display: flex !important;
325
+ align-items: center !important;
326
+ justify-content: flex-end !important;
327
+ gap: 4px !important;
328
+ }
329
+ #${ACCOUNT_SWITCHER_ID} .clankerbend-account-manage-actions button {
330
+ background: rgba(148, 163, 184, .14) !important;
331
+ white-space: nowrap !important;
332
+ padding: 6px 7px !important;
333
+ }
334
+ #${ACCOUNT_SWITCHER_ID} button:disabled {
335
+ cursor: default !important;
336
+ opacity: .55 !important;
337
+ }
132
338
  #${OVERLAY_ID} {
133
339
  position: fixed !important;
134
340
  z-index: 2147483004 !important;
@@ -159,7 +365,7 @@
159
365
  font: 13px/1.35 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important;
160
366
  padding: 9px !important;
161
367
  }
162
- #${OVERLAY_ID} .onewhack-overlay-actions {
368
+ #${OVERLAY_ID} .clankerbend-overlay-actions {
163
369
  display: flex !important;
164
370
  justify-content: flex-end !important;
165
371
  gap: 6px !important;
@@ -168,6 +374,10 @@
168
374
  background: rgba(80, 65, 20, .78) !important;
169
375
  color: #fff7d7 !important;
170
376
  }
377
+ #${OVERLAY_ID} button.clankerbend-overlay-secondary {
378
+ background: transparent !important;
379
+ color: rgba(58, 45, 12, .82) !important;
380
+ }
171
381
  #${COMPOSER_CHIPS_ID} {
172
382
  position: absolute !important;
173
383
  z-index: 2147483002 !important;
@@ -180,7 +390,7 @@
180
390
  #${COMPOSER_CHIPS_ID}.is-visible {
181
391
  display: flex !important;
182
392
  }
183
- #${COMPOSER_CHIPS_ID} .onewhack-context-chip {
393
+ #${COMPOSER_CHIPS_ID} .clankerbend-context-chip {
184
394
  display: inline-flex !important;
185
395
  align-items: center !important;
186
396
  gap: 5px !important;
@@ -194,7 +404,7 @@
194
404
  padding: 5px 8px !important;
195
405
  pointer-events: auto !important;
196
406
  }
197
- .onewhack-submitted-context-row .onewhack-context-chip {
407
+ .clankerbend-submitted-context-row .clankerbend-context-chip {
198
408
  display: inline-flex !important;
199
409
  align-items: center !important;
200
410
  gap: 5px !important;
@@ -206,24 +416,24 @@
206
416
  font: 600 12px/1.2 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important;
207
417
  padding: 5px 8px !important;
208
418
  }
209
- .onewhack-submitted-context-row .onewhack-context-chip::before {
419
+ .clankerbend-submitted-context-row .clankerbend-context-chip::before {
210
420
  content: "□" !important;
211
421
  color: #8a7327 !important;
212
422
  font-size: 10px !important;
213
423
  line-height: 1 !important;
214
424
  }
215
- #${COMPOSER_CHIPS_ID} .onewhack-context-chip::before {
425
+ #${COMPOSER_CHIPS_ID} .clankerbend-context-chip::before {
216
426
  content: "□" !important;
217
427
  color: #8a7327 !important;
218
428
  font-size: 10px !important;
219
429
  line-height: 1 !important;
220
430
  }
221
- #${COMPOSER_CHIPS_ID} .onewhack-context-chip span {
431
+ #${COMPOSER_CHIPS_ID} .clankerbend-context-chip span {
222
432
  overflow: hidden !important;
223
433
  text-overflow: ellipsis !important;
224
434
  white-space: nowrap !important;
225
435
  }
226
- #${COMPOSER_CHIPS_ID} .onewhack-context-chip button {
436
+ #${COMPOSER_CHIPS_ID} .clankerbend-context-chip button {
227
437
  padding: 0 2px !important;
228
438
  color: #725f20 !important;
229
439
  }
@@ -380,7 +590,7 @@
380
590
  const titleRow = document.createElement("div");
381
591
  titleRow.className = "codex-vim-nav-title-row";
382
592
  const title = document.createElement("strong");
383
- title.textContent = "OneWill VimNav";
593
+ title.textContent = "VimNav";
384
594
  const mode = document.createElement("span");
385
595
  mode.className = "codex-vim-nav-state";
386
596
  mode.textContent = `[${state.vimMode ? "ON" : "OFF"}]`;
@@ -388,7 +598,7 @@
388
598
  dismiss.type = "button";
389
599
  dismiss.className = "codex-vim-nav-dismiss";
390
600
  dismiss.textContent = "x";
391
- dismiss.title = "Hide VimNav badge until Ctrl-Alt";
601
+ dismiss.title = "Hide VimNav badge until Cmd-Option";
392
602
  dismiss.addEventListener("click", (event) => {
393
603
  event.preventDefault();
394
604
  event.stopPropagation();
@@ -403,12 +613,23 @@
403
613
  helpButton.className = "codex-vim-nav-help-toggle";
404
614
  helpButton.textContent = "?";
405
615
  helpButton.title = "Toggle VimNav help";
406
- helpButton.addEventListener("click", (event) => {
616
+ const activateHelp = (event) => {
407
617
  event.preventDefault();
408
618
  event.stopPropagation();
619
+ event.stopImmediatePropagation?.();
620
+ if (event.type === "click" && helpButton.dataset.clankerbendPressed === "true") {
621
+ helpButton.dataset.clankerbendPressed = "";
622
+ return;
623
+ }
624
+ if (event.type !== "click" && helpButton.dataset.clankerbendPressed === "true") return;
625
+ if (event.type !== "click") helpButton.dataset.clankerbendPressed = "true";
409
626
  toggleHelp();
410
- });
411
- toggleRow.append("(Ctrl-Alt toggles, ", helpButton, " for help)");
627
+ };
628
+ helpButton.addEventListener("pointerdown", activateHelp);
629
+ helpButton.addEventListener("mousedown", activateHelp);
630
+ helpButton.addEventListener("touchstart", activateHelp, { passive: false });
631
+ helpButton.addEventListener("click", activateHelp);
632
+ toggleRow.append("(Cmd-Option toggles, ", helpButton, " for help)");
412
633
  badge.appendChild(toggleRow);
413
634
  if (buffer) {
414
635
  const row = document.createElement("div");
@@ -1297,13 +1518,13 @@
1297
1518
  if (!(el instanceof HTMLElement)) continue;
1298
1519
  el.classList.add(ANCHOR_CLASS);
1299
1520
  el.classList.toggle(CURRENT_CLASS, item.anchorId === state.currentAnchorId);
1300
- let label = el.querySelector(`.${ANNOTATION_CLASS}[data-anchor-id="${cssEscape(item.anchorId)}"][data-onewhack-app-id="${VIM_NAV_APP_ID}"]`);
1521
+ let label = el.querySelector(`.${ANNOTATION_CLASS}[data-anchor-id="${cssEscape(item.anchorId)}"][data-clankerbend-app-id="${VIM_NAV_APP_ID}"]`);
1301
1522
  if (!label) {
1302
1523
  label = document.createElement("button");
1303
1524
  label.type = "button";
1304
1525
  label.className = ANNOTATION_CLASS;
1305
1526
  label.dataset.anchorId = item.anchorId;
1306
- label.dataset.onewhackAppId = VIM_NAV_APP_ID;
1527
+ label.dataset.clankerbendAppId = VIM_NAV_APP_ID;
1307
1528
  label.addEventListener("click", (event) => {
1308
1529
  event.preventDefault();
1309
1530
  event.stopImmediatePropagation();
@@ -1318,10 +1539,10 @@
1318
1539
  }
1319
1540
  label.textContent = markerGlyph(item);
1320
1541
  label.title = item.indexed
1321
- ? `Vim Nav ${item.order}: ${item.inferredRole || item.kind}`
1322
- : `Vim Nav index pending: ${item.inferredRole || item.kind}`;
1542
+ ? `VimNav ${item.order}: ${item.inferredRole || item.kind}`
1543
+ : `VimNav index pending: ${item.inferredRole || item.kind}`;
1323
1544
  label.setAttribute("aria-label", label.title);
1324
- oneWhackRuntime().placeAnnotation?.(el, {
1545
+ clankerbendRuntime().placeAnnotation?.(el, {
1325
1546
  appId: VIM_NAV_APP_ID,
1326
1547
  anchorId: item.anchorId,
1327
1548
  markerId: `vim-nav:${item.anchorId}`,
@@ -1329,9 +1550,9 @@
1329
1550
  element: label
1330
1551
  }) || el.insertAdjacentElement("afterbegin", label);
1331
1552
  }
1332
- oneWhackRuntime().removeAnnotations?.(VIM_NAV_APP_ID, ids);
1333
- document.querySelectorAll(`.${ANNOTATION_CLASS}[data-onewhack-app-id="${VIM_NAV_APP_ID}"]`).forEach((node) => {
1334
- if (!ids.has(node.dataset.anchorId) && !node.closest(".onewhack-transcript-annotation-slot")) node.remove();
1553
+ clankerbendRuntime().removeAnnotations?.(VIM_NAV_APP_ID, ids);
1554
+ document.querySelectorAll(`.${ANNOTATION_CLASS}[data-clankerbend-app-id="${VIM_NAV_APP_ID}"]`).forEach((node) => {
1555
+ if (!ids.has(node.dataset.anchorId) && !node.closest(".clankerbend-transcript-annotation-slot")) node.remove();
1335
1556
  });
1336
1557
  }
1337
1558
 
@@ -1429,123 +1650,6 @@
1429
1650
  });
1430
1651
  }
1431
1652
 
1432
- async function jumpRelativeMountedAcrossGap(delta, currentOrder, beforeAnchorId) {
1433
- const targetIndex = currentOrder - 1 + delta;
1434
- const indexedTarget = await mountTargetForIndex(targetIndex, delta, {
1435
- approximate: true,
1436
- preferExact: true
1437
- });
1438
- if (indexedTarget && indexedTarget.anchorId !== beforeAnchorId) {
1439
- setCurrent(indexedTarget.anchorId, "adapter", indexedTarget.order - 1);
1440
- return scrollToAnchor(indexedTarget.anchorId, {
1441
- behavior: "auto",
1442
- block: "start",
1443
- indexHint: indexedTarget.order - 1
1444
- });
1445
- }
1446
-
1447
- for (let attempt = 0; attempt < 5; attempt += 1) {
1448
- const mounted = anchors().sort((a, b) => a.order - b.order);
1449
- const adjacent = delta < 0
1450
- ? [...mounted].reverse().find((item) => item.order < currentOrder && item.anchorId !== beforeAnchorId)
1451
- : mounted.find((item) => item.order > currentOrder && item.anchorId !== beforeAnchorId);
1452
- if (adjacent) {
1453
- setCurrent(adjacent.anchorId, "adapter", adjacent.order - 1);
1454
- return scrollToAnchor(adjacent.anchorId, {
1455
- behavior: "auto",
1456
- block: blockForAnchorVisibility(adjacent.anchorId, "start"),
1457
- indexHint: adjacent.order - 1
1458
- });
1459
- }
1460
- await delay(90);
1461
- }
1462
-
1463
- const searchRoot = () => findScrollContainer() || nearestScrollContainer(anchorElements()[0] || findAnchor(state.currentAnchorId) || document.body);
1464
- let root = searchRoot();
1465
- const viewport = Math.min(root.clientHeight || window.innerHeight || 720, 1200);
1466
- const step = delta < 0
1467
- ? Math.max(520, viewport * 1.45)
1468
- : Math.max(520, Math.min(viewport, 900) * 1.35);
1469
- const attempts = [];
1470
- for (let attempt = 0; attempt < 18; attempt += 1) {
1471
- root = searchRoot();
1472
- const beforeTop = scrollTopOf(root);
1473
- scrollRootTo(root, scrollTopForViewportDelta(root, delta < 0 ? -step : step));
1474
- await delay(80);
1475
- const mounted = anchors().sort((a, b) => a.order - b.order);
1476
- const target = delta < 0
1477
- ? [...mounted].reverse().find((item) =>
1478
- item.order < currentOrder && item.anchorId !== beforeAnchorId
1479
- )
1480
- : mounted.find((item) =>
1481
- item.order > currentOrder && item.anchorId !== beforeAnchorId
1482
- );
1483
- attempts.push({
1484
- attempt,
1485
- beforeTop: Math.round(beforeTop),
1486
- afterTop: Math.round(scrollTopOf(root)),
1487
- mounted: mounted.map((item) => item.order)
1488
- });
1489
- state.lastMountSearch = { mode: "relative-mounted-gap", direction: delta, currentOrder, attempts: attempts.slice(-5) };
1490
- if (target) {
1491
- setCurrent(target.anchorId, "adapter", target.order - 1);
1492
- return scrollToAnchor(target.anchorId, {
1493
- behavior: "auto",
1494
- block: "start",
1495
- indexHint: target.order - 1
1496
- });
1497
- }
1498
- if (Math.abs(scrollTopOf(root) - beforeTop) <= 1) break;
1499
- }
1500
- if (delta > 0) {
1501
- root = searchRoot();
1502
- scrollRootTo(root, scrollRootRange(root).max);
1503
- await delay(120);
1504
- const mounted = anchors().sort((a, b) => a.order - b.order);
1505
- const target = mounted.find((item) =>
1506
- item.order > currentOrder && item.anchorId !== beforeAnchorId
1507
- ) || mounted[mounted.length - 1];
1508
- if (target && target.anchorId !== beforeAnchorId && target.order > currentOrder) {
1509
- setCurrent(target.anchorId, "adapter", target.order - 1);
1510
- return scrollToAnchor(target.anchorId, {
1511
- behavior: "auto",
1512
- block: "start",
1513
- indexHint: target.order - 1
1514
- });
1515
- }
1516
- }
1517
- if (delta > 0 && currentOrder < lastKnownIndex() + 1) {
1518
- return jumpToBottom();
1519
- }
1520
- if (delta < 0 && currentOrder > 1) {
1521
- return jumpToIndex(0, { block: "start", direction: -1, behavior: "auto" });
1522
- }
1523
- return jumpRelativeMounted(delta);
1524
- }
1525
-
1526
- function renderedCurrentOrder() {
1527
- const desiredTop = topVisibleInset() + 8;
1528
- const visibleMarker = Array.from(document.querySelectorAll(`.${ANNOTATION_CLASS}`))
1529
- .map((marker) => {
1530
- const rect = marker.getBoundingClientRect();
1531
- return {
1532
- order: Number(marker.textContent.trim()),
1533
- top: rect.top,
1534
- bottom: rect.bottom
1535
- };
1536
- })
1537
- .filter((item) =>
1538
- Number.isSafeInteger(item.order) &&
1539
- item.bottom >= topVisibleInset() &&
1540
- item.top <= viewportSafeBottom()
1541
- )
1542
- .sort((a, b) => Math.abs(a.top - desiredTop) - Math.abs(b.top - desiredTop))[0];
1543
- if (visibleMarker) return visibleMarker.order;
1544
- const currentMarker = document.querySelector(`.${CURRENT_CLASS} .${ANNOTATION_CLASS}`);
1545
- const currentValue = Number(currentMarker?.textContent?.trim());
1546
- return Number.isSafeInteger(currentValue) ? currentValue : null;
1547
- }
1548
-
1549
1653
  function mountedFloorTargetForIndex(index) {
1550
1654
  const visible = viewportVisibleAnchors(anchors()).sort((a, b) => a.order - b.order);
1551
1655
  const visibleExact = visible.find((item) => item.order - 1 === index);
@@ -1791,59 +1895,56 @@
1791
1895
  });
1792
1896
  }
1793
1897
 
1794
- async function jumpRelative(delta) {
1795
- const visible = viewportVisibleAnchors(anchors());
1796
- const currentVisible = state.currentAnchorId ? isAnchorViewportVisible(state.currentAnchorId) : false;
1898
+ function jumpRelativeFast(delta) {
1899
+ if (!hasAppServerOrder()) return jumpRelativeMounted(delta);
1900
+ const startIndex = currentIndex();
1901
+ const targetIndex = startIndex + delta;
1902
+ const lastIndex = lastKnownIndex();
1903
+ if (targetIndex < 0 || targetIndex > lastIndex) {
1904
+ state.lastRelativeDebug = {
1905
+ delta,
1906
+ path: "edge",
1907
+ startIndex,
1908
+ targetIndex,
1909
+ lastIndex
1910
+ };
1911
+ return {
1912
+ ok: true,
1913
+ noop: true,
1914
+ edge: delta < 0 ? "top" : "bottom",
1915
+ anchorId: state.currentAnchorId
1916
+ };
1917
+ }
1918
+ const targetId = state.anchorOrder[targetIndex] || state.anchorIdByIndex[targetIndex] || null;
1797
1919
  const mounted = anchors().sort((a, b) => a.order - b.order);
1798
- const selectedIsMounted = Boolean(state.currentAnchorId && mounted.some((item) => item.anchorId === state.currentAnchorId));
1799
- if (!visible.length && !currentVisible) {
1800
- return jumpRelativeMounted(delta);
1801
- }
1802
- if (!currentVisible && visible.length && !selectedIsMounted) {
1803
- const target = delta < 0
1804
- ? visible.sort((a, b) => a.viewportTop - b.viewportTop)[0]
1805
- : visible.sort((a, b) => b.viewportBottom - a.viewportBottom)[0];
1806
- if (target) setCurrent(target.anchorId, "adapter", target.order - 1);
1807
- }
1808
- const renderedOrder = renderedCurrentOrder();
1809
- const selectedOrder = state.currentAnchorId ? anchorNumber(state.currentAnchorId) : null;
1810
- const currentAnchorOrder = selectedIsMounted ? selectedOrder : renderedOrder || selectedOrder;
1811
- const currentOrder = currentAnchorOrder != null && currentAnchorOrder <= state.anchorOrder.length
1812
- ? currentAnchorOrder
1813
- : Number.isInteger(state.currentIndexValue) ? state.currentIndexValue + 1 : null;
1814
- const adjacentMounted = delta < 0
1815
- ? [...mounted].reverse().find((item) => item.order < currentOrder && item.anchorId !== state.currentAnchorId)
1816
- : mounted.find((item) => item.order > currentOrder && item.anchorId !== state.currentAnchorId);
1920
+ const target = targetId
1921
+ ? mounted.find((item) => item.anchorId === targetId)
1922
+ : mounted.find((item) => item.order - 1 === targetIndex);
1817
1923
  state.lastRelativeDebug = {
1818
1924
  delta,
1819
- renderedOrder,
1820
- currentAnchorOrder,
1821
- currentOrder,
1822
- currentAnchorId: state.currentAnchorId,
1823
- selectedIsMounted,
1824
- adjacent: adjacentMounted ? { order: adjacentMounted.order, anchorId: adjacentMounted.anchorId } : null,
1925
+ path: "fast",
1926
+ startIndex,
1927
+ targetIndex,
1928
+ target: target ? { order: target.order, anchorId: target.anchorId } : null,
1825
1929
  mounted: mounted.map((item) => item.order)
1826
1930
  };
1827
- if (adjacentMounted) {
1828
- setCurrent(adjacentMounted.anchorId, "adapter", adjacentMounted.order - 1);
1829
- return scrollToAnchor(adjacentMounted.anchorId, {
1931
+ if (!target || !findAnchor(target.anchorId)) {
1932
+ return jumpToIndex(targetIndex, {
1933
+ block: "start",
1934
+ direction: delta,
1830
1935
  behavior: "auto",
1831
- block: blockForAnchorVisibility(adjacentMounted.anchorId, "start"),
1832
- indexHint: adjacentMounted.order - 1
1936
+ approximate: true,
1937
+ preferExact: true,
1938
+ resolveMissing: "floor-on-overshoot",
1939
+ skipScrollIfResolvedFloor: true
1833
1940
  });
1834
1941
  }
1835
- const beforeAnchorId = state.currentAnchorId;
1836
- if (delta < 0) {
1837
- return jumpRelativeMountedAcrossGap(delta, currentOrder, beforeAnchorId);
1838
- }
1839
- const beforeIndex = currentIndex();
1840
- const result = await jumpToIndex(beforeIndex + delta, { block: "start", direction: delta, behavior: "auto" });
1841
- const afterIndex = state.anchorOrder.indexOf(state.currentAnchorId);
1842
- const moved = result?.ok &&
1843
- state.currentAnchorId !== beforeAnchorId &&
1844
- (delta < 0 ? afterIndex < beforeIndex : afterIndex > beforeIndex);
1845
- if (moved) return result;
1846
- return jumpRelativeMountedAcrossGap(delta, currentOrder, beforeAnchorId);
1942
+ setCurrent(target.anchorId, "adapter", target.order - 1);
1943
+ return scrollToAnchor(target.anchorId, {
1944
+ behavior: "auto",
1945
+ block: "start",
1946
+ indexHint: target.order - 1
1947
+ });
1847
1948
  }
1848
1949
 
1849
1950
  function jumpToNumberIndex(requested) {
@@ -2173,14 +2274,14 @@
2173
2274
  return new Promise((resolve) => setTimeout(resolve, ms));
2174
2275
  }
2175
2276
 
2176
- function oneWhackRuntime() {
2277
+ function clankerbendRuntime() {
2177
2278
  if (window[RUNTIME_KEY]?.registerApp) return window[RUNTIME_KEY];
2178
2279
  const runtime = {
2179
2280
  protocolVersion: null,
2180
2281
  hostUrl: null,
2181
2282
  apps: {},
2182
2283
  registerApp(app) {
2183
- if (!app?.appId) throw new Error("OneWhack appId is required");
2284
+ if (!app?.appId) throw new Error("ClankerBend appId is required");
2184
2285
  const current = this.apps[app.appId] || {};
2185
2286
  const currentVersion = Number(current.bridge?.version);
2186
2287
  const nextVersion = Number(app.bridge?.version);
@@ -2191,7 +2292,7 @@
2191
2292
  Number.isFinite(nextVersion) &&
2192
2293
  nextVersion < currentVersion
2193
2294
  ) {
2194
- throw new Error("OneWhack app bridge version regressed for " + app.appId);
2295
+ throw new Error("ClankerBend app bridge version regressed for " + app.appId);
2195
2296
  }
2196
2297
  const slot = {
2197
2298
  ...current,
@@ -2234,6 +2335,8 @@
2234
2335
 
2235
2336
  function applyHostState(hostState) {
2236
2337
  state.hostState = hostState || null;
2338
+ settleAccountUiStatus();
2339
+ renderCodexAccountSwitcher();
2237
2340
  renderSelectionMenu();
2238
2341
  renderOverlay();
2239
2342
  renderComposerContextChips();
@@ -2242,6 +2345,430 @@
2242
2345
  return { ok: true };
2243
2346
  }
2244
2347
 
2348
+ function settleAccountUiStatus() {
2349
+ if (state.accountUiStatus !== "Saving default...") return;
2350
+ if (state.hostState?.codexAccounts?.switching) return;
2351
+ state.accountUiStatus = "Default saved";
2352
+ setTimeout(() => {
2353
+ if (state.accountUiStatus !== "Default saved") return;
2354
+ state.accountUiStatus = "";
2355
+ renderCodexAccountSwitcher();
2356
+ }, 1400);
2357
+ }
2358
+
2359
+ function codexAccountsState() {
2360
+ return state.hostState?.codexAccounts || null;
2361
+ }
2362
+
2363
+ function visibleElement(el) {
2364
+ if (!(el instanceof HTMLElement)) return false;
2365
+ if (!el.isConnected) return false;
2366
+ if (isHostUiElement(el)) return false;
2367
+ const rect = el.getBoundingClientRect();
2368
+ if (rect.width <= 0 || rect.height <= 0) return false;
2369
+ const style = getComputedStyle(el);
2370
+ return style.visibility !== "hidden" && style.display !== "none";
2371
+ }
2372
+
2373
+ function normalizedText(el) {
2374
+ return String(el?.innerText || el?.textContent || "").replace(/\s+/g, " ").trim();
2375
+ }
2376
+
2377
+ function findLogoutMenuItem() {
2378
+ const candidates = Array.from(document.querySelectorAll("button,a,[role='menuitem'],[role='button']"))
2379
+ .filter(visibleElement)
2380
+ .map((el) => {
2381
+ const text = normalizedText(el);
2382
+ const label = [text, el.getAttribute("aria-label"), el.getAttribute("title")].filter(Boolean).join(" ");
2383
+ return { el, label };
2384
+ })
2385
+ .filter((candidate) => /\b(log\s*out|logout|sign\s*out)\b/i.test(candidate.label));
2386
+ return candidates
2387
+ .map((candidate) => candidate.el.closest?.("button,a,[role='menuitem']") || candidate.el)
2388
+ .find((el) => el instanceof HTMLElement && visibleElement(el)) || null;
2389
+ }
2390
+
2391
+ function accountMenuInsertionParent(logoutItem) {
2392
+ if (!(logoutItem instanceof HTMLElement)) return null;
2393
+ for (let node = logoutItem.parentElement; node && node !== document.body && node !== document.documentElement; node = node.parentElement) {
2394
+ if (isHostUiElement(node)) return null;
2395
+ const rect = node.getBoundingClientRect();
2396
+ if (rect.width < 150 || rect.width > 520 || rect.height < 40 || rect.height > window.innerHeight * 0.9) continue;
2397
+ if (node.contains(logoutItem)) return node;
2398
+ }
2399
+ return logoutItem.parentElement;
2400
+ }
2401
+
2402
+ function accountMenuInsertionReference(parent, logoutItem) {
2403
+ if (!(parent instanceof HTMLElement) || !(logoutItem instanceof HTMLElement)) return null;
2404
+ let reference = logoutItem;
2405
+ while (reference?.parentElement && reference.parentElement !== parent) {
2406
+ reference = reference.parentElement;
2407
+ }
2408
+ return reference?.parentElement === parent ? reference : null;
2409
+ }
2410
+
2411
+ function accountUiSignature(accountsState) {
2412
+ const accounts = accountsState?.accounts || [];
2413
+ return JSON.stringify({
2414
+ active: accountsState?.activeAccountId || null,
2415
+ def: accountsState?.clankerbendDefaultAccountId || null,
2416
+ switching: Boolean(accountsState?.switching),
2417
+ status: state.accountUiStatus,
2418
+ addOpen: state.accountAddOpen,
2419
+ manageOpen: state.accountManageOpen,
2420
+ pendingAdopt: state.accountPendingAdoptId,
2421
+ pendingDelete: state.accountPendingDeleteId,
2422
+ accounts: accounts.map((account) => [
2423
+ account.id,
2424
+ account.kind,
2425
+ account.label,
2426
+ account.codexHome,
2427
+ account.auth?.authJson ? 1 : 0,
2428
+ account.backup ? 1 : 0
2429
+ ])
2430
+ });
2431
+ }
2432
+
2433
+ function renderCodexAccountSwitcher() {
2434
+ const accountsState = codexAccountsState();
2435
+ const logoutItem = findLogoutMenuItem();
2436
+ if (!accountsState?.available || !logoutItem) {
2437
+ document.getElementById(ACCOUNT_SWITCHER_ID)?.remove();
2438
+ return;
2439
+ }
2440
+
2441
+ const parent = accountMenuInsertionParent(logoutItem);
2442
+ if (!(parent instanceof HTMLElement)) return;
2443
+ const reference = accountMenuInsertionReference(parent, logoutItem);
2444
+ if (!(reference instanceof HTMLElement)) return;
2445
+ let ui = document.getElementById(ACCOUNT_SWITCHER_ID);
2446
+ if (ui && ui.parentElement !== parent) {
2447
+ ui.remove();
2448
+ ui = null;
2449
+ }
2450
+ if (!ui) {
2451
+ ui = document.createElement("div");
2452
+ ui.id = ACCOUNT_SWITCHER_ID;
2453
+ ui.className = HOST_UI_CLASS;
2454
+ stopNativeMenuPropagation(ui);
2455
+ parent.insertBefore(ui, reference);
2456
+ } else if (ui.nextElementSibling !== reference) {
2457
+ parent.insertBefore(ui, reference);
2458
+ }
2459
+
2460
+ const signature = accountUiSignature(accountsState);
2461
+ if (ui.dataset.signature === signature) return;
2462
+ ui.dataset.signature = signature;
2463
+ ui.replaceChildren();
2464
+
2465
+ const head = document.createElement("div");
2466
+ head.className = "clankerbend-account-head";
2467
+ const title = document.createElement("div");
2468
+ title.className = "clankerbend-account-title";
2469
+ title.textContent = "ClankerID";
2470
+ const status = document.createElement("div");
2471
+ status.className = "clankerbend-account-status";
2472
+ status.textContent = state.accountUiStatus || (accountsState.switching ? "Switching..." : `${accountsState.accounts?.length || 0}/${accountsState.maxAccounts || 20}`);
2473
+ head.append(title, status);
2474
+ ui.appendChild(head);
2475
+
2476
+ if (state.accountManageOpen) {
2477
+ ui.appendChild(renderCodexAccountManage(accountsState));
2478
+ } else {
2479
+ const list = document.createElement("div");
2480
+ list.className = "clankerbend-account-list";
2481
+ for (const account of accountsState.accounts || []) {
2482
+ list.appendChild(renderCodexAccountRow(account, accountsState));
2483
+ }
2484
+ ui.appendChild(list);
2485
+ }
2486
+
2487
+ const actions = document.createElement("div");
2488
+ actions.className = "clankerbend-account-actions";
2489
+ const add = document.createElement("button");
2490
+ add.type = "button";
2491
+ add.textContent = "Add account";
2492
+ add.disabled = Boolean(accountsState.switching) || (accountsState.accounts?.length || 0) >= (accountsState.maxAccounts || 20);
2493
+ bindInjectedActivation(add, () => {
2494
+ state.accountAddOpen = true;
2495
+ state.accountUiStatus = "";
2496
+ renderCodexAccountSwitcher();
2497
+ setTimeout(() => {
2498
+ document.querySelector(`#${ACCOUNT_SWITCHER_ID} input[data-clankerbend-account-label]`)?.focus?.({ preventScroll: true });
2499
+ }, 0);
2500
+ });
2501
+ const manage = document.createElement("button");
2502
+ manage.type = "button";
2503
+ manage.textContent = state.accountManageOpen ? "Done" : "Manage";
2504
+ bindInjectedActivation(manage, () => {
2505
+ state.accountManageOpen = !state.accountManageOpen;
2506
+ state.accountPendingAdoptId = null;
2507
+ state.accountPendingDeleteId = null;
2508
+ state.accountUiStatus = "";
2509
+ renderCodexAccountSwitcher();
2510
+ });
2511
+ actions.append(add, manage);
2512
+ ui.appendChild(actions);
2513
+ if (state.accountAddOpen) ui.appendChild(renderCodexAccountAddForm(accountsState));
2514
+ }
2515
+
2516
+ function renderCodexAccountAddForm(accountsState) {
2517
+ const form = document.createElement("div");
2518
+ form.className = "clankerbend-account-add-form";
2519
+
2520
+ const input = document.createElement("input");
2521
+ input.type = "text";
2522
+ input.dataset.clankerbendAccountLabel = "true";
2523
+ input.placeholder = "Account label";
2524
+ input.value = state.accountAddLabel || "";
2525
+ input.autocomplete = "off";
2526
+ input.spellcheck = false;
2527
+ const stopInputEvent = (event) => {
2528
+ event.stopPropagation();
2529
+ event.stopImmediatePropagation?.();
2530
+ };
2531
+ input.addEventListener("pointerdown", stopInputEvent, true);
2532
+ input.addEventListener("mousedown", stopInputEvent, true);
2533
+ input.addEventListener("click", stopInputEvent, true);
2534
+ input.addEventListener("input", () => {
2535
+ state.accountAddLabel = input.value;
2536
+ });
2537
+ input.addEventListener("keydown", (event) => {
2538
+ event.stopPropagation();
2539
+ event.stopImmediatePropagation?.();
2540
+ if (event.key === "Enter") {
2541
+ event.preventDefault();
2542
+ submitCodexAccountAdd(input.value);
2543
+ } else if (event.key === "Escape") {
2544
+ event.preventDefault();
2545
+ cancelCodexAccountAdd();
2546
+ }
2547
+ }, true);
2548
+
2549
+ const buttons = document.createElement("div");
2550
+ buttons.className = "clankerbend-account-add-actions";
2551
+ const cancel = document.createElement("button");
2552
+ cancel.type = "button";
2553
+ cancel.textContent = "Cancel";
2554
+ bindInjectedActivation(cancel, cancelCodexAccountAdd);
2555
+ const create = document.createElement("button");
2556
+ create.type = "button";
2557
+ create.textContent = "Create";
2558
+ create.disabled = Boolean(accountsState.switching);
2559
+ bindInjectedActivation(create, () => submitCodexAccountAdd(input.value));
2560
+ buttons.append(cancel, create);
2561
+ form.append(input, buttons);
2562
+ return form;
2563
+ }
2564
+
2565
+ function cancelCodexAccountAdd() {
2566
+ state.accountAddOpen = false;
2567
+ state.accountAddLabel = "";
2568
+ state.accountUiStatus = "";
2569
+ renderCodexAccountSwitcher();
2570
+ }
2571
+
2572
+ function submitCodexAccountAdd(rawLabel) {
2573
+ const label = String(rawLabel || "").trim();
2574
+ if (!label) {
2575
+ state.accountUiStatus = "Label required";
2576
+ renderCodexAccountSwitcher();
2577
+ setTimeout(() => {
2578
+ if (state.accountUiStatus !== "Label required") return;
2579
+ state.accountUiStatus = "";
2580
+ renderCodexAccountSwitcher();
2581
+ }, 1400);
2582
+ return;
2583
+ }
2584
+ state.accountAddOpen = false;
2585
+ state.accountAddLabel = "";
2586
+ state.accountUiStatus = "Creating...";
2587
+ renderCodexAccountSwitcher();
2588
+ enqueueHostEvent({ kind: "codexAccountCreateAndSwitch", label });
2589
+ }
2590
+
2591
+ function renderCodexAccountManage(accountsState) {
2592
+ const wrap = document.createElement("div");
2593
+ wrap.className = "clankerbend-account-manage";
2594
+ const head = document.createElement("div");
2595
+ head.className = "clankerbend-account-manage-head";
2596
+ const title = document.createElement("strong");
2597
+ title.textContent = "Manage profiles";
2598
+ const count = document.createElement("span");
2599
+ const managed = (accountsState.accounts || []).filter((account) => account.kind === "managed");
2600
+ count.textContent = `${managed.length} managed`;
2601
+ head.append(title, count);
2602
+ wrap.appendChild(head);
2603
+ const list = document.createElement("div");
2604
+ list.className = "clankerbend-account-manage-list";
2605
+ if (!managed.length) {
2606
+ const row = document.createElement("div");
2607
+ row.className = "clankerbend-account-manage-row";
2608
+ const title = document.createElement("div");
2609
+ title.className = "clankerbend-account-manage-title";
2610
+ const strong = document.createElement("strong");
2611
+ strong.textContent = "No managed accounts";
2612
+ const hint = document.createElement("span");
2613
+ hint.textContent = "Create one with Add account.";
2614
+ title.append(strong, hint);
2615
+ row.appendChild(title);
2616
+ list.appendChild(row);
2617
+ wrap.appendChild(list);
2618
+ return wrap;
2619
+ }
2620
+ for (const account of managed) list.appendChild(renderCodexAccountManageRow(account, accountsState));
2621
+ wrap.appendChild(list);
2622
+ return wrap;
2623
+ }
2624
+
2625
+ function renderCodexAccountManageRow(account, accountsState) {
2626
+ const row = document.createElement("div");
2627
+ row.className = "clankerbend-account-manage-row";
2628
+ row.title = accountCodexHomeTitle(account);
2629
+ const title = document.createElement("div");
2630
+ title.className = "clankerbend-account-manage-title";
2631
+ const strong = document.createElement("strong");
2632
+ strong.textContent = account.label || account.id;
2633
+ const hint = document.createElement("span");
2634
+ const pendingAdopt = state.accountPendingAdoptId === account.id;
2635
+ const pendingDelete = state.accountPendingDeleteId === account.id;
2636
+ hint.textContent = pendingAdopt
2637
+ ? "Replace ~/.codex with this profile; current primary is backed up."
2638
+ : pendingDelete
2639
+ ? "Hide this profile and move its files to archive storage."
2640
+ : [account.auth?.authJson ? "signed in" : "not signed in", account.backup ? "backup" : ""].filter(Boolean).join(" · ");
2641
+ title.append(strong, hint);
2642
+
2643
+ const actions = document.createElement("div");
2644
+ actions.className = "clankerbend-account-manage-actions";
2645
+ const adopt = document.createElement("button");
2646
+ adopt.type = "button";
2647
+ adopt.textContent = pendingAdopt ? "Replace ~/.codex" : "Make primary";
2648
+ adopt.title = "Make this the primary Codex home";
2649
+ adopt.disabled = Boolean(accountsState.switching);
2650
+ bindInjectedActivation(adopt, () => {
2651
+ if (!pendingAdopt) {
2652
+ state.accountPendingAdoptId = account.id;
2653
+ state.accountPendingDeleteId = null;
2654
+ state.accountUiStatus = "";
2655
+ renderCodexAccountSwitcher();
2656
+ return;
2657
+ }
2658
+ state.accountUiStatus = "Making primary...";
2659
+ state.accountPendingAdoptId = null;
2660
+ renderCodexAccountSwitcher();
2661
+ enqueueHostEvent({ kind: "codexAccountAdoptAsPrimary", accountId: account.id });
2662
+ });
2663
+
2664
+ const remove = document.createElement("button");
2665
+ remove.type = "button";
2666
+ remove.textContent = pendingDelete ? "Archive profile" : "Archive";
2667
+ remove.title = "Archive this managed profile";
2668
+ remove.disabled = Boolean(accountsState.switching) || account.id === accountsState.activeAccountId;
2669
+ bindInjectedActivation(remove, () => {
2670
+ if (!pendingDelete) {
2671
+ state.accountPendingDeleteId = account.id;
2672
+ state.accountPendingAdoptId = null;
2673
+ state.accountUiStatus = "";
2674
+ renderCodexAccountSwitcher();
2675
+ return;
2676
+ }
2677
+ state.accountUiStatus = "Archiving...";
2678
+ state.accountPendingDeleteId = null;
2679
+ renderCodexAccountSwitcher();
2680
+ enqueueHostEvent({ kind: "codexAccountDelete", accountId: account.id });
2681
+ });
2682
+
2683
+ actions.append(adopt, remove);
2684
+ row.append(title, actions);
2685
+ return row;
2686
+ }
2687
+
2688
+ function stopNativeMenuPropagation(el) {
2689
+ const stop = (event) => {
2690
+ event.stopPropagation();
2691
+ event.stopImmediatePropagation?.();
2692
+ };
2693
+ el.addEventListener("pointerdown", stop);
2694
+ el.addEventListener("mousedown", stop);
2695
+ el.addEventListener("touchstart", stop, { passive: false });
2696
+ el.addEventListener("click", stop);
2697
+ }
2698
+
2699
+ function bindInjectedActivation(button, handler) {
2700
+ let lastRunAt = 0;
2701
+ const run = (event) => {
2702
+ event.preventDefault();
2703
+ event.stopPropagation();
2704
+ event.stopImmediatePropagation?.();
2705
+ if (button.disabled) return;
2706
+ const now = Date.now();
2707
+ if (now - lastRunAt < 450) return;
2708
+ lastRunAt = now;
2709
+ handler(event);
2710
+ };
2711
+ button.addEventListener("pointerdown", run, true);
2712
+ button.addEventListener("mousedown", run, true);
2713
+ button.addEventListener("touchstart", run, { capture: true, passive: false });
2714
+ button.addEventListener("click", run, true);
2715
+ }
2716
+
2717
+ function renderCodexAccountRow(account, accountsState) {
2718
+ const row = document.createElement("div");
2719
+ row.className = `clankerbend-account-row${account.id === accountsState.activeAccountId ? " is-active" : ""}`;
2720
+
2721
+ const main = document.createElement("button");
2722
+ main.type = "button";
2723
+ main.className = "clankerbend-account-main";
2724
+ main.disabled = Boolean(accountsState.switching) || account.id === accountsState.activeAccountId;
2725
+ main.title = [
2726
+ account.id === accountsState.activeAccountId ? "Current Codex account profile" : `Switch to ${account.label || account.id}`,
2727
+ accountCodexHomeTitle(account)
2728
+ ].filter(Boolean).join("\n");
2729
+ bindInjectedActivation(main, () => {
2730
+ if (account.id === accountsState.activeAccountId) return;
2731
+ state.accountUiStatus = "Switching...";
2732
+ renderCodexAccountSwitcher();
2733
+ enqueueHostEvent({ kind: "codexAccountSwitch", accountId: account.id });
2734
+ });
2735
+
2736
+ const label = document.createElement("span");
2737
+ label.className = "clankerbend-account-label";
2738
+ label.textContent = account.label || account.id;
2739
+ const meta = document.createElement("span");
2740
+ meta.className = "clankerbend-account-meta";
2741
+ meta.textContent = [
2742
+ account.kind,
2743
+ account.id === accountsState.clankerbendDefaultAccountId ? "default" : "",
2744
+ account.auth?.authJson ? "signed in" : "not signed in"
2745
+ ].filter(Boolean).join(" · ");
2746
+ main.append(label, meta);
2747
+
2748
+ const def = document.createElement("button");
2749
+ def.type = "button";
2750
+ def.className = "clankerbend-account-icon";
2751
+ def.textContent = account.id === accountsState.clankerbendDefaultAccountId ? "✓" : "☆";
2752
+ def.setAttribute("aria-label", account.id === accountsState.clankerbendDefaultAccountId ? "Launches by default" : "Launch by default");
2753
+ def.title = [
2754
+ account.id === accountsState.clankerbendDefaultAccountId ? "Launches by default" : "Launch by default",
2755
+ accountCodexHomeTitle(account)
2756
+ ].filter(Boolean).join("\n");
2757
+ def.disabled = Boolean(accountsState.switching) || account.id === accountsState.clankerbendDefaultAccountId;
2758
+ bindInjectedActivation(def, () => {
2759
+ state.accountUiStatus = "Saving default...";
2760
+ renderCodexAccountSwitcher();
2761
+ enqueueHostEvent({ kind: "codexAccountSetDefault", accountId: account.id });
2762
+ });
2763
+
2764
+ row.append(main, def);
2765
+ return row;
2766
+ }
2767
+
2768
+ function accountCodexHomeTitle(account) {
2769
+ return account?.codexHome ? `CODEX_HOME: ${account.codexHome}` : "";
2770
+ }
2771
+
2245
2772
  function hostSelectionActions() {
2246
2773
  return (state.hostState?.selectionActions || [])
2247
2774
  .filter((action) => action?.enabled !== false && action.appliesTo === "text-selection");
@@ -2332,7 +2859,7 @@
2332
2859
  function hideSelectionMenu() {
2333
2860
  const menu = document.getElementById(SELECTION_MENU_ID);
2334
2861
  menu?.remove();
2335
- document.querySelectorAll("[data-onewhack-native-selection-action='true']").forEach((node) => node.remove());
2862
+ document.querySelectorAll("[data-clankerbend-native-selection-action='true']").forEach((node) => node.remove());
2336
2863
  }
2337
2864
 
2338
2865
  function selectionPayloadForAction(action, selection) {
@@ -2419,7 +2946,7 @@
2419
2946
 
2420
2947
  function extendNativeSelectionToolbar(toolbar, target) {
2421
2948
  if (!(toolbar instanceof HTMLElement)) return;
2422
- toolbar.dataset.onewhackNativeSelectionToolbar = "true";
2949
+ toolbar.dataset.clankerbendNativeSelectionToolbar = "true";
2423
2950
  toolbar.style.setProperty("display", "inline-flex", "important");
2424
2951
  toolbar.style.setProperty("align-items", "center", "important");
2425
2952
  toolbar.style.setProperty("flex-wrap", "nowrap", "important");
@@ -2465,7 +2992,7 @@
2465
2992
  triggerSelectionAction(action, selection);
2466
2993
  };
2467
2994
  button.type = "button";
2468
- button.dataset.onewhackNativeSelectionAction = "true";
2995
+ button.dataset.clankerbendNativeSelectionAction = "true";
2469
2996
  button.textContent = nativeSelectionButtonLabel(action);
2470
2997
  button.title = action.label || action.type;
2471
2998
  button.style.cssText = [
@@ -2503,7 +3030,7 @@
2503
3030
  function injectNativeSelectionActions(selection, actions) {
2504
3031
  const toolbar = nativeSelectionToolbar();
2505
3032
  if (!toolbar) return false;
2506
- document.querySelectorAll("[data-onewhack-native-selection-action='true']").forEach((node) => node.remove());
3033
+ document.querySelectorAll("[data-clankerbend-native-selection-action='true']").forEach((node) => node.remove());
2507
3034
  const target = nativeSelectionInsertionTarget(toolbar);
2508
3035
  extendNativeSelectionToolbar(toolbar, target);
2509
3036
  const referenceButton = nativeSelectionReferenceButton(toolbar);
@@ -2525,7 +3052,7 @@
2525
3052
  const actions = hostSelectionActions();
2526
3053
  if (!selection || !actions.length) {
2527
3054
  menu?.remove();
2528
- document.querySelectorAll("[data-onewhack-native-selection-action='true']").forEach((node) => node.remove());
3055
+ document.querySelectorAll("[data-clankerbend-native-selection-action='true']").forEach((node) => node.remove());
2529
3056
  return;
2530
3057
  }
2531
3058
  if (injectNativeSelectionActions(selection, actions)) {
@@ -2563,8 +3090,8 @@
2563
3090
  }
2564
3091
 
2565
3092
  function overlayFieldValues(el) {
2566
- return Object.fromEntries([...el.querySelectorAll("[data-onewhack-field-id]")]
2567
- .map((input) => [input.dataset.onewhackFieldId, input.value || ""]));
3093
+ return Object.fromEntries([...el.querySelectorAll("[data-clankerbend-field-id]")]
3094
+ .map((input) => [input.dataset.clankerbendFieldId, input.value || ""]));
2568
3095
  }
2569
3096
 
2570
3097
  function positionOverlay(el, overlay) {
@@ -2613,13 +3140,13 @@
2613
3140
  return;
2614
3141
  }
2615
3142
  const currentValues = overlayFieldValues(el);
2616
- const activeFieldId = document.activeElement?.dataset?.onewhackFieldId || null;
3143
+ const activeFieldId = document.activeElement?.dataset?.clankerbendFieldId || null;
2617
3144
  const fields = (overlay.fields || []).map((field) => {
2618
3145
  const input = field.kind === "textarea" ? document.createElement("textarea") : document.createElement("input");
2619
3146
  input.name = field.fieldId;
2620
3147
  input.placeholder = field.label || field.fieldId;
2621
3148
  input.value = currentValues[field.fieldId] ?? field.value ?? "";
2622
- input.dataset.onewhackFieldId = field.fieldId;
3149
+ input.dataset.clankerbendFieldId = field.fieldId;
2623
3150
  return input;
2624
3151
  });
2625
3152
  const titleText = String(overlay.title || "").trim();
@@ -2627,14 +3154,22 @@
2627
3154
  const title = titleText && titleText !== firstFieldLabel ? document.createElement("strong") : null;
2628
3155
  if (title) title.textContent = titleText;
2629
3156
  const actions = document.createElement("div");
2630
- actions.className = "onewhack-overlay-actions";
3157
+ actions.className = "clankerbend-overlay-actions";
2631
3158
  for (const action of overlay.actions || []) {
2632
3159
  const button = document.createElement("button");
2633
3160
  button.type = "button";
2634
3161
  button.textContent = action.label || "Apply";
3162
+ button.className = action.type === "overlay.close" ? "clankerbend-overlay-secondary" : "clankerbend-overlay-primary";
2635
3163
  button.addEventListener("click", () => {
2636
- const values = Object.fromEntries([...el.querySelectorAll("[data-onewhack-field-id]")]
2637
- .map((input) => [input.dataset.onewhackFieldId, input.value || ""]));
3164
+ if (action.type === "overlay.close") {
3165
+ enqueueHostEvent({
3166
+ kind: "overlayClose",
3167
+ overlayId: overlay.overlayId
3168
+ });
3169
+ return;
3170
+ }
3171
+ const values = Object.fromEntries([...el.querySelectorAll("[data-clankerbend-field-id]")]
3172
+ .map((input) => [input.dataset.clankerbendFieldId, input.value || ""]));
2638
3173
  enqueueHostEvent({
2639
3174
  kind: "appAction",
2640
3175
  appId: overlay.appId,
@@ -2653,7 +3188,7 @@
2653
3188
  el.classList.add("is-visible");
2654
3189
  positionOverlay(el, overlay);
2655
3190
  const activeField = activeFieldId
2656
- ? [...el.querySelectorAll("[data-onewhack-field-id]")].find((field) => field.dataset.onewhackFieldId === activeFieldId)
3191
+ ? [...el.querySelectorAll("[data-clankerbend-field-id]")].find((field) => field.dataset.clankerbendFieldId === activeFieldId)
2657
3192
  : null;
2658
3193
  const firstField = fields.find((field) => field instanceof HTMLTextAreaElement || field instanceof HTMLInputElement);
2659
3194
  setTimeout(() => focusOverlayField(activeField || firstField), 0);
@@ -2738,7 +3273,7 @@
2738
3273
  }
2739
3274
  el.replaceChildren(...items.map((item) => {
2740
3275
  const chip = document.createElement("div");
2741
- chip.className = "onewhack-context-chip";
3276
+ chip.className = "clankerbend-context-chip";
2742
3277
  const label = document.createElement("span");
2743
3278
  label.textContent = item.label || item.body || item.itemId;
2744
3279
  chip.title = item.body || item.label || item.itemId;
@@ -2817,10 +3352,10 @@
2817
3352
  return true;
2818
3353
  }
2819
3354
 
2820
- function oneWhackSubmitPrompt(submission) {
3355
+ function clankerbendSubmitPrompt(submission) {
2821
3356
  const lines = [
2822
- `[[ONEWHACK_CONTEXT:${submission.submissionId}]]`,
2823
- "Use these OneWhack sticky notes as context. Do not mention the marker."
3357
+ `[[CLANKERBEND_CONTEXT:${submission.submissionId}]]`,
3358
+ "Use these ClankerBend sticky notes as context. Do not mention the marker."
2824
3359
  ];
2825
3360
  for (const [index, item] of submission.items.entries()) {
2826
3361
  const selected = item.range?.quote || item.range?.text || item.label || "";
@@ -2841,16 +3376,16 @@
2841
3376
  }
2842
3377
  }
2843
3378
  lines.push(
2844
- `[[/ONEWHACK_CONTEXT:${submission.submissionId}]]`,
3379
+ `[[/CLANKERBEND_CONTEXT:${submission.submissionId}]]`,
2845
3380
  "",
2846
- submission.userText || "Use the OneWhack context above."
3381
+ submission.userText || "Use the ClankerBend context above."
2847
3382
  );
2848
3383
  return lines.join("\n");
2849
3384
  }
2850
3385
 
2851
3386
  function submittedChipRow(submission) {
2852
3387
  const row = document.createElement("div");
2853
- row.className = "onewhack-submitted-context-row";
3388
+ row.className = "clankerbend-submitted-context-row";
2854
3389
  row.style.cssText = [
2855
3390
  "display:flex",
2856
3391
  "flex-wrap:wrap",
@@ -2859,7 +3394,7 @@
2859
3394
  ].join(";");
2860
3395
  for (const item of submission.items) {
2861
3396
  const chip = document.createElement("span");
2862
- chip.className = "onewhack-context-chip";
3397
+ chip.className = "clankerbend-context-chip";
2863
3398
  chip.textContent = item.label || item.body || item.itemId;
2864
3399
  chip.title = item.body || item.label || item.itemId;
2865
3400
  row.appendChild(chip);
@@ -2870,15 +3405,15 @@
2870
3405
  function maskSubmittedComposerContexts() {
2871
3406
  if (!state.submittedComposerSubmissions.length) return;
2872
3407
  for (const submission of state.submittedComposerSubmissions) {
2873
- const marker = `[[ONEWHACK_CONTEXT:${submission.submissionId}]]`;
3408
+ const marker = `[[CLANKERBEND_CONTEXT:${submission.submissionId}]]`;
2874
3409
  const candidates = Array.from(document.querySelectorAll("main *, [role='main'] *, body *"))
2875
3410
  .filter((el) => el instanceof HTMLElement && !isHostUiElement(el))
2876
3411
  .filter((el) => (el.innerText || el.textContent || "").includes(marker))
2877
3412
  .filter((el) => !Array.from(el.children || []).some((child) => (child.innerText || child.textContent || "").includes(marker)))
2878
3413
  .sort((a, b) => (a.textContent || "").length - (b.textContent || "").length);
2879
3414
  const target = candidates[0];
2880
- if (!target || target.dataset.onewhackMaskedSubmission === submission.submissionId) continue;
2881
- target.dataset.onewhackMaskedSubmission = submission.submissionId;
3415
+ if (!target || target.dataset.clankerbendMaskedSubmission === submission.submissionId) continue;
3416
+ target.dataset.clankerbendMaskedSubmission = submission.submissionId;
2882
3417
  const prompt = document.createElement("span");
2883
3418
  prompt.textContent = submission.userText || "";
2884
3419
  target.replaceChildren(submittedChipRow(submission), prompt);
@@ -2904,7 +3439,7 @@
2904
3439
  items: items.map((item) => ({ ...item })),
2905
3440
  submittedAt: Date.now()
2906
3441
  };
2907
- const nextText = oneWhackSubmitPrompt(submission);
3442
+ const nextText = clankerbendSubmitPrompt(submission);
2908
3443
  if (!setComposerText(input, nextText)) return false;
2909
3444
  state.pendingComposerSubmission = submission;
2910
3445
  state.submittedComposerSubmissions = [...state.submittedComposerSubmissions, submission].slice(-8);
@@ -2941,22 +3476,22 @@
2941
3476
  }
2942
3477
 
2943
3478
  function installComposerSubmitInterceptor() {
2944
- if (window.__oneWhackComposerSubmitInterceptorInstalled) return;
2945
- window.__oneWhackComposerSubmitInterceptorInstalled = true;
2946
- window.__oneWhackComposerSubmitClickHandler = (event) => {
3479
+ if (window.__clankerbendComposerSubmitInterceptorInstalled) return;
3480
+ window.__clankerbendComposerSubmitInterceptorInstalled = true;
3481
+ window.__clankerbendComposerSubmitClickHandler = (event) => {
2947
3482
  if (isComposerSubmitButton(event.target)) prepareComposerContextForSubmit();
2948
3483
  };
2949
- window.__oneWhackComposerSubmitKeyHandler = (event) => {
3484
+ window.__clankerbendComposerSubmitKeyHandler = (event) => {
2950
3485
  if (event.key !== "Enter" || event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return;
2951
3486
  const input = composerInputElement();
2952
3487
  if (input && (event.target === input || input.contains?.(event.target))) prepareComposerContextForSubmit();
2953
3488
  };
2954
- document.addEventListener("click", window.__oneWhackComposerSubmitClickHandler, true);
2955
- document.addEventListener("keydown", window.__oneWhackComposerSubmitKeyHandler, true);
3489
+ document.addEventListener("click", window.__clankerbendComposerSubmitClickHandler, true);
3490
+ document.addEventListener("keydown", window.__clankerbendComposerSubmitKeyHandler, true);
2956
3491
  }
2957
3492
 
2958
- function oneWhackAppUrl() {
2959
- return String(oneWhackRuntime().getEntryUrl(VIM_NAV_APP_ID) || "").trim();
3493
+ function clankerbendAppUrl() {
3494
+ return String(clankerbendRuntime().getEntryUrl(VIM_NAV_APP_ID) || "").trim();
2960
3495
  }
2961
3496
 
2962
3497
  async function ensureNativeBrowserPanel() {
@@ -3010,8 +3545,8 @@
3010
3545
  }
3011
3546
 
3012
3547
  async function doOpenPanel() {
3013
- const serverUrl = oneWhackAppUrl().replace(/\/$/, "");
3014
- if (!serverUrl) return { ok: false, error: "OneWhack host URL missing" };
3548
+ const serverUrl = clankerbendAppUrl().replace(/\/$/, "");
3549
+ if (!serverUrl) return { ok: false, error: "ClankerBend host URL missing" };
3015
3550
  const panelUrl = `${serverUrl}/`;
3016
3551
  const webviews = () => Array.from(document.querySelectorAll("webview, iframe"));
3017
3552
  const normalizedSrc = (el) => String(el.getAttribute("src") || "")
@@ -3076,7 +3611,7 @@
3076
3611
  : null;
3077
3612
  };
3078
3613
  const installPanelIframe = async () => {
3079
- const existing = document.getElementById("onewhack-vim-nav-side-panel-frame");
3614
+ const existing = document.getElementById("clankerbend-vim-nav-side-panel-frame");
3080
3615
  if (existing) {
3081
3616
  existing.setAttribute("src", panelUrl);
3082
3617
  await delay(300);
@@ -3109,10 +3644,10 @@
3109
3644
  if (!input && !/new tab|start browsing|enter a url|browser/i.test(panelText)) return null;
3110
3645
  if (getComputedStyle(panel).position === "static") panel.style.position = "relative";
3111
3646
  const frame = document.createElement("iframe");
3112
- frame.id = "onewhack-vim-nav-side-panel-frame";
3647
+ frame.id = "clankerbend-vim-nav-side-panel-frame";
3113
3648
  frame.src = panelUrl;
3114
- frame.title = "Vim Nav";
3115
- frame.setAttribute("data-onewhack-panel-frame", VIM_NAV_APP_ID);
3649
+ frame.title = "VimNav";
3650
+ frame.setAttribute("data-clankerbend-panel-frame", VIM_NAV_APP_ID);
3116
3651
  frame.style.cssText = [
3117
3652
  "position:absolute",
3118
3653
  "left:0",
@@ -3154,12 +3689,12 @@
3154
3689
  if (!browserUrlInput() && !localServerButton()) {
3155
3690
  return { ok: false, error: "native Browser panel controls not found", debug: panelDebug({ stage: "no-controls" }) };
3156
3691
  }
3157
- return { ok: false, error: "native Browser panel did not navigate to Vim Nav", debug: panelDebug({ stage: "not-navigated" }) };
3692
+ return { ok: false, error: "native Browser panel did not navigate to VimNav", debug: panelDebug({ stage: "not-navigated" }) };
3158
3693
  }
3159
3694
 
3160
3695
  function shouldIgnoreKey(event) {
3696
+ if (effectiveCommandOption(event)) return false;
3161
3697
  if (event.metaKey) return true;
3162
- if (effectiveCtrlAlt(event)) return false;
3163
3698
  if (state.vimMode) return false;
3164
3699
  if (event.defaultPrevented) return true;
3165
3700
  if (event.ctrlKey || event.altKey) return true;
@@ -3170,25 +3705,25 @@
3170
3705
  }
3171
3706
 
3172
3707
  function isModifierOnlyKey(event) {
3173
- return event.key === "Control" ||
3708
+ return event.key === "Meta" ||
3174
3709
  event.key === "Alt" ||
3175
- event.code === "ControlLeft" ||
3176
- event.code === "ControlRight" ||
3710
+ event.code === "MetaLeft" ||
3711
+ event.code === "MetaRight" ||
3177
3712
  event.code === "AltLeft" ||
3178
3713
  event.code === "AltRight";
3179
3714
  }
3180
3715
 
3181
3716
  function updateModifierState(event, isDown) {
3182
- if (event.key === "Control" || event.code === "ControlLeft" || event.code === "ControlRight") {
3183
- state.ctrlDown = Boolean(isDown);
3717
+ if (event.key === "Meta" || event.code === "MetaLeft" || event.code === "MetaRight") {
3718
+ state.metaDown = Boolean(isDown);
3184
3719
  }
3185
3720
  if (event.key === "Alt" || event.code === "AltLeft" || event.code === "AltRight") {
3186
3721
  state.altDown = Boolean(isDown);
3187
3722
  }
3188
3723
  }
3189
3724
 
3190
- function effectiveCtrlAlt(event) {
3191
- return Boolean((event.ctrlKey && event.altKey) || (state.ctrlDown && state.altDown));
3725
+ function effectiveCommandOption(event) {
3726
+ return Boolean((event.metaKey && event.altKey) || (state.metaDown && state.altDown));
3192
3727
  }
3193
3728
 
3194
3729
  function digitFromKeyEvent(event) {
@@ -3228,12 +3763,44 @@
3228
3763
  return null;
3229
3764
  });
3230
3765
  };
3231
- if (effectiveCtrlAlt(event)) {
3766
+ const runRelativeCommand = (name, delta, commandForDelta) => {
3767
+ if (state.relativeMoveInFlight) {
3768
+ state.pendingRelativeDelta = Math.max(-1, Math.min(1, state.pendingRelativeDelta + Math.sign(delta)));
3769
+ state.lastCommandResult = { name, pending: true, pendingRelativeDelta: state.pendingRelativeDelta };
3770
+ ensureModeBadge();
3771
+ return;
3772
+ }
3773
+ const prefixBefore = state.countPrefix;
3774
+ state.relativeMoveInFlight = true;
3775
+ state.commandChain = state.commandChain
3776
+ .catch(() => {})
3777
+ .then(async () => {
3778
+ let step = Math.sign(delta);
3779
+ let result = null;
3780
+ try {
3781
+ while (step) {
3782
+ result = await Promise.resolve().then(() => commandForDelta(step));
3783
+ state.lastCommandResult = { name, prefixBefore, result };
3784
+ ensureModeBadge();
3785
+ step = state.pendingRelativeDelta;
3786
+ state.pendingRelativeDelta = 0;
3787
+ }
3788
+ return result;
3789
+ } catch (err) {
3790
+ state.lastCommandResult = { name, prefixBefore, ok: false, error: err?.message || String(err) };
3791
+ ensureModeBadge();
3792
+ return null;
3793
+ } finally {
3794
+ state.relativeMoveInFlight = false;
3795
+ }
3796
+ });
3797
+ };
3798
+ if (effectiveCommandOption(event)) {
3232
3799
  if (isModifierOnlyKey(event)) {
3233
3800
  event.preventDefault();
3234
3801
  event.stopImmediatePropagation();
3235
- if (!event.repeat && !state.ctrlAltToggleDown) {
3236
- state.ctrlAltToggleDown = true;
3802
+ if (!event.repeat && !state.metaAltToggleDown) {
3803
+ state.metaAltToggleDown = true;
3237
3804
  setVimMode(!state.vimMode);
3238
3805
  }
3239
3806
  return;
@@ -3249,35 +3816,35 @@
3249
3816
  if (code === "KeyJ" || key === "j" || key === "ArrowDown") {
3250
3817
  event.preventDefault();
3251
3818
  event.stopImmediatePropagation();
3252
- runCommand("ctrl-alt-j", () => withVisibleMountedIndex(() => hasAppServerOrder() ? jumpRelative(1) : jumpRelativeMounted(1)));
3819
+ runRelativeCommand("cmd-option-j", 1, (step) => withVisibleMountedIndex(() => jumpRelativeFast(step)));
3253
3820
  } else if (code === "KeyK" || key === "k" || key === "ArrowUp") {
3254
3821
  event.preventDefault();
3255
3822
  event.stopImmediatePropagation();
3256
- runCommand("ctrl-alt-k", () => withVisibleMountedIndex(() => hasAppServerOrder() ? jumpRelative(-1) : jumpRelativeMounted(-1)));
3823
+ runRelativeCommand("cmd-option-k", -1, (step) => withVisibleMountedIndex(() => jumpRelativeFast(step)));
3257
3824
  } else if ((code === "KeyG" && event.shiftKey) || key === "G" || key === "End") {
3258
3825
  event.preventDefault();
3259
3826
  event.stopImmediatePropagation();
3260
- runCommand("ctrl-alt-G", () => afterIndexing(() => jumpToBottom()));
3827
+ runCommand("cmd-option-G", () => afterIndexing(() => jumpToBottom()));
3261
3828
  } else if (code === "KeyG" || key === "g" || key === "Home") {
3262
3829
  event.preventDefault();
3263
3830
  event.stopImmediatePropagation();
3264
- runCommand("ctrl-alt-g", () => afterIndexing(() => jumpToIndex(0, { block: "start", behavior: "auto" })));
3831
+ runCommand("cmd-option-g", () => afterIndexing(() => jumpToIndex(0, { block: "start", behavior: "auto" })));
3265
3832
  } else if (key === "[" || (code === "BracketLeft" && !event.shiftKey)) {
3266
3833
  event.preventDefault();
3267
3834
  event.stopImmediatePropagation();
3268
- runCommand("ctrl-alt-[", () => alignCurrent("start"));
3835
+ runCommand("cmd-option-[", () => alignCurrent("start"));
3269
3836
  } else if (key === "]" || (code === "BracketRight" && !event.shiftKey)) {
3270
3837
  event.preventDefault();
3271
3838
  event.stopImmediatePropagation();
3272
- runCommand("ctrl-alt-]", () => alignCurrent("end"));
3839
+ runCommand("cmd-option-]", () => alignCurrent("end"));
3273
3840
  } else if (key === "{" || (code === "BracketLeft" && event.shiftKey) || key === "PageUp") {
3274
3841
  event.preventDefault();
3275
3842
  event.stopImmediatePropagation();
3276
- runCommand("ctrl-alt-{", () => jumpRole("user", -1));
3843
+ runCommand("cmd-option-{", () => jumpRole("user", -1));
3277
3844
  } else if (key === "}" || (code === "BracketRight" && event.shiftKey) || key === "PageDown") {
3278
3845
  event.preventDefault();
3279
3846
  event.stopImmediatePropagation();
3280
- runCommand("ctrl-alt-}", () => jumpRole("user", 1));
3847
+ runCommand("cmd-option-}", () => jumpRole("user", 1));
3281
3848
  } else if (key === "?" || (code === "Slash" && event.shiftKey)) {
3282
3849
  event.preventDefault();
3283
3850
  event.stopImmediatePropagation();
@@ -3349,12 +3916,12 @@
3349
3916
  event.preventDefault();
3350
3917
  state.countPrefix = "";
3351
3918
  ensureModeBadge();
3352
- runCommand("j", () => withVisibleMountedIndex(() => hasAppServerOrder() ? jumpRelative(1) : jumpRelativeMounted(1)));
3919
+ runRelativeCommand("j", 1, (step) => withVisibleMountedIndex(() => jumpRelativeFast(step)));
3353
3920
  } else if (code === "KeyK" || key === "k") {
3354
3921
  event.preventDefault();
3355
3922
  state.countPrefix = "";
3356
3923
  ensureModeBadge();
3357
- runCommand("k", () => withVisibleMountedIndex(() => hasAppServerOrder() ? jumpRelative(-1) : jumpRelativeMounted(-1)));
3924
+ runRelativeCommand("k", -1, (step) => withVisibleMountedIndex(() => jumpRelativeFast(step)));
3358
3925
  } else if ((code === "KeyG" && event.shiftKey) || key === "G") {
3359
3926
  event.preventDefault();
3360
3927
  const prefix = state.countPrefix;
@@ -3391,10 +3958,10 @@
3391
3958
 
3392
3959
  function handleKeyUp(event) {
3393
3960
  updateModifierState(event, false);
3394
- if ((event.key === "Control" || event.key === "Alt" || !event.ctrlKey || !event.altKey) &&
3395
- state.ctrlAltToggleDown &&
3396
- !effectiveCtrlAlt(event)) {
3397
- state.ctrlAltToggleDown = false;
3961
+ if ((event.key === "Meta" || event.key === "Alt" || !event.metaKey || !event.altKey) &&
3962
+ state.metaAltToggleDown &&
3963
+ !effectiveCommandOption(event)) {
3964
+ state.metaAltToggleDown = false;
3398
3965
  }
3399
3966
  }
3400
3967
 
@@ -3409,9 +3976,9 @@
3409
3976
  }
3410
3977
 
3411
3978
  function resetModifierState() {
3412
- state.ctrlDown = false;
3979
+ state.metaDown = false;
3413
3980
  state.altDown = false;
3414
- state.ctrlAltToggleDown = false;
3981
+ state.metaAltToggleDown = false;
3415
3982
  }
3416
3983
 
3417
3984
  function snapshot() {
@@ -3464,10 +4031,10 @@
3464
4031
  document.removeEventListener("keydown", window.__codexVimNavKeyHandler, true);
3465
4032
  window.removeEventListener("keyup", window.__codexVimNavKeyUpHandler, true);
3466
4033
  document.removeEventListener("keyup", window.__codexVimNavKeyUpHandler, true);
3467
- document.removeEventListener("mouseup", window.__oneWhackTextSelectionHandler, true);
4034
+ document.removeEventListener("mouseup", window.__clankerbendTextSelectionHandler, true);
3468
4035
  window.__codexVimNavKeyHandler = handleKey;
3469
4036
  window.__codexVimNavKeyUpHandler = handleKeyUp;
3470
- window.__oneWhackTextSelectionHandler = handlePossibleTextSelection;
4037
+ window.__clankerbendTextSelectionHandler = handlePossibleTextSelection;
3471
4038
  window.addEventListener("keydown", handleKey, true);
3472
4039
  document.addEventListener("keydown", handleKey, true);
3473
4040
  window.addEventListener("keyup", handleKeyUp, true);
@@ -3478,6 +4045,7 @@
3478
4045
  const mutationObserver = new MutationObserver(() => {
3479
4046
  state.version += 1;
3480
4047
  scheduleAnnotationRefresh();
4048
+ renderCodexAccountSwitcher();
3481
4049
  maskSubmittedComposerContexts();
3482
4050
  });
3483
4051
  window.addEventListener("resize", resizeHandler);
@@ -3489,14 +4057,15 @@
3489
4057
  window.removeEventListener("keyup", handleKeyUp, true);
3490
4058
  document.removeEventListener("keyup", handleKeyUp, true);
3491
4059
  document.removeEventListener("mouseup", handlePossibleTextSelection, true);
3492
- document.removeEventListener("click", window.__oneWhackComposerSubmitClickHandler, true);
3493
- document.removeEventListener("keydown", window.__oneWhackComposerSubmitKeyHandler, true);
4060
+ document.removeEventListener("click", window.__clankerbendComposerSubmitClickHandler, true);
4061
+ document.removeEventListener("keydown", window.__clankerbendComposerSubmitKeyHandler, true);
4062
+ document.getElementById(ACCOUNT_SWITCHER_ID)?.remove();
3494
4063
  window.removeEventListener("resize", resizeHandler);
3495
4064
  window.removeEventListener("blur", blurHandler);
3496
4065
  mutationObserver.disconnect();
3497
4066
  if (annotationRefreshTimer) clearTimeout(annotationRefreshTimer);
3498
4067
  annotationRefreshTimer = null;
3499
- window.__oneWhackComposerSubmitInterceptorInstalled = false;
4068
+ window.__clankerbendComposerSubmitInterceptorInstalled = false;
3500
4069
  };
3501
4070
 
3502
4071
  const bridge = {
@@ -3508,18 +4077,18 @@
3508
4077
  openPanel,
3509
4078
  scrollToAnchor,
3510
4079
  highlightAnchor,
3511
- highlightRange: (range, options) => oneWhackRuntime().highlightRange
3512
- ? oneWhackRuntime().highlightRange(range, options)
4080
+ highlightRange: (range, options) => clankerbendRuntime().highlightRange
4081
+ ? clankerbendRuntime().highlightRange(range, options)
3513
4082
  : highlightAnchor(range?.anchorId, options),
3514
4083
  setAnnotations,
3515
4084
  setTranscriptOrder,
3516
4085
  primeAnchorOrder,
3517
4086
  jumpToIndex,
3518
- jumpRelative,
4087
+ jumpRelativeFast,
3519
4088
  __testUpdateTextSelectionFromDom: updateTextSelectionFromDom
3520
4089
  };
3521
4090
 
3522
- oneWhackRuntime().registerApp({
4091
+ clankerbendRuntime().registerApp({
3523
4092
  appId: VIM_NAV_APP_ID,
3524
4093
  capabilities: {
3525
4094
  transcriptRead: true,