feat(app): ship end-to-end Darkbloom macOS experience - #645
Conversation
…nfig/earnings/login/models)
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
💡 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".
| 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 |
There was a problem hiding this comment.
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 👍 / 👎.
| 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) | ||
| } |
There was a problem hiding this comment.
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 👍 / 👎.
| availability: runState == .paused | ||
| ? ProviderAvailabilitySnapshot(state: .paused, summary: "Paused by you", nextChangeAt: nil) | ||
| : ProviderAvailabilitySnapshot(state: .alwaysAvailable, summary: "Available whenever Darkbloom is running", nextChangeAt: nil), |
There was a problem hiding this comment.
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 👍 / 👎.
| private func open(_ fix: DiagnosticFix) { | ||
| guard store.triggerFix(id: fix.id) != nil else { return } | ||
| launchedFix = fix | ||
| } |
There was a problem hiding this comment.
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 👍 / 👎.
| 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, |
There was a problem hiding this comment.
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 👍 / 👎.
| if let loadError = inputs.state?.lastModelLoadError, | ||
| runState != .paused { |
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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) } }, |
There was a problem hiding this comment.
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 👍 / 👎.
Summary
darkbloomCLI/daemonDarkbloom-macOS-arm64.zipwith the GUI, CLI, enclave helper, fan helper, metallib, fonts, and SwiftPM resources~/.darkbloom/Darkbloom.app, creates a guarded~/Applications/Darkbloom.appshortcut, and keeps self-update state inside the dedicated writable rootBefore
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] endAfter
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 endMachine contracts
doctor --json: readiness and diagnosticslogin --json: code / linked / error NDJSONenroll --json: schema-1 already-enrolled / opened / downloaded resultmodels ... --json: catalog, local inventory, resumable progress, verification, completionconfig get|set schedule --json: availability policyearnings --json: provider contributionsdarkbloom start --model <catalog-id> --local-endpointThe 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
io.darkbloom.provideridentifier, and Team OUSLDQ2GJ6TL~/.darkbloom; the Applications entry is only a guarded symlinkVerification
swift test: 2,520 tests / 257 suites passed, two consecutive full runsswift test --filter DarkbloomAppTests: 343 tests / 31 suites passedswift build -c release --product DarkbloomApp: passedswift build -c release --product darkbloom: passedscripts/test-macos-app-fresh-install.sh: passed (isolated exact main/welcome window +.readyinstall state)scripts/test-bundle-macos-app.sh: passedscripts/test-install-atomic.sh: passedactionlint,shellcheck,git diff --check: passedManual release gates
The code and artifact workflow are ready, but these require the protected dev-release workflow and a clean test Mac:
Canonical checklist:
docs/operations/app-release.md.Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.