Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 13 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1151,8 +1151,8 @@ jobs:
# `if: always()` is load-bearing. Without it, a failed or skipped dependency
# skips this job too — and GitHub reports a skipped job as success, so the gate
# would go green precisely when something went wrong.
macos-app:
name: macos app
widget:
name: macos widget
needs: [changes, gates]
if: github.event_name != 'pull_request' || needs.changes.outputs.ci == 'true'
runs-on: macos-latest
Expand All @@ -1171,11 +1171,16 @@ jobs:
- name: Install dependencies
run: bun install --frozen-lockfile

- name: Test macOS menu bar app
- name: Test MenuBarCore
run: bun run test:macos

- name: Build macOS menu bar app
run: bun run build:macos
- name: Build WidgetKit appex
run: bash desktop/scripts/build-widget.sh

- name: Verify WidgetKit appex
run: |
test -x desktop/src-tauri/widget/OpenCodexWidget.appex/Contents/MacOS/OpenCodexWidget
codesign -dv desktop/src-tauri/widget/OpenCodexWidget.appex

desktop-shell:
name: desktop shell
Expand Down Expand Up @@ -1225,7 +1230,7 @@ jobs:
# direct dependencies only, so a failing `select-windows-runner` would
# otherwise reach this gate as nothing at all while its dependents report
# `skipped`, which is the shape the step below is written to catch.
needs: [changes, select-windows-runner, test, storage-policy, api-usage, gates, platform-macos, macos-control, platform-windows, keyring-smoke, docker-smoke, docs-site-build, structure-gate, npm-global-smoke, macos-app, desktop-shell]
needs: [changes, select-windows-runner, test, storage-policy, api-usage, gates, platform-macos, macos-control, platform-windows, keyring-smoke, docker-smoke, docs-site-build, structure-gate, npm-global-smoke, widget, desktop-shell]
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
Expand Down Expand Up @@ -1291,13 +1296,13 @@ jobs:
GATED_JOBS="changes select-windows-runner test storage-policy api-usage gates"
GATED_JOBS="$GATED_JOBS platform-macos keyring-smoke docker-smoke npm-global-smoke"
GATED_JOBS="$GATED_JOBS macos-control platform-windows docs-site-build"
GATED_JOBS="$GATED_JOBS structure-gate macos-app"
GATED_JOBS="$GATED_JOBS structure-gate widget"
GATED_JOBS="$GATED_JOBS desktop-shell"

expected_for() {
case "$1" in
changes|select-windows-runner) echo requested ;;
test|storage-policy|api-usage|gates|platform-macos|keyring-smoke|docker-smoke|macos-app)
test|storage-policy|api-usage|gates|platform-macos|keyring-smoke|docker-smoke|widget)
echo "$scoped" ;;
desktop-shell)
echo "$scoped" ;;
Expand Down
51 changes: 2 additions & 49 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,47 +69,6 @@ jobs:
process.exit(1);
}
NODE
package-macos:
needs: validate-dispatch
runs-on: macos-latest
timeout-minutes: 20
permissions:
contents: read
outputs:
archive_name: ${{ steps.package.outputs.archive_name }}
checksum_name: ${{ steps.package.outputs.checksum_name }}
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false

- name: Package the macOS companion
id: package
env:
RELEASE_VERSION: ${{ inputs.version }}
UNIVERSAL: "1"
# A monotonic numeric CFBundleVersion. Preview versions carry a suffix that
# Apple does not accept in that field, so the script uses the numeric core
# plus this run number.
MACOS_BUILD_NUMBER: ${{ github.run_number }}
# NOTE: intentionally no MACOS_SIGN_IDENTITY here. The build script honours
# it, but an identity NAME alone cannot sign on a hosted runner — the
# certificate and private key are never imported into a keychain, so codesign
# fails with "no identity found". Real Developer ID signing needs a protected
# P12 import, a temporary keychain, notarytool credentials, and stapling, all
# as one security-reviewed change. Until then the asset is ad-hoc signed and
# the docs carry the Gatekeeper first-launch path.
run: bash scripts/package-macos-release.sh

- name: Upload the release asset
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: macos-release
path: dist/release/
if-no-files-found: error
retention-days: 7

package-standalone:
needs: validate-dispatch
strategy:
Expand Down Expand Up @@ -212,20 +171,14 @@ jobs:
if-no-files-found: error
retention-days: 7

attach-macos:
attach-standalone:
runs-on: ubuntu-latest
needs: [publish, package-macos, package-standalone]
needs: [publish, package-standalone]
if: ${{ inputs.dry-run != true }}
timeout-minutes: 10
permissions:
contents: write
steps:
- name: Download the macOS packaged asset
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: macos-release
path: dist/release

- name: Download standalone packaged assets
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,6 @@ dist/macos/
dist/release/
desktop/src-tauri/binaries/
desktop/src-tauri/resources/
desktop/src-tauri/widget/
desktop/src-tauri/gen/
desktop/src-tauri/target/
5 changes: 2 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,8 @@ Bun-native TypeScript with no separate server compile step.
seeds in `layout.json` place a conventionally named file until then.
History: `devlog/_fin/260905_test_modularization_and_windows/`.
- `gui/` — React + Vite dashboard; packaged output is served from `gui/dist`.
- `app/` — native macOS menu bar companion (Swift + AppKit, no third-party
dependencies). `MenuBarCore` is the testable transport/model layer,
`MenuBarUI` the AppKit views, `MenuBarApp` the entry point. Its tests are
- `app/` — native macOS WidgetKit extension bundled into the Tauri desktop app;
`MenuBarCore` is its snapshot model/formatting layer. Its tests are
executables, not XCTest bundles — Command Line Tools ships neither a usable
XCTest module nor the swift-testing runtime.
- `docs-site/` — public docs (Astro + Starlight), deployed to GitHub Pages.
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,13 @@ Open **http://localhost:10100** and configure everything in the web dashboard
(40+ built-ins, or any OpenAI-compatible endpoint), pick models, manage accounts. `ocx gui`
re-opens the dashboard at any time.

### macOS menu bar app
### macOS desktop app and widget

A native companion for proxy status, usage, and provider quotas without opening the
dashboard. The source lives in [`app/`](./app) (Swift + AppKit, no third-party
dependencies). Download it from the
A native desktop app and WidgetKit extension for proxy status, usage, and provider
quotas without opening the dashboard. The snapshot model lives in [`app/`](./app)
(`MenuBarCore`). Download it from the
[releases page](https://github.com/lidge-jun/opencodex/releases) or build it locally with
`bun run build:macos`.
`bun run prepare-sidecar && bun run prepare-widget && bunx tauri build`.

The first launch needs a right-click → Open, because the app is ad-hoc signed rather
than notarized. See the [macOS Menu Bar App guide](https://lidge-jun.github.io/opencodex/guides/macos-menu-bar/)
Expand Down
42 changes: 0 additions & 42 deletions app/Info.plist

This file was deleted.

23 changes: 1 addition & 22 deletions app/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,14 @@
import PackageDescription

let package = Package(
name: "OpenCodexMenuBar",
name: "OpenCodexWidget",
platforms: [.macOS(.v13)],
products: [
.executable(name: "OpenCodexMenuBar", targets: ["MenuBarApp"]),
.executable(name: "OpenCodexWidget", targets: ["OpenCodexWidget"]),
.executable(name: "MenuBarCoreTests", targets: ["MenuBarCoreTests"]),
.executable(name: "MenuBarUITests", targets: ["MenuBarUITests"]),
.executable(name: "UIProbe", targets: ["UIProbe"]),
.executable(name: "IconProbe", targets: ["IconProbe"]),
],
targets: [
.target(name: "MenuBarCore", path: "Sources/MenuBarCore"),
// AppKit views live in a library so both the app and the visual-QA probe can
// build the same surface. An executable target cannot be imported.
.target(name: "MenuBarUI", dependencies: ["MenuBarCore"], path: "Sources/MenuBarUI"),
.executableTarget(
name: "MenuBarApp",
dependencies: ["MenuBarCore", "MenuBarUI"],
path: "Sources/MenuBarApp"
),
.executableTarget(
name: "OpenCodexWidget",
dependencies: ["MenuBarCore"],
Expand All @@ -41,15 +29,6 @@ let package = Package(
dependencies: ["MenuBarCore"],
path: "Sources/MenuBarCoreTests"
),
// UI-layer tests need AppKit and an NSApplication, so they are a separate
// executable from the dependency-free core suite.
.executableTarget(
name: "MenuBarUITests",
dependencies: ["MenuBarCore", "MenuBarUI"],
path: "Sources/MenuBarUITests"
),
.executableTarget(name: "UIProbe", dependencies: ["MenuBarCore", "MenuBarUI"], path: "Sources/UIProbe"),
.executableTarget(name: "IconProbe", dependencies: ["MenuBarCore", "MenuBarUI"], path: "Sources/IconProbe"),
],
swiftLanguageVersions: [.v5]
)
32 changes: 0 additions & 32 deletions app/Sources/IconProbe/main.swift

This file was deleted.

11 changes: 0 additions & 11 deletions app/Sources/MenuBarApp/main.swift

This file was deleted.

2 changes: 1 addition & 1 deletion app/Sources/MenuBarCore/Discovery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public enum ProxyDiscovery {
/// Reads `runtime-port.json`, falling back to the default port on any problem.
///
/// Every failure mode — missing file, malformed JSON, out-of-range port — resolves to
/// the default rather than throwing. A menu bar app that refuses to start because a
/// the default rather than throwing. A desktop app that refuses to start because a
/// cache file is unreadable would be worse than one that probes the usual port.
public static func resolve(configDirectory directory: URL) -> ProxyEndpoint {
let file = directory.appendingPathComponent("runtime-port.json")
Expand Down
2 changes: 1 addition & 1 deletion app/Sources/MenuBarCore/PollingCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Foundation

/// Owns the refresh schedule and turns transport results into a `ProxySnapshot`.
///
/// Polling is deliberately conservative. A menu bar app that hits a local server every
/// Polling is deliberately conservative. A desktop app that hits a local server every
/// five seconds forever is a battery complaint waiting to happen, so heavy aggregation
/// endpoints are fetched only while the popover is open, and repeated failures back the
/// liveness tick off rather than hammering a proxy the user has stopped on purpose.
Expand Down
2 changes: 1 addition & 1 deletion app/Sources/MenuBarCore/ProxyClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ public actor ProxyClient {
request.httpMethod = method
request.timeoutInterval = timeout ?? (method == "GET" ? 4 : 6)
let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "dev"
request.setValue("OpenCodexMenuBar/\(version)", forHTTPHeaderField: "User-Agent")
request.setValue("OpenCodexWidget/\(version)", forHTTPHeaderField: "User-Agent")
if let credential = key ?? apiKey {
request.setValue(credential, forHTTPHeaderField: "x-opencodex-api-key")
}
Expand Down
2 changes: 1 addition & 1 deletion app/Sources/MenuBarCore/WidgetSnapshot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ public final class WidgetSnapshotStore: @unchecked Sendable {
private var loggedFailures = Set<String>()

public init(
widgetBundleID: String = "com.opencodex.menubar.widget",
widgetBundleID: String = "com.opencodex.desktop.widget",
fileManager: FileManager = .default,
homeDirectory: URL = FileManager.default.homeDirectoryForCurrentUser
) {
Expand Down
2 changes: 1 addition & 1 deletion app/Sources/MenuBarCoreTests/TransportSuite.swift
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ enum TransportSuite {
let url = StubProtocol.recorded.first?.url?.absoluteString ?? ""
t.expect(url.contains("range=7d"), "expected range=7d in \(url)")
t.expect(url.contains("/api/usage"), "expected /api/usage in \(url)")
t.equal(StubProtocol.recorded.first?.value(forHTTPHeaderField: "User-Agent"), "OpenCodexMenuBar/dev")
t.equal(StubProtocol.recorded.first?.value(forHTTPHeaderField: "User-Agent"), "OpenCodexWidget/dev")
}

t.test("requests: the provider patch sends exactly {\"disabled\":true}") {
Expand Down
Loading
Loading