Skip to content

Land remaining unmerged branch work: single-instance follow-ups and browser extensions - #323

Merged
arzafran merged 10 commits into
mainfrom
chore/cherry-pick-unmerged-2026-08-28
Aug 28, 2026
Merged

Land remaining unmerged branch work: single-instance follow-ups and browser extensions#323
arzafran merged 10 commits into
mainfrom
chore/cherry-pick-unmerged-2026-08-28

Conversation

@arzafran

Copy link
Copy Markdown
Member

What this does

Collects the work that was still sitting on old branches so those branches can be deleted.

  • Single-instance arbitration follow-ups (six commits that continued fix: arbitrate simultaneous app launches #318 after it was merged): bounded shutdown arbitration state, consent required before a duplicate is force-closed, exact process identities and a durable per-target responsive state so a healthy instance cannot be force-closed by an overlapping contender.
  • Browser web extensions proof of concept (two commits from feat/browser-extensions): load unpacked extensions in the built-in browser and expose browser panels to them as tabs and windows.

Not included, on purpose

  • reland/ghostty-occluded-render (Aug 5): pins a stale ghostty submodule SHA; needs a fresh reland against the current fork, not a cherry-pick.
  • fix/provider-usage-handshake-sizing: draft that fix: make provider usage controls compact and current #320 rewrote; every pick conflicts with the shipped version.
  • browser-red-proof-prereqs, fix/escrow-orphan-reconciliation-307, fix/safe-area-loop, followup/sidebar-drag-snapshots-2026-08-26: already on main by content.
  • tmp/rendering-fixes-integration: a single Aug 4 merge commit with nothing discrete to pick.

Review order

  1. Sources/AppDelegate.swift single-instance section and programaTests/AppDelegateShortcutRoutingTests.swift.
  2. Browser extension loading in Sources/Panels/.

Test plan

  • PR CI green
  • Launch two copies at once: one survives, and a responsive instance is never force-closed without consent

arzafran and others added 10 commits August 27, 2026 11:34
…concept)

The browser can now run Chrome-style Web Extensions (manifest v2/v3)
through WebKit's WKWebExtension API (macOS 15.4+; on older macOS the
browser simply has no extensions). Extensions are read from
~/.config/programa/extensions/ — one unpacked directory or .zip per
extension — and their content scripts inject into main panels and popups
alike via the shared configuration path.

Deliberately narrow scope for this slice:
- Every requested permission and host pattern is granted up front; the
  extensions directory is trusted input until a consent UI exists.
  Grant expiration dates must be distantFuture — the dictionary values
  are expirations, and a "now" timestamp silently expires the grant
  before first use.
- Extension storage persists under a fixed controller identifier.
- No toolbar/popup/options UI and no WKWebExtensionTab/Window adapters
  yet, so extension APIs that enumerate tabs see none. Those are the
  next slices toward running 1Password.

Verified: a manifest v3 test extension's content script injects into
https pages in both a browser panel and a window.open popup.

(cherry picked from commit ead7d4f)
Extensions inject dynamically through tabs.query + scripting.executeScript
(1Password's autofill works exactly this way), and both APIs resolve
against WKWebExtensionTab/Window adapters. Without them an extension sees
a browser with zero tabs and can inject into nothing.

- BrowserExtensionTabAdapter wraps a BrowserPanel; a single
  BrowserExtensionWindowAdapter presents the app as one flat window whose
  tabs are all live browser panels across workspaces. Popup windows are
  not represented yet (their content scripts still run).
- Lifecycle: register on bindWebView, unregister in BrowserPanel.close,
  activate via the applyTabSelection funnel (didFocusPane lands there
  too, so one hook covers pane focus and tab selection).
- The controller delegate's openWindowsFor answer is what initially
  populates each context's openWindows/openTabs — without a delegate the
  world starts empty and tabs.query returns [] forever, regardless of
  didOpenTab notifications. Cost a debug cycle; documented.
- Both WebKit protocols are fully @optional and a near-miss Swift
  signature is silently ignored; the "nearly matches" compiler warning
  and the tabs-poc runtime probe (PR #284) are the safety nets. The
  warning already caught one (indexInWindow wants Int, not UInt).

Verified: a manifest v3 test extension's background worker enumerates
all tabs and stamps every page via scripting.executeScript. 1Password
still needs its action popup UI (extension sign-in) before it decorates
fields — next slice.

(cherry picked from commit 94f9f58)
@arzafran
arzafran merged commit 8b78864 into main Aug 28, 2026
8 checks passed
@arzafran
arzafran deleted the chore/cherry-pick-unmerged-2026-08-28 branch August 28, 2026 16:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants