Skip to content

feat(app): ship end-to-end Darkbloom macOS experience - #645

Open
Gajesh2007 wants to merge 11 commits into
masterfrom
feat/darkbloom-macos-app-redesign
Open

feat(app): ship end-to-end Darkbloom macOS experience#645
Gajesh2007 wants to merge 11 commits into
masterfrom
feat/darkbloom-macos-app-redesign

Conversation

@Gajesh2007

@Gajesh2007 Gajesh2007 commented Aug 19, 2026

Copy link
Copy Markdown
Member

Summary

  • ships a native SwiftUI macOS app that remains a thin UI over the existing darkbloom CLI/daemon
  • makes fresh setup real end to end: readiness → account link → MDM profile → compatible model download → noninteractive provider start → live hardware-trust verification
  • wires every product surface to live daemon/CLI/coordinator truth while preserving deterministic preview fixtures
  • publishes a post-staple Darkbloom-macOS-arm64.zip with the GUI, CLI, enclave helper, fan helper, metallib, fonts, and SwiftPM resources
  • relocates authenticated production downloads to ~/.darkbloom/Darkbloom.app, creates a guarded ~/Applications/Darkbloom.app shortcut, and keeps self-update state inside the dedicated writable root
  • adds hermetic fresh-install state-machine/CLI/bundle/window coverage without touching real HOME, keychain, MDM, coordinator, or provider state

Before

flowchart TB
  subgraph BehaviorBefore[Behavior]
    B1[User runs Terminal installer / CLI] --> B2[darkbloom login, enroll, models, start]
    B2 --> B3[CLI daemon serves in background]
    B4[SwiftUI concept] --> B5[Fixtures and simulated onboarding]
    B5 -. no live completion .-> B3
  end

  subgraph CodeBefore[Code]
    C1[ProviderCore + darkbloom CLI] --> C2[launchd / coordinator / MLX]
    C3[DarkbloomApp views] --> C4[Preview stores]
    C4 -. no production adapters .-> C1
    C5[release-swift.yml] --> C6[CLI-first tar bundle]
  end
Loading

After

flowchart TB
  subgraph BehaviorAfter[Behavior]
    A1[Download Darkbloom-macOS-arm64.zip] --> A2[Double-click Darkbloom.app]
    A2 --> A3[Verify Team/signature and relocate to managed user install]
    A3 --> A4[Real readiness checks]
    A4 --> A5[Browser account approval]
    A5 --> A6[macOS profile Install + admin approval]
    A6 --> A7[Choose compatible model and resume-capable download]
    A7 --> A8[darkbloom start --model ID --local-endpoint]
    A8 --> A9[Require live daemon + endpoint + selected model + hardware trust]
    A9 --> A10[Live product shell]
  end

  subgraph CodeAfter[Code]
    D1[SwiftUI views + @Observable stores] --> D2[Foundation-only app services]
    D2 -->|read| D3[daemon-state.json / local.json]
    D2 -->|machine JSON / NDJSON| D4[co-bundled darkbloom CLI]
    D2 -->|account-scoped read| D5[Coordinator + Privy app-link]
    D4 --> D6[launchd / ProviderCore / MLX]
    D7[ProviderCoreFoundation] --> D3
    D8[release-swift.yml] --> D9[Signed + notarized + stapled app zip]
    D10[Hermetic fake CLI + isolated app smoke] --> D1
  end
Loading

Machine contracts

  • doctor --json: readiness and diagnostics
  • login --json: code / linked / error NDJSON
  • enroll --json: schema-1 already-enrolled / opened / downloaded result
  • models ... --json: catalog, local inventory, resumable progress, verification, completion
  • config get|set schedule --json: availability policy
  • earnings --json: provider contributions
  • exact setup start: darkbloom start --model <catalog-id> --local-endpoint

The app never links ProviderCore/MLX. It links only ProviderCoreFoundation for shared file schemas and invokes the signed bundled CLI for provider actions.

Security and install invariants

  • source, staged copy, and owned destination must satisfy the pinned Apple anchor, io.darkbloom.provider identifier, and Team OU SLDQ2GJ6TL
  • ad-hoc or foreign same-ID destinations are preserved, never treated as owned
  • onboarding cannot proceed from Downloads or another disposable location
  • canonical install/update root stays ~/.darkbloom; the Applications entry is only a guarded symlink
  • onboarding completion requires fresh daemon state, live matching daemon/local-endpoint PIDs, the selected model current/warm, and verified hardware trust
  • current doctor MDM evidence is authoritative over historical daemon trust

Verification

  • swift test: 2,520 tests / 257 suites passed, two consecutive full runs
  • swift test --filter DarkbloomAppTests: 343 tests / 31 suites passed
  • swift build -c release --product DarkbloomApp: passed
  • swift build -c release --product darkbloom: passed
  • scripts/test-macos-app-fresh-install.sh: passed (isolated exact main/welcome window + .ready install state)
  • scripts/test-bundle-macos-app.sh: passed
  • scripts/test-install-atomic.sh: passed
  • coordinator embedded installer parity/templating tests: passed
  • console UI: 505/505 tests, lint and production build passed
  • actionlint, shellcheck, git diff --check: passed
  • account-link subprocess race stress: 10 consecutive runs passed

Manual release gates

The code and artifact workflow are ready, but these require the protected dev-release workflow and a clean test Mac:

  1. Developer ID signing, Apple notarization/stapling, and Gatekeeper launch of the downloaded zip
  2. real Privy browser approval and keychain persistence
  3. macOS administrator-authorized MDM profile installation
  4. APNs check-in, Secure Enclave attestation, and hardware-trust transition
  5. real model download interruption/resume and post-bootstrap crash recovery
  6. managed self-update + rollback from the canonical install root

Canonical checklist: docs/operations/app-release.md.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

@vercel

vercel Bot commented Aug 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
d-inference Ready Ready Preview Aug 19, 2026 10:51pm
d-inference-console-ui-dev Ready Ready Preview Aug 19, 2026 10:51pm
d-inference-landing Ready Ready Preview Aug 19, 2026 10:51pm

Request Review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a9b804577d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +359 to +362
let address = wallet ?? ProviderAccountStore.load()
guard let address, !address.isEmpty else {
printError("No linked account found for this Mac. Run `darkbloom login` to link it, or pass --wallet <address>.")
throw ExitCode.failure

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Backfill account IDs for existing linked providers

On upgrades where ~/.darkbloom/auth_token predates this commit, the new provider_account file does not exist, so this path makes every darkbloom earnings and Contributions refresh fail. Its suggested darkbloom login recovery cannot work because performDeviceCodeLogin rejects an existing token as already logged in; without a migration or authenticated account lookup, users must discover that they need to log out and relink or manually supply --wallet, which the app cannot do.

AGENTS.md reference: AGENTS.md:L225-L225

Useful? React with 👍 / 👎.

Comment on lines 375 to +383
try AuthTokenStore.save(token)
// Persist the linked account id next to the token (best-effort):
// `darkbloom earnings` and the daemon-state identity block read
// it to address the coordinator's wallet-keyed earnings endpoint.
// A failure here must not fail the link — the auth token already
// carries the serving-critical half.
if let accountID = tokenResp.accountID, !accountID.isEmpty {
try? ProviderAccountStore.save(accountID)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clear the paired account ID during unenrollment

Once an authorized login creates provider_account here, darkbloom unenroll --force still calls LocalDataCleanup.purge(), which deletes AuthTokenStore but never ProviderAccountStore. After the command reports that local data was cleaned, daemon-state identity and darkbloom earnings continue using the previous account; the new account file must be removed alongside every auth-token cleanup path, not only by logout.

AGENTS.md reference: AGENTS.md:L225-L225

Useful? React with 👍 / 👎.

Comment on lines +67 to +69
availability: runState == .paused
? ProviderAvailabilitySnapshot(state: .paused, summary: "Paused by you", nextChangeAt: nil)
: ProviderAvailabilitySnapshot(state: .alwaysAvailable, summary: "Available whenever Darkbloom is running", nextChangeAt: nil),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Map the daemon's reported availability schedule

For any provider with a configured schedule, this mapper discards DaemonState.schedule and reports alwaysAvailable whenever the process is not paused. Consequently the live Overview row always says “Available whenever Darkbloom is running” and never shows the daemon-reported window or next transition, even though the shared state contract now supplies exactly that posture; construct this snapshot from schedule.mode, summary, and nextChangeAtEpoch.

AGENTS.md reference: AGENTS.md:L224-L224

Useful? React with 👍 / 👎.

Comment on lines +116 to +119
private func open(_ fix: DiagnosticFix) {
guard store.triggerFix(id: fix.id) != nil else { return }
launchedFix = fix
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Dispatch the selected live diagnostic action

When a live diagnostic fix is clicked, the returned DiagnosticFixAction is discarded and the view only assigns launchedFix, whose live alert has an OK button. Thus buttons labeled “Restart,” “Network Settings,” “Check for Updates,” or “Finish Setup” perform none of those actions; dispatch the returned action through the corresponding CLI/navigation/system-settings callback instead of treating every live fix as informational.

AGENTS.md reference: AGENTS.md:L226-L226

Useful? React with 👍 / 👎.

Comment on lines +234 to +240
let daemonState = DaemonStateFile.read(from: stateFileURL)
let serving = daemonState?.inferenceActive == true ? daemonState?.currentModel : nil
return ModelLibrarySnapshot(
catalog: catalog,
local: local,
warmModelIDs: Set(daemonState?.warmModels ?? []),
servingModelID: serving,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Ignore warm models from inactive daemon snapshots

After a model has been loaded and the provider is stopped or crashes, daemon-state.json remains on disk with its last warmModels, but this refresh imports those values without checking snapshot freshness or whether the recorded process is alive. The model therefore remains .warm indefinitely in the library, and removeModel continually rejects deletion with “Take this model offline” even though it is already offline; only use runtime model state from a fresh, live daemon snapshot.

Useful? React with 👍 / 👎.

Comment on lines +150 to +151
if let loadError = inputs.state?.lastModelLoadError,
runState != .paused {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Expire the model-load problem banner

A transient model-load error is deliberately removed from the .attention run state after five minutes, but resolveProblem does not apply the same age check. Because the daemon keeps rewriting its last error and does not clear it on a later successful load, the Overview continues showing “A model failed to load” and recommending a restart indefinitely after the provider has recovered; suppress the problem once loadErrorAttentionAge has elapsed.

Useful? React with 👍 / 👎.

else {
throw ValidationError("Configured coordinator URL '\(coordinatorURL)' is not a usable HTTP base.")
}
components.path = "/v1/provider/earnings"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Use the linked-account earnings history

For any provider linked through device login, ProviderAccountStore contains an account ID, but this URL targets the unlinked-wallet ProviderPayout endpoint; linked work is stored in provider_earnings, and its ledger fallback recognizes only LedgerPayout, not LedgerFloorDraw. As a result base rewards disappear from lifetime totals/history, and all per-node keys/token/model fields are discarded, making the live per-Mac Contributions scopes inaccurate; fetch the account-linked earnings records with appropriate authentication or return equivalent records from this endpoint.

AGENTS.md reference: AGENTS.md:L225-L225

Useful? React with 👍 / 👎.

if let failure = store.failure {
ChatFailureNotice(
failure: failure,
onRetry: lastUserPrompt.map { prompt in { submit(prompt) } },

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Retry the failed turn without appending it again

When a live request fails before or during streaming, the failed user message remains in store.messages, but Retry calls submit(prompt), and beginResponse appends the same prompt again before building the wire history. Each retry therefore duplicates the visible turn and sends the model two copies, then three, changing the prompt and response; retry the existing last turn or remove it before resubmission.

Useful? React with 👍 / 👎.

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