Skip to content

Phase 0: ElectroBun scaffold with tray app and Swift FFI bridge#1

Merged
GeiserX merged 5 commits intomainfrom
phase-0/electrobun-scaffold
Apr 14, 2026
Merged

Phase 0: ElectroBun scaffold with tray app and Swift FFI bridge#1
GeiserX merged 5 commits intomainfrom
phase-0/electrobun-scaffold

Conversation

@GeiserX
Copy link
Copy Markdown
Owner

@GeiserX GeiserX commented Apr 13, 2026

Summary

  • ElectroBun project structure: electrobun.config.ts, package.json, tsconfig.json with menu bar app configuration (exitOnLastWindowClosed: false)
  • Menu bar tray app: System tray with capture toggle, per-provider enable/disable, status display, and quit — all wired up via Tray class from electrobun/bun
  • Swift FFI bridge: Universal dylib (arm64 + x86_64) with C-compatible exports (@_cdecl) for version, system info, firewall/keychain capability checks. Bun FFI bindings in TypeScript via bun:ffi
  • Main window view: Dark theme status dashboard showing capture status, proxy port, trace count, provider list, and native bridge status
  • Shared types: Provider definitions (8 providers), app state, tray action constants
  • GitHub Actions CI: Two-stage pipeline — build Swift dylib, then build ElectroBun app
  • Placeholder icons: Tray template icon (22x22) and app iconset (16-512px)

Architecture

src/bun/index.ts          → Main process (tray, lifecycle, state)
src/bun/native-bridge.ts  → Bun FFI wrapper for Swift dylib
src/shared/types.ts       → Provider config, app state types
src/views/mainview/       → Status dashboard (HTML/CSS/TS)
native/AgentTapBridge.swift → C-compatible Swift bridge
native/Makefile           → Universal dylib build (arm64 + x86_64)

Phase 0 Checklist (from ROADMAP.md)

  • Initialize ElectroBun project structure
  • Configure TypeScript build pipeline
  • Create macOS menu bar app shell (system tray icon, basic dropdown)
  • Set up Swift FFI bridge skeleton for macOS-specific APIs
    • Compile Swift to .dylib with C-compatible exports
    • Create Bun FFI bindings (type-safe TypeScript wrappers)
    • Verify round-trip: TypeScript → FFI → Swift → return value
  • Basic app lifecycle: launch, show tray icon, quit
  • App icon and branding (dark theme)
  • CI pipeline: build universal macOS binary (arm64 + x86_64)

Test plan

  • cd native && make universal compiles without errors
  • nm -gU native/build/libAgentTapBridge.dylib | grep agenttap shows all 5 symbols
  • lipo -info native/build/libAgentTapBridge.dylib shows both arm64 and x86_64
  • bun install && bun run dev launches the tray app (requires ElectroBun)
  • GitHub Actions build workflow passes on macOS-14 runner

Summary by CodeRabbit

  • New Features

    • System tray menu with capture start/stop controls and provider toggles
    • Main application window displaying capture status, system information, proxy port, and provider list
  • Chores

    • Added build configuration for native components and application
    • Added TypeScript and Electron application configuration files

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 13, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Introduces the foundational build and application infrastructure for AgentTap: a macOS application combining a system tray interface (via electrobun/Bun), a Swift native bridge exporting system information and capability checks, a build pipeline compiling architecture-specific dylibs, and a web-based main view. Includes configuration for TypeScript, npm/Bun package management, and GitHub Actions CI.

Changes

Cohort / File(s) Summary
Build & CI Configuration
.github/workflows/build.yml, native/Makefile, package.json, tsconfig.json, electrobun.config.ts
Defines GitHub Actions pipeline with dual-job chain (native build → app build), Makefile for universal macOS dylib compilation, npm scripts for dev/build/clean workflows, TypeScript compiler options, and electrobun app configuration including tray UI, asset handling, and platform-specific macOS/Linux build settings.
Native Bridge (Swift)
native/AgentTapBridge.swift
Swift FFI exports for version, system info retrieval (CPU/memory/uptime via ProcessInfo), firewall/keychain capability stubs, and string memory deallocation. Uses @_cdecl annotations and JSON serialization for native-to-C interop.
JavaScript FFI Wrapper
src/bun/native-bridge.ts
Bun FFI wrapper loading platform-specific libAgentTapBridge.dylib, with fallback paths for bundled and dev builds. Typed symbol map, graceful degradation when library unavailable, C string decoding, and memory cleanup via native free function.
Application Entrypoint
src/bun/index.ts
System tray application initializing capture state, provider list, and trace counter. Implements tray menu with toggle capture, per-provider enable/disable, trace viewer placeholder, and quit actions. Async startup with native bridge instantiation and error handling.
Shared Types & Constants
src/shared/types.ts
Exports Provider, CaptureStatus, AppState, and TrayActions interfaces/types; defines DEFAULT_PROVIDERS list with Anthropic/OpenAI enabled and others disabled; establishes domain allowlists per provider.
UI — Main View
src/views/mainview/index.html, src/views/mainview/index.css, src/views/mainview/index.ts
Static HTML structure with placeholders for dynamic status/counts/provider list; dark-themed CSS design system using CSS variables, flex/grid layouts, and status indicator styling; DOM population script rendering provider list with enabled/disabled badges and provider count aggregation.
Project Metadata
.gitignore
Excludes native/build/ directory and notes bun.lockb inclusion policy for reproducible builds.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the main changes: introducing the ElectroBun project scaffold with a tray app and Swift FFI bridge, which aligns with the substantial Phase 0 initialization work across multiple files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch phase-0/electrobun-scaffold

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (12)
electrobun.config.ts (1)

36-41: Gate packaging security flags by environment.

Unconditionally disabling ASAR/signing/notarization is risky for release builds. Keep this dev-only via env-based config.

Proposed fix
 import type { ElectrobunConfig } from "electrobun";
 
+const isRelease = process.env.BUILD_CHANNEL === "release";
+
 export default {
@@
-    useAsar: false,
+    useAsar: isRelease,
@@
     mac: {
-      codesign: false,
-      notarize: false,
+      codesign: isRelease,
+      notarize: isRelease,
       bundleCEF: false,
       defaultRenderer: "native",
       icons: "icon.iconset",
     },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@electrobun.config.ts` around lines 36 - 41, The config currently sets
useAsar: false and mac.codesign/notarize/bundleCEF all false unconditionally;
change these to be environment-gated so they remain disabled only in
development. Update electrobun config where useAsar and the mac object
(codesign, notarize, bundleCEF) are defined to read an env check (e.g. NODE_ENV
=== 'production' or a specific ELECTROBUN_DISABLE_SIGNING flag) and set useAsar
= true and mac.codesign/mac.notarize/mac.bundleCEF = true for production,
otherwise false for dev; ensure the logic is centralized so release builds get
ASAR and signing/notarization enabled while local/dev builds keep them off.
package.json (1)

13-16: Make app build depend on native bridge build.

build currently skips build:native. This can produce app artifacts without the expected dylib state.

Proposed fix
-    "build": "bunx electrobun build",
+    "build": "bun run build:native && bunx electrobun build",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` around lines 13 - 16, The build script currently runs
electrobun build without building the native bridge first; update the
package.json "build" script so it invokes the "build:native" script before
running "build" (i.e., run "build:native" then the existing electrobun build
command) so artifacts include the native dylib; ensure you reference the
existing script names "build" and "build:native" when making the change and
preserve the "build:stable" and "clean" scripts behavior.
src/views/mainview/index.ts (1)

8-16: Use safe DOM APIs instead of innerHTML for better maintainability.

While the provider data is currently hardcoded, using innerHTML with string interpolation is a code anti-pattern. Refactor to use textContent and append() for clarity and future-proofing.

Proposed fix
   for (const provider of DEFAULT_PROVIDERS) {
     const li = document.createElement("li");
-    li.innerHTML = `
-      <div>
-        <strong>${provider.name}</strong>
-        <span class="domain">${provider.domains[0]}</span>
-      </div>
-      <span class="badge ${provider.enabled ? "on" : "off"}">
-        ${provider.enabled ? "ON" : "OFF"}
-      </span>
-    `;
+    const left = document.createElement("div");
+    const name = document.createElement("strong");
+    name.textContent = provider.name;
+    const domain = document.createElement("span");
+    domain.className = "domain";
+    domain.textContent = provider.domains[0] ?? "—";
+    left.append(name, domain);
+
+    const badge = document.createElement("span");
+    badge.className = `badge ${provider.enabled ? "on" : "off"}`;
+    badge.textContent = provider.enabled ? "ON" : "OFF";
+
+    li.append(left, badge);
     providerList.appendChild(li);
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/views/mainview/index.ts` around lines 8 - 16, The current code sets
li.innerHTML using string interpolation which risks injection and is hard to
maintain; replace the innerHTML block by creating and appending DOM nodes
explicitly: create a container div, create a strong element and set its
textContent to provider.name, create a span.domain and set its textContent to
provider.domains[0], append those to the div, create a badge span, set its
className to `badge ${provider.enabled ? "on" : "off"}` and its textContent to
`ON` or `OFF` based on provider.enabled, then clear li (e.g., li.textContent =
"" or remove children) and append the new elements to li (references: the local
variable li, and the provider properties provider.name, provider.domains,
provider.enabled).
src/bun/native-bridge.ts (4)

1-1: Unused import: ptr.

ptr is imported but never used in this file.

Remove unused import
-import { dlopen, FFIType, ptr, suffix } from "bun:ffi";
+import { dlopen, FFIType, suffix } from "bun:ffi";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bun/native-bridge.ts` at line 1, The import list in
src/bun/native-bridge.ts includes an unused symbol `ptr`; remove `ptr` from the
import statement (the line importing dlopen, FFIType, ptr, suffix) so it only
imports the actually used symbols (dlopen, FFIType, suffix) to eliminate the
unused import warning.

56-61: Dead code: readCString is never called.

This method is defined but unused. Both getVersion and getSystemInfo use Bun.mmap directly instead.

Remove or use it
-  private readCString(pointer: ReturnType<typeof ptr>): string {
-    if (!pointer) return "";
-    const buf = Buffer.from(pointer as unknown as ArrayBuffer);
-    const str = new TextDecoder().decode(buf);
-    return str;
-  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bun/native-bridge.ts` around lines 56 - 61, The private helper
readCString is dead code and should be removed or actually used; to fix, delete
the readCString(pointer) method from the class in native-bridge.ts (removing its
Buffer/TextDecoder logic) and ensure no other code references it, or
alternatively refactor getVersion and getSystemInfo to call readCString with the
pointer returned by Bun.mmap instead of performing their own decoding—update
references to use the function name readCString (or remove it entirely) and run
tests/TS build to confirm no unused symbol errors remain.

38-43: Redundant candidate path on macOS.

On macOS, suffix is "dylib", so lines 39 and 40 produce identical paths. The redundancy is harmless but unnecessary.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bun/native-bridge.ts` around lines 38 - 43, The candidates array contains
a redundant macOS path because suffix is "dylib", so the second entry duplicates
the first; update the construction of candidates (the candidates constant that
uses join and import.meta.dir with suffix) to avoid adding duplicate
"libAgentTapBridge.dylib" — either remove the explicit duplicate entry or build
the array conditionally (e.g., only push the literal "libAgentTapBridge.dylib"
when suffix !== "dylib" or dedupe the array after creation) so the list contains
unique paths.

68-76: Unused variable and hardcoded buffer length.

Line 68 creates view but never uses it. The 256-byte mmap length is arbitrary; if native returns longer strings, they'll be truncated to 64 chars (fallback on line 74).

Remove unused variable
     // Read null-terminated C string from pointer
-    const view = new DataView(new ArrayBuffer(0));
     const cStr = new Uint8Array(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bun/native-bridge.ts` around lines 68 - 76, Remove the unused local
"view" and stop using the hardcoded mmap length; instead query the native for
the returned string length (e.g., call a length helper like
this.lib.symbols.agenttap_string_length(result) or an equivalent native API) and
pass that length to Bun.mmap when creating cStr, then compute nullIdx and slice
with cStr.subarray(0, nullIdx >= 0 ? nullIdx : cStr.length) (do not fallback to
64), and keep calling this.lib.symbols.agenttap_free_string(result) afterwards;
update the null check to use >= 0 so an empty string is handled correctly.
src/shared/types.ts (1)

10-19: Consider marking DEFAULT_PROVIDERS as readonly.

The array is spread-copied in src/bun/index.ts, but marking it readonly prevents accidental direct mutation elsewhere.

Proposed change
-export const DEFAULT_PROVIDERS: Provider[] = [
+export const DEFAULT_PROVIDERS: readonly Provider[] = [
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/types.ts` around lines 10 - 19, Mark DEFAULT_PROVIDERS as readonly
to prevent accidental mutation: change its type to a readonly tuple/array (e.g.,
ReadonlyArray<Provider> or readonly Provider[]) and update the declaration of
DEFAULT_PROVIDERS accordingly so callers (like the spread in index.ts) still can
copy it but cannot mutate the original; ensure the symbol DEFAULT_PROVIDERS and
its export remain unchanged except for the readonly type annotation.
src/bun/index.ts (2)

126-129: Abrupt exit without cleanup.

process.exit(0) terminates immediately. If future phases add resources (open files, network connections, the NativeBridge), they won't be cleaned up gracefully.

Fine for Phase 0 scaffold; flag for later.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bun/index.ts` around lines 126 - 129, When handling TrayActions.QUIT,
avoid calling process.exit(0) directly; instead implement and invoke a graceful
shutdown sequence (e.g., shutdownAgent / cleanupResources) that closes/flushes
the NativeBridge, network connections, and open files, removes listeners, and
then exits; replace the direct process.exit(0) in the if (action ===
TrayActions.QUIT) block with a call to that shutdown function and only call
process.exit(0) after cleanup completes.

96-106: Toggle doesn't handle "error" state.

If captureStatus is "error", toggling sets it to "active". This may be intentional (retry behavior), but consider whether error state should require explicit acknowledgment or different handling.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bun/index.ts` around lines 96 - 106, The toggle handler currently treats
an "error" captureStatus the same as "inactive" and immediately starts capture;
change TrayActions.TOGGLE_CAPTURE logic to explicitly handle state.captureStatus
=== "error" by clearing or resetting the error instead of starting capture
(e.g., set state.captureStatus = "inactive", console.log a message about
clearing the error, update tray.setTitle and call buildTrayMenu, and return) so
error requires an explicit retry path rather than automatically activating; keep
existing behavior for "inactive" and "active" branches and reference
TrayActions.TOGGLE_CAPTURE, state.captureStatus, tray.setTitle, and
buildTrayMenu when applying the change.
src/views/mainview/index.html (1)

17-40: Several status elements are not wired to JS updates.

#status-indicator, #capture-status, #traces-count, and #bridge-status are defined but index.ts only updates #provider-list and #providers-count. These will show stale/initial values indefinitely.

If intentional placeholders for future phases, consider adding data-todo attributes or comments to clarify.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/views/mainview/index.html` around lines 17 - 40, The status UI elements
(`#status-indicator`, `#capture-status`, `#traces-count`, `#bridge-status`) are declared
in the template but never updated by the script that currently only updates
`#provider-list` and `#providers-count` in index.ts; update index.ts to select these
DOM nodes and set their values/state when the underlying data changes (e.g.,
update capture state text and indicator class in the capture flow, set
traces-count from the traces metric, and set bridge-status from the native
bridge health check), or if they are intentionally left as placeholders add
data-todo attributes or an inline comment next to each element to indicate they
are pending implementation (refer to the DOM IDs above and the update logic in
index.ts where `#provider-list/`#providers-count are handled).
native/AgentTapBridge.swift (1)

5-9: strdup can return NULL on allocation failure.

If memory allocation fails, strdup returns NULL. The FFI consumer checks for null results, but for completeness, consider a fallback.

Defensive fallback
 `@_cdecl`("agenttap_version")
 public func agenttap_version() -> UnsafeMutablePointer<CChar>? {
     let version = "0.1.0"
-    return strdup(version)
+    return strdup(version) // Returns nil on OOM; handled by caller
 }

Or return optional type explicitly. The TypeScript side already handles null.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/AgentTapBridge.swift` around lines 5 - 9, In agenttap_version, strdup
may return NULL on allocation failure; update the function to check the strdup
return value and handle NULL defensively: if strdup(version) returns nil,
attempt a safe fallback (e.g., try strdup on an empty string or allocate a
minimal C string) and if that also fails, return a clear NULL pointer so the FFI
consumer can detect the failure; ensure the check and fallback are implemented
around the call to strdup in agenttap_version.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/build.yml:
- Around line 54-59: The upload step named "Upload app artifact" currently uses
actions/upload-artifact@v4 with if-no-files-found: warn which allows the
workflow to pass when .build/ is missing; change the if-no-files-found value to
error in that step so the CI fails if the artifact directory is absent, ensuring
missing build outputs cause a failed job.
- Around line 37-40: The CI workflow currently pins Bun via the setup step named
"Install Bun" using oven-sh/setup-bun@v2 with bun-version: latest; change that
bun-version value from "latest" to the specific stable release "1.3.12" to
ensure reproducible builds, i.e., update the bun-version field in the Install
Bun step accordingly.

In @.gitignore:
- Around line 24-25: Remove bun.lockb from .gitignore and add/commit the actual
lockfile to the repo so CI has a deterministic dependency graph; specifically
delete the bun.lockb entry from the .gitignore file and run git add bun.lockb &&
git commit -m "chore: commit bun.lockb lockfile" (ensure the file is tracked so
bun-version: latest uses the committed lockfile in CI).

In `@native/AgentTapBridge.swift`:
- Around line 13-29: The current agenttap_system_info function includes
info.hostName which can leak PII; update agenttap_system_info to stop exposing
raw hostnames by either removing the "hostname" key entirely or replacing it
with a non-identifying value (e.g., "REDACTED" or a deterministic hash).
Concretely, edit agenttap_system_info to (a) drop the "hostname" entry from the
json dictionary OR (b) import CryptoKit and replace info.hostName with a
hashed/trimmed representation (e.g., SHA256 hex and maybe first 8 chars) so the
value is non-identifying, then proceed with JSONSerialization as before; ensure
the code references agenttap_system_info and info.hostName when making the
change.

In `@src/bun/index.ts`:
- Around line 7-12: The AppState initialization uses providers:
[...DEFAULT_PROVIDERS] which only shallow-copies the array and leaves the
provider objects shared with DEFAULT_PROVIDERS; change state initialization to
deep-clone the provider objects (for example map over DEFAULT_PROVIDERS and
create new objects for each provider) so toggling provider.enabled (the code
that mutates provider.enabled) does not mutate the originals in
DEFAULT_PROVIDERS; update the initializer that sets state to create fresh
provider instances (and consider doing the same pattern wherever you
reinitialize defaults) so the app works with independent provider objects.

In `@src/views/mainview/index.css`:
- Around line 180-186: The footer link styles only handle hover; add keyboard
focus styling by defining a :focus-visible rule for the same selector (e.g.,
footer a:focus-visible) so keyboard users see focus; update the footer a CSS
block to include an accessible visible focus indicator (matching or
complementing footer a:hover color and an outline or box-shadow) to ensure links
are perceivable via keyboard navigation.

---

Nitpick comments:
In `@electrobun.config.ts`:
- Around line 36-41: The config currently sets useAsar: false and
mac.codesign/notarize/bundleCEF all false unconditionally; change these to be
environment-gated so they remain disabled only in development. Update electrobun
config where useAsar and the mac object (codesign, notarize, bundleCEF) are
defined to read an env check (e.g. NODE_ENV === 'production' or a specific
ELECTROBUN_DISABLE_SIGNING flag) and set useAsar = true and
mac.codesign/mac.notarize/mac.bundleCEF = true for production, otherwise false
for dev; ensure the logic is centralized so release builds get ASAR and
signing/notarization enabled while local/dev builds keep them off.

In `@native/AgentTapBridge.swift`:
- Around line 5-9: In agenttap_version, strdup may return NULL on allocation
failure; update the function to check the strdup return value and handle NULL
defensively: if strdup(version) returns nil, attempt a safe fallback (e.g., try
strdup on an empty string or allocate a minimal C string) and if that also
fails, return a clear NULL pointer so the FFI consumer can detect the failure;
ensure the check and fallback are implemented around the call to strdup in
agenttap_version.

In `@package.json`:
- Around line 13-16: The build script currently runs electrobun build without
building the native bridge first; update the package.json "build" script so it
invokes the "build:native" script before running "build" (i.e., run
"build:native" then the existing electrobun build command) so artifacts include
the native dylib; ensure you reference the existing script names "build" and
"build:native" when making the change and preserve the "build:stable" and
"clean" scripts behavior.

In `@src/bun/index.ts`:
- Around line 126-129: When handling TrayActions.QUIT, avoid calling
process.exit(0) directly; instead implement and invoke a graceful shutdown
sequence (e.g., shutdownAgent / cleanupResources) that closes/flushes the
NativeBridge, network connections, and open files, removes listeners, and then
exits; replace the direct process.exit(0) in the if (action ===
TrayActions.QUIT) block with a call to that shutdown function and only call
process.exit(0) after cleanup completes.
- Around line 96-106: The toggle handler currently treats an "error"
captureStatus the same as "inactive" and immediately starts capture; change
TrayActions.TOGGLE_CAPTURE logic to explicitly handle state.captureStatus ===
"error" by clearing or resetting the error instead of starting capture (e.g.,
set state.captureStatus = "inactive", console.log a message about clearing the
error, update tray.setTitle and call buildTrayMenu, and return) so error
requires an explicit retry path rather than automatically activating; keep
existing behavior for "inactive" and "active" branches and reference
TrayActions.TOGGLE_CAPTURE, state.captureStatus, tray.setTitle, and
buildTrayMenu when applying the change.

In `@src/bun/native-bridge.ts`:
- Line 1: The import list in src/bun/native-bridge.ts includes an unused symbol
`ptr`; remove `ptr` from the import statement (the line importing dlopen,
FFIType, ptr, suffix) so it only imports the actually used symbols (dlopen,
FFIType, suffix) to eliminate the unused import warning.
- Around line 56-61: The private helper readCString is dead code and should be
removed or actually used; to fix, delete the readCString(pointer) method from
the class in native-bridge.ts (removing its Buffer/TextDecoder logic) and ensure
no other code references it, or alternatively refactor getVersion and
getSystemInfo to call readCString with the pointer returned by Bun.mmap instead
of performing their own decoding—update references to use the function name
readCString (or remove it entirely) and run tests/TS build to confirm no unused
symbol errors remain.
- Around line 38-43: The candidates array contains a redundant macOS path
because suffix is "dylib", so the second entry duplicates the first; update the
construction of candidates (the candidates constant that uses join and
import.meta.dir with suffix) to avoid adding duplicate "libAgentTapBridge.dylib"
— either remove the explicit duplicate entry or build the array conditionally
(e.g., only push the literal "libAgentTapBridge.dylib" when suffix !== "dylib"
or dedupe the array after creation) so the list contains unique paths.
- Around line 68-76: Remove the unused local "view" and stop using the hardcoded
mmap length; instead query the native for the returned string length (e.g., call
a length helper like this.lib.symbols.agenttap_string_length(result) or an
equivalent native API) and pass that length to Bun.mmap when creating cStr, then
compute nullIdx and slice with cStr.subarray(0, nullIdx >= 0 ? nullIdx :
cStr.length) (do not fallback to 64), and keep calling
this.lib.symbols.agenttap_free_string(result) afterwards; update the null check
to use >= 0 so an empty string is handled correctly.

In `@src/shared/types.ts`:
- Around line 10-19: Mark DEFAULT_PROVIDERS as readonly to prevent accidental
mutation: change its type to a readonly tuple/array (e.g.,
ReadonlyArray<Provider> or readonly Provider[]) and update the declaration of
DEFAULT_PROVIDERS accordingly so callers (like the spread in index.ts) still can
copy it but cannot mutate the original; ensure the symbol DEFAULT_PROVIDERS and
its export remain unchanged except for the readonly type annotation.

In `@src/views/mainview/index.html`:
- Around line 17-40: The status UI elements (`#status-indicator`, `#capture-status`,
`#traces-count`, `#bridge-status`) are declared in the template but never updated by
the script that currently only updates `#provider-list` and `#providers-count` in
index.ts; update index.ts to select these DOM nodes and set their values/state
when the underlying data changes (e.g., update capture state text and indicator
class in the capture flow, set traces-count from the traces metric, and set
bridge-status from the native bridge health check), or if they are intentionally
left as placeholders add data-todo attributes or an inline comment next to each
element to indicate they are pending implementation (refer to the DOM IDs above
and the update logic in index.ts where `#provider-list/`#providers-count are
handled).

In `@src/views/mainview/index.ts`:
- Around line 8-16: The current code sets li.innerHTML using string
interpolation which risks injection and is hard to maintain; replace the
innerHTML block by creating and appending DOM nodes explicitly: create a
container div, create a strong element and set its textContent to provider.name,
create a span.domain and set its textContent to provider.domains[0], append
those to the div, create a badge span, set its className to `badge
${provider.enabled ? "on" : "off"}` and its textContent to `ON` or `OFF` based
on provider.enabled, then clear li (e.g., li.textContent = "" or remove
children) and append the new elements to li (references: the local variable li,
and the provider properties provider.name, provider.domains, provider.enabled).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 72736a8d-79c5-4b77-a899-ceb295c66349

📥 Commits

Reviewing files that changed from the base of the PR and between 416f2c6 and 77e19aa.

⛔ Files ignored due to path filters (10)
  • icon.iconset/icon_128x128.png is excluded by !**/*.png
  • icon.iconset/icon_128x128@2x.png is excluded by !**/*.png
  • icon.iconset/icon_16x16.png is excluded by !**/*.png
  • icon.iconset/icon_16x16@2x.png is excluded by !**/*.png
  • icon.iconset/icon_256x256.png is excluded by !**/*.png
  • icon.iconset/icon_256x256@2x.png is excluded by !**/*.png
  • icon.iconset/icon_32x32.png is excluded by !**/*.png
  • icon.iconset/icon_32x32@2x.png is excluded by !**/*.png
  • icon.iconset/icon_512x512.png is excluded by !**/*.png
  • src/views/mainview/assets/tray-icon-template.png is excluded by !**/*.png
📒 Files selected for processing (13)
  • .github/workflows/build.yml
  • .gitignore
  • electrobun.config.ts
  • native/AgentTapBridge.swift
  • native/Makefile
  • package.json
  • src/bun/index.ts
  • src/bun/native-bridge.ts
  • src/shared/types.ts
  • src/views/mainview/index.css
  • src/views/mainview/index.html
  • src/views/mainview/index.ts
  • tsconfig.json

.gitignore Outdated
Comment on lines +24 to +25
# Bun
bun.lockb
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify lockfile is ignored and CI is using floating dependency resolution.
rg -n 'bun\.lockb|bun\.lock' .gitignore
rg -n 'bun install|bun-version:\s*latest' .github/workflows/build.yml package.json

Repository: GeiserX/AgentTap

Length of output: 189


Remove bun.lockb from .gitignore and commit the lockfile.

The lockfile is currently ignored while CI uses bun-version: latest, creating non-deterministic builds. Commit the lockfile to ensure consistent dependency resolution.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.gitignore around lines 24 - 25, Remove bun.lockb from .gitignore and
add/commit the actual lockfile to the repo so CI has a deterministic dependency
graph; specifically delete the bun.lockb entry from the .gitignore file and run
git add bun.lockb && git commit -m "chore: commit bun.lockb lockfile" (ensure
the file is tracked so bun-version: latest uses the committed lockfile in CI).

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/bun/index.ts (1)

87-94: Type assertion lacks runtime validation.

Line 88 casts e.data without checking the shape. If ElectroBun's event structure changes, this fails silently.

Consider a type guard or at least defensive access:

Safer event data access
 tray.on("tray-clicked", (e) => {
-  const { action } = e.data as { id: number; action: string };
+  const data = e.data as Record<string, unknown>;
+  const action = typeof data?.action === "string" ? data.action : "";

   if (action === "" || action === undefined) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bun/index.ts` around lines 87 - 94, The handler registered with
tray.on("tray-clicked") is unsafely asserting e.data's shape; add runtime
validation instead of direct type assertion: in the tray.on callback (and before
using buildTrayMenu), check that e and e.data exist and that e.data has an
'action' property of type string (e.g., typeof e.data === 'object' && 'action'
in e.data && typeof (e.data as any).action === 'string'); if validation fails,
handle gracefully (early return or a safe default) so tray-clicked cannot throw
due to unexpected event shapes. Ensure references to e.data and action in this
handler are guarded accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@native/AgentTapBridge.swift`:
- Around line 32-41: The capability checks are currently over-reporting support;
update the functions agenttap_can_manage_firewall and
agenttap_can_manage_keychain to conservatively return false until proper runtime
checks are implemented: change agenttap_can_manage_firewall to stop relying only
on FileManager.fileExists(atPath:) and return false (or perform a real
privileged-helper verification later), and change agenttap_can_manage_keychain
to return false instead of always true (add TODO comments in both functions to
implement real checks such as verifying the privileged helper/service for pfctl
and performing an actual Security.framework / SecItem access test).

---

Nitpick comments:
In `@src/bun/index.ts`:
- Around line 87-94: The handler registered with tray.on("tray-clicked") is
unsafely asserting e.data's shape; add runtime validation instead of direct type
assertion: in the tray.on callback (and before using buildTrayMenu), check that
e and e.data exist and that e.data has an 'action' property of type string
(e.g., typeof e.data === 'object' && 'action' in e.data && typeof (e.data as
any).action === 'string'); if validation fails, handle gracefully (early return
or a safe default) so tray-clicked cannot throw due to unexpected event shapes.
Ensure references to e.data and action in this handler are guarded accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 317f8df6-be7f-489c-a5c3-876671b3300a

📥 Commits

Reviewing files that changed from the base of the PR and between 4b4aeb0 and 7624e03.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (8)
  • .github/workflows/build.yml
  • .gitignore
  • native/AgentTapBridge.swift
  • package.json
  • src/bun/index.ts
  • src/bun/native-bridge.ts
  • src/shared/types.ts
  • src/views/mainview/index.css
✅ Files skipped from review due to trivial changes (5)
  • .gitignore
  • src/views/mainview/index.css
  • .github/workflows/build.yml
  • package.json
  • src/shared/types.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/bun/native-bridge.ts

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/bun/native-bridge.ts (2)

84-99: Free native strings in finally blocks.

If string decoding throws, agenttap_free_string is skipped and leaks native memory. Wrap decode with try/finally in both methods.

Suggested safety diff
   getVersion(): string {
     if (!this.lib) return "native bridge unavailable";
     const result = this.lib.symbols.agenttap_version();
     if (!result) return "unknown";
-    const str = this.readCString(result as unknown as number);
-    this.lib.symbols.agenttap_free_string(result);
-    return str;
+    try {
+      return this.readCString(result as unknown as number);
+    } finally {
+      this.lib.symbols.agenttap_free_string(result);
+    }
   }

   getSystemInfo(): string {
     if (!this.lib) return "{}";
     const result = this.lib.symbols.agenttap_system_info();
     if (!result) return "{}";
-    const str = this.readCString(result as unknown as number);
-    this.lib.symbols.agenttap_free_string(result);
-    return str;
+    try {
+      return this.readCString(result as unknown as number);
+    } finally {
+      this.lib.symbols.agenttap_free_string(result);
+    }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bun/native-bridge.ts` around lines 84 - 99, Both getVersion and
getSystemInfo can leak native memory if readCString throws because
agenttap_free_string is only called after decoding; modify both methods
(getVersion and getSystemInfo) to store the native pointer (result) and perform
the readCString call inside a try block with agenttap_free_string invoked in a
finally block via this.lib.symbols.agenttap_free_string(result) so the native
string is always freed even on decode errors; keep the same early-return checks
for this.lib and null result.

67-75: Capture final dlopen failure for diagnostics.

The empty catch makes packaging/runtime failures opaque. Keep trying candidates, but retain the last error and emit one warning after exhaustion.

Suggested observability diff
+    let lastError: unknown = null;
     for (const path of candidates) {
       try {
         this.lib = dlopen(path, FFI_SYMBOLS);
         this.available = true;
         return;
-      } catch {
-        // Try next candidate
+      } catch (error) {
+        lastError = error;
       }
     }
+    console.warn("[native-bridge] failed to load dylib", { candidates, lastError });
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bun/native-bridge.ts` around lines 67 - 75, The loop that calls dlopen
over candidates swallows all errors, making failures opaque; modify the code in
native-bridge.ts (the block that iterates candidates and calls dlopen with
FFI_SYMBOLS, setting this.lib and this.available) to capture the last thrown
error (e.g., lastError variable) instead of an empty catch, continue trying
other candidates, and after the loop completes with no success emit a single
diagnostic warning/error that includes the lastError message and any candidate
info so consumers can diagnose why dlopen failed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/bun/native-bridge.ts`:
- Around line 36-65: The current load() routine uses findProjectRoot() and the
first candidates entry to load a dylib from process.cwd(), which creates a
library-hijack vector; restrict this behavior to explicit dev-only mode by
gating the project-root-derived candidate behind a dev flag (e.g., an isDev or
ENABLE_NATIVE_DEV lookup) and reorder candidates to try bundled paths first
(those using import.meta.dir and Resources) before any cwd-derived path; update
the load() function to only include the join(projectRoot, "native", "build",
`libAgentTapBridge.${suffix}`) candidate when that dev flag is true and keep
findProjectRoot() unchanged but only called when dev mode is active.

---

Nitpick comments:
In `@src/bun/native-bridge.ts`:
- Around line 84-99: Both getVersion and getSystemInfo can leak native memory if
readCString throws because agenttap_free_string is only called after decoding;
modify both methods (getVersion and getSystemInfo) to store the native pointer
(result) and perform the readCString call inside a try block with
agenttap_free_string invoked in a finally block via
this.lib.symbols.agenttap_free_string(result) so the native string is always
freed even on decode errors; keep the same early-return checks for this.lib and
null result.
- Around line 67-75: The loop that calls dlopen over candidates swallows all
errors, making failures opaque; modify the code in native-bridge.ts (the block
that iterates candidates and calls dlopen with FFI_SYMBOLS, setting this.lib and
this.available) to capture the last thrown error (e.g., lastError variable)
instead of an empty catch, continue trying other candidates, and after the loop
completes with no success emit a single diagnostic warning/error that includes
the lastError message and any candidate info so consumers can diagnose why
dlopen failed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6fffd489-3a77-44ab-b974-8365d19b68d1

📥 Commits

Reviewing files that changed from the base of the PR and between 7624e03 and 2e8c330.

📒 Files selected for processing (3)
  • README.md
  • ROADMAP.md
  • src/bun/native-bridge.ts
✅ Files skipped from review due to trivial changes (2)
  • README.md
  • ROADMAP.md

@GeiserX GeiserX force-pushed the phase-0/electrobun-scaffold branch from 2e8c330 to d466fef Compare April 14, 2026 08:21
GeiserX added 5 commits April 14, 2026 10:51
- ElectroBun project structure (electrobun.config.ts, package.json, tsconfig)
- Menu bar tray app with provider toggles and capture control
- Swift FFI bridge skeleton (universal dylib, C-compatible exports)
- Main window view with dark theme status dashboard
- GitHub Actions CI workflow (build native + app)
- Shared types for providers, app state, and tray actions
- Pin bun-version to 1.3.12 in CI (reproducible builds)
- Fail CI when app artifact is missing (if-no-files-found: error)
- Commit bun.lock for deterministic dependency resolution
- Remove hostname from system_info (PII concern)
- Deep-clone providers in state init (prevent DEFAULT_PROVIDERS mutation)
- Mark DEFAULT_PROVIDERS as readonly
- Clean up native-bridge.ts (remove unused imports, dead code, dedup paths)
- Add focus-visible styling for footer links (accessibility)
- Chain build:native into build script
…very

- Replace Bun.mmap (takes file path) with CString (reads C pointer)
- Walk up from CWD to find project root for dylib path resolution
- Add Resources/ as additional candidate for bundled dylib
- Gate CWD-derived dylib paths to dev mode only (prevent library hijack)
- Capability checks return false until real implementations (Phase 1/2)
- Safe runtime type check on tray event data instead of blind assertion
@GeiserX GeiserX force-pushed the phase-0/electrobun-scaffold branch from d466fef to b9c3d4e Compare April 14, 2026 08:51
@GeiserX GeiserX merged commit f9f8e5f into main Apr 14, 2026
3 of 4 checks passed
@GeiserX GeiserX deleted the phase-0/electrobun-scaffold branch April 14, 2026 10:55
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.

1 participant