Imported from eugenepyvovarov/CodeAgentsMobile (
AGENTS.md). Install upstream withnpx skills add eugenepyvovarov/CodeAgentsMobile. Copyright stays with the author.
Repository Guidelines
Runtime note (important)
- Claude Code chat is retired. OpenCode is the only chat runtime (
OpenCodeClient,OpenCode*Service,CodingAgentRuntimeKind.openCode). - Claude → OpenCode migration runs automatically per project via
ClaudeToOpenCodeMigrationServiceon chat open / send / runtime apply: promotes runtime, clears proxy transport anchors, copies compatible API keys, soft-copiesCLAUDE.md→AGENTS.mdwhen missing, imports.mcp.jsonintoopencode.json(c), ensures session + managed scheduler MCP, stampsopenCodeMigrationVersion. - Nil/unknown
agentRuntimeRawValueresolves to OpenCode (not Claude). - CodingAgentRuntimeRegistry always returns
OpenCodeRuntimeService(legacy.claudeProxyenum case remains for decode/migration only). - Agent daemon (
:8787,ProxyTaskService,server/claude-proxy) remains for tasks/push/scheduled runs until rehomed — do not delete those paths when cleaning chat. - Daemon Anthropic auth preference lives in
AgentDaemonAuthService(not Claude chat). - Do not expand Claude Code chat UX. MCP UI/management is OpenCode-primary (
CodingAgentMCPService→OpenCodeMCPService). - Chat must not import
ClaudeCodeService. Residual CCS usage is tasks/installer/legacy settings only until retirement.
ChatViewModel module map
| File | Responsibility |
|---|---|
ChatViewModel.swift |
Lifecycle, configure, public API, thin orchestration |
ChatViewModel+OpenCodeSend.swift |
Send / stream / abort |
ChatViewModel+OpenCodeHydration.swift |
Hydrate + session reconcile |
ChatViewModel+MCP.swift |
MCP cache / fetch / deferred |
ChatViewModel+ToolApproval.swift |
Tool permission + OpenCode questions |
ChatViewModel+Persistence.swift |
Load/save messages |
ChatViewModel+MediaPrefetch.swift |
CodeAgents UI media prefetch |
ChatViewModel+DeferredStartup.swift |
Post-ready MCP/rules/media queue |
ChatViewModelSupport.swift |
Pure helpers (merge, planners) |
Runtime service files
| File | Responsibility |
|---|---|
CodingAgentRuntimeTypes.swift |
Kind, selection store, protocol |
OpenCodeRuntimeService.swift |
OpenCode health/send/hydrate/permissions |
CodingAgentRuntimeRegistry.swift |
OpenCode-only resolution |
AgentDaemonAuthService.swift |
Daemon Anthropic API key vs token preference |
Retired Claude chat stack
ClaudeCodeServicedeleted. Shared leftovers still used by OpenCode/tasks:MessageChunk,LineBuffer.ClaudeNotInstalledViewdeleted. UseOpenCodeUnavailableViewfor chat gates.StreamingJSONParser/ClaudeStreamingModels/ProxyEventRecoverydeleted (Claude chat stream decode + event recovery).- Task credentials still use
ClaudeProviderSettingsViewviaAIProviderSettingsMode.claudeProxylabeled Task Provider. ProxyStreamClientis not a chat client: onlyfetchCanonicalConversationIdremains for tasks on:8787. Chat stream/replay/permission methods removed.
Testing principles (always apply)
- Do not add app/production code solely so tests pass. No
--ui-testing/MOBILECODE_E2E_*branches, secret seeding, auto-presented sheets, extra invisible buttons, or other harness-only paths insideMobileCode/unless they are real product features users need. - Tests exist to make the app better, not to greenwash CI. Prefer fixing real UX/reliability (hittable controls, correct defaults, clearer errors, timeouts/retries that help users) over teaching the app to cheat for XCTest.
- Put harness logic in
MobileCodeTests/,MobileCodeUITests/, andscripts/e2e/(launch args only as inputs the test uses, not behavior forks in the app). - Existing evidence/seed helpers under UI-testing flags are legacy; do not add more. Prefer deleting or narrowing them when touching that code.
- When a UI test is flaky, fix test interaction (waits, scrolling, coordinates) or product accessibility/layout that helps everyone — not a test-only overlay.
Project Structure & Module Organization
MobileCode/: App source codeViews/,ViewModels/,Models/,Services/,Utils/,AppIntents/,Assets.xcassets,CodeAgentsMobileApp.swift,AppDelegate.swift- Notable service areas:
Services/OpenCode*,Services/Skills/,Services/SSH/,Services/Chat/, proxy/push (Proxy*,Push*), cloud providers (DigitalOcean / Hetzner)
MobileCodeTests/: Unit/integration tests (XCTest and Swift Testing)MobileCodeUITests/: UI / E2E testsCodeAgentsMobile.xcodeproj/: Xcode project and workspace filesscripts/: Canonical automation entrypoints (ci.sh,coverage.sh,artifact.sh,ios-simulator-evidence.sh, deploy/preview helpers)project/: OpenCode Gitea automation contract (opencode-managed.json,README.md)server/claude-proxy/,server/firebase-functions/: Backend / proxy and push-related server codespecs/,docs/plans/,docs/specs/,requirements/: Product specs, design plans, and requirement snapshotsscreenshots/: Visual assets used in README and PRsVERSIONS.TXT: Release marketing version
Version and build numbers
VERSIONS.TXTis the source of truth forMARKETING_VERSION.- Gitea's production-artifact workflow run number is the source of truth for
CURRENT_PROJECT_VERSION;.gitea/workflows/production-artifact.ymlexports it asOPENCODE_PRODUCTION_ARTIFACT_RUN_NUMBER. scripts/artifact.shmust inject both resolved values into the built bundle. Do not create timestamp build numbers or manually increment the committed fallback build number for agent-driven releases.- Settings displays the bundled values as
Version (Build). When diagnosing a reported build, verify both values from the built.appor installed app. - For local physical-device builds, use the committed fallback build number unless reproducing a specific Gitea artifact. Never label a local build with a Gitea run number unless it was built from that run's exact source revision.
Build, Test, and Development Commands
- Open in Xcode:
open CodeAgentsMobile.xcodeproj - Preferred validation (matches OpenCode automation):
./scripts/ci.sh- Uses XcodeBuildMCP, scheme
CodeAgentsMobile, default simulator iPhone 17 - Runs
CodeAgentsMobileTestsonly; skips live SSH suites (DirectSSHTest,SSHClaudeIntegrationTest)
- Uses XcodeBuildMCP, scheme
- Demo / visual evidence (native simulator):
./scripts/ios-simulator-evidence.sh - Artifacts:
./scripts/artifact.sh(simulator.app.zipby default;OPENCODE_ARTIFACT_MODE=testflightor./scripts/artifact.sh testflightfor TestFlight publication when credentials are set) - Check date:
date +%Y-%m-%d - Manual Debug build (simulator), if not using scripts:
xcodebuild -scheme CodeAgentsMobile -configuration Debug -destination 'platform=iOS Simulator,name=iPhone 17' build - Manual tests (aligned with CI skips):
xcodebuild test -scheme CodeAgentsMobile -destination 'platform=iOS Simulator,name=iPhone 17 Pro,OS=26.2' -skip-testing:CodeAgentsMobileUITests -skip-testing:CodeAgentsMobileTests/DirectSSHTest -skip-testing:CodeAgentsMobileTests/SSHClaudeIntegrationTest - Scope/skip tests:
-only-testing:CodeAgentsMobileTests/ShortcutPromptBuilderTestsor-skip-testing:CodeAgentsMobileTests/DirectSSHTest - Clean:
xcodebuild -scheme CodeAgentsMobile clean - Focused recovery coverage to prefer when touching chat open / resume:
ChatDeferredStartupTests,ChatRecoveryTimingTests,ClaudeToOpenCodeMigrationTests, OpenCode hydration/session tests underMobileCodeTests/
Chat open / recovery (do not regress)
- Local-first chat open: render persisted SwiftData messages before remote recovery / migration side effects.
- On configure:
loadMessages()first, thenClaudeToOpenCodeMigrationService.migrateIfNeeded(must not block showing local messages), then OpenCode hydration decision. - Defer MCP server refresh, managed rules setup, and broad CodeAgents UI media prefetch until after local messages and required active-session recovery complete — or until explicit MCP/tool UI actions, just-in-time send/action paths, or the project-scoped post-ready background queue.
- Project switches must cancel deferred startup work so it cannot update stale chat state.
- OpenCode only: initial recovery fetch is bounded and diffed against stored message/part hydration anchors; full-session refresh may run later in the background when more history may exist.
- Do not reintroduce Claude proxy chat poll/sync/recovery on chat open (no
ProxyStreamClientstream, no event replay, noProxyEventRecovery). - Prefer OpenCode hydration over full reload on every re-entry.
- Debug builds may emit
[ChatRecoveryTiming]lines. Timing metadata only: runtimes, project ids, operation labels, elapsed ms, statuses, booleans, counts. Do not log prompts, message text, raw payloads, credentials, URLs, project paths, attachment paths, or file contents. - Focused recovery coverage:
ClaudeToOpenCodeMigrationTests,ChatDeferredStartupTests,ChatRecoveryTimingTests, OpenCode hydration/session tests underMobileCodeTests/.
OpenCode automation
- This repo is bootstrap-managed by the OpenCode Gitea automation controller (
project/opencode-managed.json, seeproject/README.md). - Validation command for agents/CI:
/bin/bash ./scripts/ci.sh - Coverage:
/bin/bash ./scripts/coverage.sh - Production artifact (when enabled):
/bin/bash ./scripts/artifact.sh testflight(Phase app metadata is in managed JSON / README) - Visual validation and demo evidence use native iOS simulator providers; preview support is not enabled (
preview.supported: false) - Prefer existing
scripts/entrypoints over inventing new CI; do not fight controller-managed workflows under.gitea/workflows/ - Persona/review behavior is configured in the automation controller repo, not this one
Coding Style & Naming Conventions
- Swift 5.9+; 4‑space indentation, no tabs. Keep lines readable (~120 chars).
- Types
PascalCase; methods, vars, and casescamelCase. File names match primary type (e.g.,ChatViewModel.swift). - Use
// MARK:to organize sections; prefer///doc comments for public APIs. - UI-facing types annotate with
@MainActor; prefer@Observableview models. - No enforced linter in repo; format in Xcode (Editor → Structure → Re-Indent) before committing.
Testing Guidelines
- Unit/integration tests live in
MobileCodeTests/; UI/E2E inMobileCodeUITests/. Mix of XCTest and Swift Testing (@Test). - Name files
*Tests.swift; follow arrange–act–assert; isolate side effects. - Integration/network tests (e.g.,
DirectSSHTest,SSHClaudeIntegrationTest) may require external services — skip locally unless configured. Default CI already skips them. - Prefer
./scripts/ci.shover ad-hoc full-suite runs. - Examples:
- Single file:
xcodebuild test -scheme CodeAgentsMobile -destination 'platform=iOS Simulator,name=iPhone 17' -only-testing:CodeAgentsMobileTests/ShortcutPromptBuilderTests - Skip integration: add
-skip-testing:CodeAgentsMobileTests/DirectSSHTest -skip-testing:CodeAgentsMobileTests/SSHClaudeIntegrationTest
- Single file:
Commit & Pull Request Guidelines
- Commits: concise, imperative mood (e.g., "Fix scrolling in chat view"). Group related changes.
- PRs must include: clear description, linked issues, test plan (commands + expected outcome), screenshots for UI, and note any migrations/config touches. CI via
./scripts/ci.sh(or equivalent skips above) must pass.
Proxy Deployment (legacy Claude proxy)
Local proxy repo: ~/Projects/mobilecode.swift/MobileCode/server/claude-proxy
- Policy: For proxy-only work, do not push iOS app code changes. If a task is MCP/proxy/server related and does not touch
MobileCode/UI/app logic, update onlyserver/claude-proxyand avoid committing/pushing iOS project files. - Remember: Claude Code / Claude proxy is deprecated relative to OpenCode; only change the proxy when maintaining legacy compatibility.
- Commit proxy code changes to the proxy repo.
- Push the proxy repo to GitHub.
- SSH to the test server (if available).
- Pull latest:
cd /opt/claude-proxy && git pull - Proxy venv lives at
/opt/claude-proxy/.venv(created by installer). - If requirements changed:
cd /opt/claude-proxy && . .venv/bin/activate && pip install -r requirements.txt - Restart:
supervisorctl restart claude-proxy
Firebase Functions Deployment
- Deploy (from repo):
cd server/firebase-functions && firebase deploy --only functions,firestore - If
firebase login --reauthfails due to the localhost callback, use:firebase login --reauth --no-localhost - If the CLI prompts
? Enter authorization code:and you have a redirect URL likehttp://localhost:9005?...&code=XYZ&..., paste only theXYZpart. - If deploy fails with
HTTP Error: 429, Quota exceeded for quota metric 'Mutate requests'fromserviceusage.googleapis.com, wait 1–2 minutes and retry (Firebase CLI sometimes hits a shared API-enable rate limit). If it persists, enable the missing API(s) in Google Cloud Console (oftenfirebaseextensions.googleapis.com) and retry. - If deploy fails at
Error generating the service identity for pubsub.googleapis.com(oreventarc.googleapis.com), wait 1–2 minutes and retry. If it persists, run (in Cloud Shell):gcloud beta services identity create --project <project-id> --service pubsub.googleapis.com(andeventarc.googleapis.com), then retry deploy.
Security & Configuration Tips
- Never commit secrets or API keys; use Keychain at runtime (see
KeychainManager). Do not commit realGoogleService-Info.plistcontents — useGoogleService-Info.plist.exampleas the template. - Don’t commit local build artifacts or
*.xcuser*; keep.gitignoreintact. - Review
PRIVACY_POLICY.mdand relevant docs inspecs//docs/plans/when changing data flows. - If using XcodeBuildMCP, use the installed XcodeBuildMCP skill before calling XcodeBuildMCP tools.
Public project page (Selfhosted Ninja)
- Agent skills for this repo:
.agents/skills/skills.md. - Use the wordpress-manager skill (
.agents/skills/wordpress-manager/) whenever user-facing product info changes so https://selfhosted.ninja/projects/codeagents-mobile/ stays accurate (features, links, screenshots, FAQ, requirements). Note OpenCode as the supported runtime when updating public copy; Claude Code is deprecated. - Site config (selfhosted only):
.skills-data/wordpress-manager/sites.yamlor the template at.agents/skills/wordpress-manager/sites.selfhosted.yaml.