diff --git a/.github/prompts/build-lint.prompt.md b/.github/prompts/build-lint.prompt.md deleted file mode 100644 index 7a9e9c98..00000000 --- a/.github/prompts/build-lint.prompt.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -description: "Build the project, check for SwiftLint errors and warnings, and fix them." -agent: "agent" ---- - -Build the project and fix any SwiftLint violations found. - -Before either path, ensure the required local build configuration exists. Do not overwrite an existing file: - -```bash -if [ ! -f Secrets.xcconfig ]; then - cat > Secrets.xcconfig << 'EOF' -VOTICE_API_KEY = -VOTICE_API_SECRET = -VOTICE_APP_ID = -EOF -fi -``` - -## MCP Detection - -Before building, check whether the **XcodeBuildMCP** MCP server is available by searching for its tools using `tool_search_tool_regex` with the pattern `mcp_xcodebuildmcp_build_sim`. Then follow the appropriate path below. - ---- - -## Path A — XcodeBuildMCP available (preferred) - -1. **Verify session defaults** by calling `mcp_xcodebuildmcp_session_show_defaults` before the first build. - - Defaults are pre-configured in `.xcodebuildmcp/config.yaml` and loaded automatically at server startup: - - scheme: `openclient-llm` - - simulator: `iPhone 17 Pro Max` - - If `projectPath` is missing or wrong, use project discovery and set it with `mcp_xcodebuildmcp_session_set_defaults`. - - Only override other values if they are missing or wrong. - -2. **Build** by calling `mcp_xcodebuildmcp_build_sim` with `CODE_SIGN_IDENTITY=""` and `CODE_SIGNING_REQUIRED=NO` as extra build arguments. -3. **Review output**: identify all SwiftLint warnings and errors from the build output. -4. **Report violations**: list every violation with file, line number, rule name, and description. -5. **If any violation is found**: - - Read the affected file to understand context. - - Fix the violation following `.swiftlint.yml` rules. - - Call `mcp_xcodebuildmcp_build_sim` again to confirm the fix. - - Repeat until zero violations remain. -6. **Report final result**: confirm clean build with no violations. - ---- - -## Path B — XcodeBuildMCP not available (fallback) - -1. **Build the project** using the shell command below. -2. **Review output**: identify all SwiftLint warnings and errors. -3. **Report violations**: list every violation with file, line number, rule name, and description. -4. **If any violation is found**: - - Read the affected file to understand context. - - Fix the violation following `.swiftlint.yml` rules. - - Re-run the command to confirm the fix. - - Repeat until zero violations remain. -5. **Report final result**: confirm clean build with no violations. - -```bash -set -o pipefail -xcodebuild build \ - -project openclient-llm.xcodeproj \ - -scheme openclient-llm \ - -destination 'platform=iOS Simulator,name=iPhone 17 Pro Max' \ - CODE_SIGN_IDENTITY="" \ - CODE_SIGNING_REQUIRED=NO \ - 2>&1 | tee /tmp/openclient-llm-build.log -``` - ---- - -## Rules - -- Never disable a SwiftLint rule (inline or in `.swiftlint.yml`) to suppress a violation -- Never use `// swiftlint:disable` comments to silence warnings -- Fix the root cause: refactor code to comply with the rule (extract methods, split files, rename variables, etc.) -- If a fix requires changing shared code, ensure it doesn't break other features -- Do not modify `.swiftlint.yml` unless explicitly asked by the user -- After fixing violations, run the full test suite to ensure nothing is broken diff --git a/.github/prompts/create-tests.prompt.md b/.github/prompts/create-tests.prompt.md deleted file mode 100644 index a58ca994..00000000 --- a/.github/prompts/create-tests.prompt.md +++ /dev/null @@ -1,111 +0,0 @@ ---- -description: "Add unit tests for existing code that lacks coverage: ViewModels, UseCases, Repositories, and Parsers." -agent: "agent" ---- - -Add unit tests for existing code that has no or insufficient test coverage. - -## Input required - -Before starting, ask the user for: -1. **Target** — which file, type, or feature to cover (e.g. `OrderViewModel`, `SaveOrderUseCase`, all of `Features/Order/`) -2. **Priority** — full coverage or just critical paths - ---- - -## Process - -### 1. Analyse existing code - -- Read the target source file(s) fully -- List every public method and every `Event` → `State` transition to cover -- Check `openclient-llm-test/` for any existing tests to avoid duplication - -### 2. Create or locate the test file - -- Path: `openclient-llm-test/Features//Tests.swift` -- If testing a Core type, mirror its Core subfolder, for example `openclient-llm-test/Core/Managers/Tests.swift` -- File header: - ```swift - // - // Tests.swift - // openclient-llm - // - // Created by Arturo Carretero Calvo on DD/MM/YYYY. - // Copyright © YYYY Arturo Carretero Calvo. All rights reserved. - // - ``` - -### 3. Create mocks if needed - -- One mock per protocol dependency, in `openclient-llm-test/Mocks/Mock.swift` -- Every mock file must use the same mandatory copyright header shown above, with its actual file name -- Pattern: - ```swift - // Safety: Only used within serialized @MainActor test methods. - final class Mock: , @unchecked Sendable { - // Configurable stubs: var result / var error - } - ``` -- Only create a mock if one does not already exist - -### 4. Write the tests - -Follow these coverage requirements per type: - -**ViewModel (`@MainActor final class`)** -- Every `Event` → `State` transition -- Error path (repository/useCase throws) -- Loading state set before async work completes - -**UseCase (`struct`)** -- Success path with valid input -- Error path (dependency throws) -- Edge cases (empty input, nil optional, boundary values) - -**Repository (mocked data source)** -- CRUD operations -- Data mapping correctness -- Cache invalidation (if stateful `actor`) - -**Parser (pure function)** -- Valid input → correct model -- Invalid input → correct error type -- Round-trip: parse → serialize → parse yields same result - -### 5. Naming convention - -``` -test___() -``` - -Examples: -```swift -func test_send_viewAppeared_setsLoadingState() { } -func test_execute_withValidInput_returnsItem() async throws { } -func test_execute_whenRepositoryThrows_setsErrorState() async { } -func test_parse_withMissingRequiredField_throwsError() throws { } -``` - -### 6. Run and verify - -- Run `mcp_xcodebuildmcp_test_sim` or the `run-tests` prompt -- All new tests must pass -- No previously passing tests may regress - -### 7. Report - -- List every test added with its name and what it covers -- Final count: total tests / passed / failed - ---- - -## Rules - -- Never write a test that always passes regardless of the implementation -- Never use `XCTAssertTrue(true)` or equivalent no-op assertions -- Use `XCTAssertEqual`, `XCTAssertThrowsError`, `XCTAssertNil`, `XCTUnwrap` — be specific -- Every test class must be `@MainActor` because the target uses default MainActor isolation -- Do not add `@MainActor` per method if the class is already `@MainActor` -- Do not test private methods directly — test through the public API -- Do not modify source code to make it easier to test (except extracting a protocol if genuinely needed) diff --git a/.github/prompts/fix-bug.prompt.md b/.github/prompts/fix-bug.prompt.md deleted file mode 100644 index e07e324e..00000000 --- a/.github/prompts/fix-bug.prompt.md +++ /dev/null @@ -1,75 +0,0 @@ ---- -description: "Reproduce a bug, identify its root cause, fix it in source code, and verify with tests." -agent: "agent" ---- - -Investigate and fix a bug reported by the user. - -## Input required - -Before starting, ask the user for: -1. **Bug description** — what happens vs. what should happen -2. **Steps to reproduce** — if known -3. **Affected area** — feature name, screen, or file if known - ---- - -## Investigation cycle - -### 1. Reproduce - -- Read the relevant source files (ViewModel, UseCase, Repository, Model) -- Identify the exact code path that triggers the wrong behaviour -- If a test already exists for the affected area, run it: - - Use the `run-tests` prompt or `mcp_xcodebuildmcp_test_sim` directly - - Confirm whether the test catches the bug (if not, the test is incomplete) - -### 2. Identify root cause - -- Trace the bug to its origin layer: - - **View**: incorrect state observation or missing `.task` - - **ViewModel**: wrong Event/State transition or missing case - - **UseCase**: incorrect business logic or missing error handling - - **Repository**: wrong data mapping or cache invalidation issue - - **Model**: invariant violation or incorrect default value -- Do not fix symptoms — fix the root cause - -### 3. Fix - -- Edit only the files necessary to fix the root cause -- Do not refactor unrelated code while fixing the bug -- If the fix affects shared code (Core, Managers), verify no other feature breaks - -### 4. Add or update tests - -- If the bug had no test covering it, add one: - - Name it `test___()` - - Place it in the mirrored path (`openclient-llm-test/Features//` or the matching `Core/` subfolder) - - Ensure every new Swift file has the repository copyright header and every test class is `@MainActor` -- If an existing test was wrong (not the code), fix the test and document why - -### 5. Verify - -- Run the full test suite using the `run-tests` prompt or `mcp_xcodebuildmcp_test_sim` -- Confirm: - - The new/updated test passes - - No previously passing tests have regressed -- Run a build to confirm zero SwiftLint violations - -### 6. Report - -Summarise: -- Root cause found -- Files changed and why -- Test added or updated -- Final test count: total / passed / failed - ---- - -## Rules - -- Never suppress a test or disable a SwiftLint rule to make the suite pass -- Never use `// swiftlint:disable` comments -- Fix the root cause — not the symptom -- If the fix requires a behaviour change visible to the user, flag it explicitly before applying -- Do not introduce new dependencies or abstractions to fix a simple bug diff --git a/.github/prompts/new-feature.prompt.md b/.github/prompts/new-feature.prompt.md deleted file mode 100644 index 83d4edeb..00000000 --- a/.github/prompts/new-feature.prompt.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -description: "Scaffold a complete feature module with View, ViewModel, UseCase, Repository, Model, and Tests following project architecture." -agent: "agent" -argument-hint: "Feature name (e.g., Chat, Settings, Models)" ---- - -Create a new feature module named `${input}`. Generate all files following the project architecture: - -## Files to create - -### Shared code (in `openclient-llm/Shared/Features/${input}/`) - -1. **Views/${input}View.swift** — SwiftUI view following the View Template in `AGENTS.md` and `specs/architecture.instructions.md` -2. **ViewModels/${input}ViewModel.swift** — `@Observable @MainActor` ViewModel with the Event/State pattern from those instructions -3. **UseCases/** — Create relevant UseCase(s) with protocol -4. **Repositories/** — Create relevant Repository with protocol -5. **Models/** — Create domain models as Codable structs - -### Tests (in `openclient-llm-test/Features/${input}/`) - -6. **${input}ViewModelTests.swift** — Test all Event → State transitions -7. **UseCase and Repository tests** — Add isolated tests for every generated UseCase and Repository -8. **Mock files** in `openclient-llm-test/Mocks/` — Mock protocols for dependencies - -## Rules - -- Follow the ViewModel Event/State template exactly -- Follow the View template exactly (with `@State private var viewModel`, switch on state, `.task {}`) -- Every generated Swift file must start with the repository copyright header, using its actual file name and creation date -- Use `// MARK: -` sections consistently -- All protocols must be `Sendable` -- Prefer `struct` UseCases; use a class only when the operation requires shared mutable state or reference semantics -- UseCases and Repositories must have protocol definitions -- Every test class must be `@MainActor` -- Test naming: `test___()` diff --git a/.github/prompts/new-usecase.prompt.md b/.github/prompts/new-usecase.prompt.md deleted file mode 100644 index b124d336..00000000 --- a/.github/prompts/new-usecase.prompt.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -description: "Scaffold a UseCase with protocol and unit test following project architecture." -agent: "agent" -argument-hint: "UseCase name (e.g., SendMessage, FetchModels)" ---- - -Create a new UseCase named `${input}UseCase`. Generate all files: - -## Files to create - -### UseCase (in the appropriate `openclient-llm/Shared/Features//UseCases/`) - -1. **${input}UseCase.swift** — UseCase implementation with protocol - -```swift -protocol ${input}UseCaseProtocol: Sendable { - func execute(...) async throws -> ... -} - -struct ${input}UseCase: ${input}UseCaseProtocol { - // MARK: - Properties - - private let repository: Protocol - - // MARK: - Init - - init(repository: Protocol) { - self.repository = repository - } - - // MARK: - Execute - - func execute(...) async throws -> ... { - // Business logic - } -} -``` - -### Test (in `openclient-llm-test/Features//`) - -2. **${input}UseCaseTests.swift** — Unit tests with mocked repository, Given-When-Then pattern - -## Rules - -- Protocol must be `Sendable` -- Prefer a `struct`; use a class only when the operation requires shared mutable state or reference semantics -- Inject dependencies through init -- One public `execute` method per UseCase -- Every generated Swift file must start with the repository copyright header, using its actual file name and creation date -- The test class must be `@MainActor` -- Test naming: `test_execute__()` diff --git a/.github/prompts/new-view.prompt.md b/.github/prompts/new-view.prompt.md deleted file mode 100644 index 2d2adaa3..00000000 --- a/.github/prompts/new-view.prompt.md +++ /dev/null @@ -1,105 +0,0 @@ ---- -description: "Scaffold a SwiftUI View with its ViewModel following the Event/State pattern and project templates." -agent: "agent" -argument-hint: "View name (e.g., ChatDetail, ServerSettings)" ---- - -Create a new View named `${input}View` with its ViewModel. Generate all files: - -Every generated Swift file must start with the repository copyright header, using its actual file name and creation date, before imports. - -## Files to create - -### In the appropriate `openclient-llm/Shared/Features//` - -1. **Views/${input}View.swift** - -```swift -import SwiftUI - -struct ${input}View: View { - // MARK: - Properties - - @State private var viewModel = ${input}ViewModel() - - // MARK: - View - - var body: some View { - Group { - switch viewModel.state { - case .loading: - ProgressView() - case .loaded: - // View content - } - } - .task { - viewModel.send(.viewAppeared) - } - } -} - -// MARK: - Private - -private extension ${input}View {} - -#Preview { - ${input}View() -} -``` - -2. **ViewModels/${input}ViewModel.swift** - -```swift -import Foundation - -@Observable -@MainActor -final class ${input}ViewModel { - // MARK: - Properties - - enum Event { - case viewAppeared - } - - enum State: Equatable { - case loading - case loaded(LoadedState) - } - - struct LoadedState: Equatable {} - - private(set) var state: State - - // MARK: - Init - - init(state: State = .loading) { - self.state = state - } - - // MARK: - Input functions - - func send(_ event: Event) { - switch event { - case .viewAppeared: - state = .loaded(.init()) - } - } -} - -// MARK: - Private - -private extension ${input}ViewModel {} -``` - -### Test (in `openclient-llm-test/Features//`) - -3. **${input}ViewModelTests.swift** — Test all Event → State transitions - -## Rules - -- Follow templates exactly -- Use `.task {}` not `.onAppear` -- Always include `#Preview` -- Use `// MARK: -` sections consistently -- Mark the test class `@MainActor` diff --git a/.github/prompts/run-app.prompt.md b/.github/prompts/run-app.prompt.md deleted file mode 100644 index 9f46c0c0..00000000 --- a/.github/prompts/run-app.prompt.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -description: "Build and launch the app on the iPhone 17 Pro Max simulator." -agent: "agent" ---- - -Build and run the app on the iPhone 17 Pro Max simulator so the user can interact with it. - -Before either path, create the required local build configuration if it does not exist. Never overwrite an existing file: - -```bash -if [ ! -f Secrets.xcconfig ]; then - cat > Secrets.xcconfig << 'EOF' -VOTICE_API_KEY = -VOTICE_API_SECRET = -VOTICE_APP_ID = -EOF -fi -``` - -## MCP Detection - -Before building, check whether the **XcodeBuildMCP** MCP server is available by searching for its tools using `tool_search_tool_regex` with the pattern `mcp_xcodebuildmcp_build_run_sim`. Then follow the appropriate path below. - ---- - -## Path A — XcodeBuildMCP available (preferred) - -1. **Verify session defaults** by calling `mcp_xcodebuildmcp_session_show_defaults` before the first build. - - Defaults are pre-configured in `.xcodebuildmcp/config.yaml` and loaded automatically at server startup: - - scheme: `openclient-llm` - - simulator: `iPhone 17 Pro Max` - - If `projectPath` is missing or wrong, use project discovery and set it with `mcp_xcodebuildmcp_session_set_defaults`. - - Only override other values if they are missing or wrong. - -2. **Build and run** by calling `mcp_xcodebuildmcp_build_run_sim` with `CODE_SIGN_IDENTITY=""` and `CODE_SIGNING_REQUIRED=NO` as extra build arguments. - - This boots the simulator automatically if needed and launches the app. - -3. **Report**: confirm the app launched successfully. The user will now interact with it directly in the simulator. - ---- - -## Path B — XcodeBuildMCP not available (fallback) - -1. **Boot the simulator** (if not already running): - -```bash -xcrun simctl boot "iPhone 17 Pro Max" 2>/dev/null || true -open -a Simulator -``` - -2. **Build and install the app**: - -```bash -set -o pipefail -xcodebuild build \ - -project openclient-llm.xcodeproj \ - -scheme openclient-llm \ - -destination 'platform=iOS Simulator,name=iPhone 17 Pro Max' \ - -derivedDataPath /tmp/openclient-llm-build \ - CODE_SIGN_IDENTITY="" \ - CODE_SIGNING_REQUIRED=NO \ - 2>&1 | tee /tmp/openclient-llm-build.log -``` - -3. **Install and launch**: - -```bash -# Get the booted simulator UDID -UDID=$(xcrun simctl list devices booted | grep "iPhone 17 Pro Max" | grep -E -o '[0-9A-F-]{36}' | head -1) - -# Install the built app -xcrun simctl install "$UDID" \ - /tmp/openclient-llm-build/Build/Products/Debug-iphonesimulator/openclient-llm.app - -# Launch the app -xcrun simctl launch "$UDID" com.artcc.openclient-llm -``` - -4. **Report**: confirm the app launched. The user will now interact with it directly in the simulator. - ---- - -## Rules - -- Do not modify source code unless a build error prevents the app from launching -- If the build fails due to SwiftLint violations, run the `build-lint` prompt first -- Do not alter simulator state beyond booting it (no erase, no reset) -- Do not capture or stream logs — the goal is to have the app running for the user to use diff --git a/.github/prompts/run-tests.prompt.md b/.github/prompts/run-tests.prompt.md deleted file mode 100644 index 4980ea54..00000000 --- a/.github/prompts/run-tests.prompt.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -description: "Run all unit tests, report results, and fix any failures found." -agent: "agent" ---- - -Run the full unit test suite for the project and report results. - -Before either path, create the required local build configuration if it does not exist. Never overwrite an existing file: - -```bash -if [ ! -f Secrets.xcconfig ]; then - cat > Secrets.xcconfig << 'EOF' -VOTICE_API_KEY = -VOTICE_API_SECRET = -VOTICE_APP_ID = -EOF -fi -``` - -## MCP Detection - -Before running tests, check whether the **XcodeBuildMCP** MCP server is available by searching for its tools using `tool_search_tool_regex` with the pattern `mcp_xcodebuildmcp_test_sim`. Then follow the appropriate path below. - ---- - -## Path A — XcodeBuildMCP available (preferred) - -1. **Verify session defaults** by calling `mcp_xcodebuildmcp_session_show_defaults` before the first test call. - - Defaults are pre-configured in `.xcodebuildmcp/config.yaml` and loaded automatically at server startup: - - scheme: `openclient-llm` - - simulator: `iPhone 17 Pro Max` - - If `projectPath` is missing or wrong, use project discovery and set it with `mcp_xcodebuildmcp_session_set_defaults`. - - Only override other values if they are missing or wrong. - -2. **Run all tests** by calling `mcp_xcodebuildmcp_test_sim` with `-test-timeouts-enabled YES`, `-maximum-test-execution-time-allowance 120`, `CODE_SIGN_IDENTITY=""`, and `CODE_SIGNING_REQUIRED=NO` as extra arguments. -3. **Report results**: list every test case with pass/fail status. -4. **If any test fails**: - - Investigate the failure by reading the relevant test and source files. - - Fix the issue in the source code (not in the test, unless the test itself is wrong). - - Call `mcp_xcodebuildmcp_test_sim` again to confirm the fix. - - Repeat until all tests pass. -5. **Report final count**: total tests, passed, failed. - ---- - -## Path B — XcodeBuildMCP not available (fallback) - -1. **Run all tests** using the shell command below. -2. **Report results**: list every test case with pass/fail status. -3. **If any test fails**: - - Investigate the failure by reading the relevant test and source files. - - Fix the issue in the source code (not in the test, unless the test itself is wrong). - - Re-run the command to confirm the fix. - - Repeat until all tests pass. -4. **Report final count**: total tests, passed, failed. - -```bash -set -o pipefail -xcodebuild test \ - -project openclient-llm.xcodeproj \ - -scheme openclient-llm \ - -destination 'platform=iOS Simulator,name=iPhone 17 Pro Max' \ - -test-timeouts-enabled YES \ - -maximum-test-execution-time-allowance 120 \ - CODE_SIGN_IDENTITY="" \ - CODE_SIGNING_REQUIRED=NO \ - 2>&1 | tee /tmp/xcodebuild_test.txt -``` - -This single command: -- Runs the full test suite without `-quiet` (so all output is available) -- Preserves and displays the complete output while returning the real `xcodebuild` status -- Saves the full output to `/tmp/xcodebuild_test.txt` for inspection if needed - -To read failed test details after the run: -```bash -grep -A 5 "failed" /tmp/xcodebuild_test.txt -``` - ---- - -## Rules - -- Never skip or disable a failing test to make the suite pass -- Never use `--no-verify` or equivalent flags to bypass checks -- If a fix requires changing shared code, ensure it doesn't break other features -- Report the final count: total tests, passed, failed diff --git a/.gitignore b/.gitignore index 5d37f6a0..7a47b8cc 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,6 @@ Secrets.xcconfig # VS Code .vscode/ + +# Remote config +config.json diff --git a/.opencode/skills/test-coverage/SKILL.md b/.opencode/skills/test-coverage/SKILL.md new file mode 100644 index 00000000..60ba9ed4 --- /dev/null +++ b/.opencode/skills/test-coverage/SKILL.md @@ -0,0 +1,43 @@ +--- +name: test-coverage +description: Use when adding unit tests, improving test coverage, creating XCTest cases, or adding test mocks for existing openclient-llm Swift code. +--- + +# Test Coverage + +Use this skill to add meaningful XCTest coverage for existing behavior. Read `AGENTS.md` and +`specs/testing.instructions.md` before editing; also read the specifications governing the source under test. + +## Scope + +If the requested target or desired depth is unclear, ask one concise question. Otherwise infer the smallest useful scope +from the request and nearby tests. Do not demand a coverage percentage when critical paths are evident. + +## Process + +1. Read the source under test and its direct dependencies. Identify public behavior, state transitions, errors, and + meaningful boundaries. +2. Search `openclient-llm-test/` for existing coverage and reusable mocks before adding files. +3. Add focused tests at the smallest useful boundary. Prioritize changed or risky behavior over exhaustive permutations. +4. Mirror the source organization under `openclient-llm-test/Features/` or `openclient-llm-test/Core/`. +5. Reuse protocol-backed mocks. Add a shared mock under `openclient-llm-test/Mocks/` only when multiple tests benefit; + keep a one-off helper private to its test file. +6. Follow the repository's Given-When-Then structure, test naming, file header, `@MainActor`, and concurrency rules. +7. Ask the user before running tests unless their request already explicitly includes verification. When approved, load + `xcode-verify` and run the smallest relevant test set first. + +## Coverage Guidance + +- **ViewModels:** observable Event-to-State transitions, async loading behavior, coordination, and recoverable errors. +- **UseCases:** business rules, transformations, dependency failures, and meaningful edge cases. +- **Repositories:** mapping, persistence, caching, and invalidation behavior with external boundaries mocked. +- **Managers and parsers:** public contracts, malformed input, invariants, and round trips when applicable. + +Do not require a test file for a pass-through type with no independent behavior. Do not test private methods directly, +add delays, call real services, or weaken production design solely to make testing convenient. Every test must be capable +of failing when its covered behavior regresses. + +## Completion + +Report the behavior covered, files added or changed, and any important gaps. If tests ran, report their result; otherwise +state that verification awaits approval. diff --git a/.opencode/skills/xcode-verify/SKILL.md b/.opencode/skills/xcode-verify/SKILL.md index e73813a5..3b794dab 100644 --- a/.opencode/skills/xcode-verify/SKILL.md +++ b/.opencode/skills/xcode-verify/SKILL.md @@ -1,26 +1,48 @@ --- name: xcode-verify -description: Use when building, linting, or verifying the openclient-llm Xcode project after Swift changes or when SwiftLint warnings/errors must be fixed. +description: Use when building, running, testing, linting, or verifying the openclient-llm Xcode project, including fixing compiler, test, or SwiftLint failures. --- # Xcode Verification -Use this skill to verify Swift changes in `openclient-llm`. +Use this skill for Xcode operations in `openclient-llm`. After implementation work, ask the user before compiling, +checking SwiftLint, or running tests, as required by `AGENTS.md`. A direct request to build, run, lint, or test is approval +for that requested operation. -## Process +## Setup 1. Before the first XcodeBuildMCP build or test call, use `session_show_defaults`. 2. Before building, create `Secrets.xcconfig` from the template in `AGENTS.md` if it is missing; never overwrite it. -3. Prefer XcodeBuildMCP. Use `build_sim` for compilation and `test_sim` for tests, with code signing disabled; add the documented test timeout arguments for test runs. -4. If an MCP request times out, use the complete `xcodebuild` fallback from `AGENTS.md`, including `-project`, code-signing overrides, and test timeouts. -5. Read every compiler error and SwiftLint warning in context before changing code. -6. Fix the root cause of every SwiftLint violation. Never disable rules, add `swiftlint:disable`, or modify `.swiftlint.yml` unless the user explicitly asks. -7. Run affected tests after each fix. Run the full iOS test suite after changes to shared code or before reporting completion. -8. Run `git diff --check` before completion. +3. Prefer XcodeBuildMCP and use the project, scheme, and simulator defaults from `.xcodebuildmcp/config.yaml`. Only repair + defaults that are missing or wrong. +4. Disable code signing for builds and tests. Add the test timeout arguments documented in `AGENTS.md` for test runs. +5. If an MCP request fails or times out, use the complete `xcodebuild` fallback from `AGENTS.md`. + +## Operations + +- **Build iOS:** use `build_sim`. +- **Run iOS:** use `build_run_sim`; it boots, installs, and launches the app without separate simulator setup. +- **Build macOS:** use `build_macos`. +- **Run macOS:** use `build_run_macos`. +- **Focused tests:** use `test_sim` with `-only-testing` for the smallest relevant test class or target. +- **Full tests:** use `test_sim` for the complete iOS suite. Shared-code changes require the full suite when verification + is approved. +- **SwiftLint:** inspect build output because SwiftLint runs as part of the app builds. + +Do not erase or reset simulators. Do not modify source code merely to run the app unless the user also asks to fix a +failure. + +## Failure Handling + +1. Read every compiler error, failed test, and SwiftLint warning in context before changing code. +2. Fix root causes rather than suppressing checks. Never skip tests, disable SwiftLint rules, add `swiftlint:disable`, or + modify `.swiftlint.yml` unless the user explicitly requests the configuration change. +3. After a fix, rerun the smallest operation that proves it. Run broader verification only when required by `AGENTS.md` + and approved by the user. +4. Run `git diff --check` before reporting completion after source changes. ## Completion Criteria -- The build has no compiler errors. -- SwiftLint reports no warnings or errors. -- Relevant tests pass; shared-code changes require the full suite. -- Report the test total, failures, and any verification that could not run. +- Report exactly which builds, launches, lint checks, or tests ran and their result. +- For tests, report the available total and failures without listing every passing test unless requested. +- State any verification that could not run and why. diff --git a/AGENTS.md b/AGENTS.md index 52e7e6b4..7d6c4eb2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,9 +23,9 @@ Each specification must use the `.instructions.md` suffix and start with YAML fr | `concurrency.instructions.md` | Working with async code, isolation, or `Sendable`. | | `conversation-backup-format.instructions.md` | Exporting, importing, restoring, validating, or versioning conversation backups. | | `design-ui.instructions.md` | Designing general SwiftUI UI, accessibility, haptics, or animation. | +| `icloud-sync.instructions.md` | Implementing or changing iCloud synchronization, storage, conflict resolution, or cloud data management. | | `litellm-api.instructions.md` | Changing LiteLLM/OpenAI-compatible API integration. | | `readme.instructions.md` | Updating `README.md`. | -| `roadmap.instructions.md` | Planning or prioritizing future work. | | `roadmap-completed.instructions.md` | Reviewing completed roadmap work. | | `security.instructions.md` | Handling sensitive data, user input, credentials, or security review. | | `swiftui-multiplatform.instructions.md` | Building shared iOS, iPadOS, or macOS SwiftUI. | @@ -71,7 +71,9 @@ main-actor isolation when compiled into either app. The test, Share Extension, a - `@MainActor` annotations on ViewModels are redundant but kept for documentation. - All test classes **must** be `@MainActor` — otherwise they cannot access `@MainActor`-isolated types synchronously. -- Use `nonisolated` ONLY for genuinely background work (image processing, large JSON parsing). +- Use `nonisolated` only when a declaration must run or be constructed outside `MainActor` and its dependencies and + transferred values are safe across isolation boundaries. Common cases include shared DTOs, parsing helpers, constants + required by nonisolated protocols, and genuinely background processing. Do not use it for UI-bound state. - `@unchecked Sendable` requires a documented safety invariant comment — never use without justification. - Production wrappers: `// Safety: is thread-safe per Apple documentation. All stored properties are immutable (\`let\`).` - Test mocks: `// Safety: Only used within serialized @MainActor test methods.` @@ -135,10 +137,13 @@ final class FeatureViewModel { } ``` -- Views access ViewModels via `@State private var viewModel = FeatureViewModel()`. +- Root screen views generally own `@Observable` ViewModels with `@State`. Custom initialization and internal visibility are + allowed for split `Type+Concern.swift` implementations; child views may receive the same ViewModel and use `@Bindable` + when bindings are required. - ViewModels primarily coordinate UseCases, but some also inject Managers directly for settings, memory, cloud sync, user profile, and app-wide routing state. Preserve the local pattern instead of adding pass-through UseCases. -- ViewModel `send(_:)` is the public input point; asynchronous work and state mutation stay inside the ViewModel. +- ViewModel `send(_:)` is the primary UI event entry point. Preserve established explicit methods such as awaitable refresh + APIs where the surrounding feature already uses them. Asynchronous ownership and state mutation stay inside the ViewModel. - Typical data flow is View → ViewModel → UseCase → Repository → APIClient/LocalStorage. Managers are transversal services coordinated by UseCases, ViewModels, repositories, and app entry points where the implementation requires it. @@ -146,10 +151,13 @@ final class FeatureViewModel { - Every `.swift` file starts with the boilerplate copyright header (see any existing file). - One public type per file, named after the type. -- `// MARK: - Properties` / `// MARK: - Init` / `// MARK: - ` / `// MARK: - Private` at file bottom. -- Every SwiftUI view file must include `#Preview`. +- Use `// MARK: -` sections where they improve navigation. Common sections are `Properties`, `Init`, a meaningful public + section such as `View` or `Input functions`, and `Private` near the bottom; do not force sections into small files. +- Primary SwiftUI screens and reusable visual components need preview coverage, either in the same file or a dedicated + `Type+Previews.swift` file. Platform adapters and infrastructure-only views may rely on a composed parent preview. - Never initialize optional stored properties with `= nil` (optionals default to nil). -- Use `String(localized:)` for ALL user-facing strings. Never manually edit `Localizable.xcstrings` — Xcode syncs it from `String(localized:)` usage. +- Localize all user-facing source strings. Use `String(localized:)` when an API requires `String`; direct localized literals + are valid for APIs taking `LocalizedStringKey` or `LocalizedStringResource`. Never manually edit `Localizable.xcstrings`. - Write localized source strings in English only; translations are maintained manually by the project author. - SwiftLint configuration: warnings/errors are 120/150 lines for line length, 50/80 for function bodies, 300/400 for type bodies, and 500/650 for files. `force_unwrapping` and `force_cast` are errors. @@ -164,7 +172,7 @@ final class FeatureViewModel { as client configuration, not as confidential server-side secrets; never place a privileged credential there. - Release workflows derive tag and artifact labels from the first numeric `CHANGELOG.md` header. The deployment process increments the published build number automatically, so the checked-in `CURRENT_PROJECT_VERSION` does not need to match - the changelog build suffix. Tags use the full changelog version prefixed with `v`. + the changelog build suffix. The standard release tag is `v`; the macOS DMG tag is `v-macos`. ## Change Completion diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 39c82616..2a60d25d 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -104,7 +104,10 @@ Widgets/ # WidgetsExtension target (iOS 26+) │ ├── NewChatWidget.swift │ ├── SearchWidget.swift │ ├── QuickActionsWidget.swift -│ └── ConversationsOverviewWidget.swift +│ ├── ConversationsOverviewWidget.swift +│ ├── LatestConversationWidget.swift +│ ├── PinnedConversationsWidget.swift +│ └── TaggedConversationsWidget.swift └── Resources/ openclient-llm-test/ # Unit tests @@ -143,7 +146,7 @@ SwiftLintPlugins, VoticeSDK, and ConfettiSwiftUI. - **`openclient-llm/`** (outside Shared) — iOS/iPadOS-specific views, app entry point, iOS resources. - **`openclient-llm-macOS/`** — macOS-specific views, app entry point, macOS resources. No shared logic duplicated here. - **`ShareExtension/`** — iOS/iPadOS Share Extension. It owns compatible write-side payload/store types; the main app owns the read side. They exchange JSON and attachments through `group.com.artcc.openclient-llm`; the extension does not link `Shared/`. -- **`Widgets/`** — Source folder for `WidgetsExtension` (iOS 26+), containing four home-screen widgets and a Control Center control. `AppGroupStore` and `WidgetConversation` compile into both apps and the extension; `WidgetControlStore` compiles into the iOS app and extension. The remaining widget UI does not link the shared feature layer. +- **`Widgets/`** — Source folder for `WidgetsExtension` (iOS 26+), containing seven home-screen widgets and a Control Center control. `AppGroupStore` and `WidgetConversation` compile into both apps and the extension; `WidgetControlStore` compiles into the iOS app and extension. The remaining widget UI does not link the shared feature layer. - **`#if os(iOS)` / `#if os(macOS)`** — Used inside shared views for platform-specific UI variations. The iOS and macOS app targets set `SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor`. The test and extension targets do not; diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e0d0663..afda181d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Contributions are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. +## [1.6.15-build-71] - 2026-08-13 + +### Added + +- **iCloud data management** — Settings now shows a privacy-safe inventory and supports durable deletion of individual synchronized records or all cloud data +- **iCloud runtime status** — Settings now reports checking, synchronizing, pending-download, unavailable, failed, and last-successful synchronization states +- **iCloud account-change protection** — synchronization requires explicit approval before associating local data with a different iCloud account + +### Changed + +- **Complete iCloud reconciliation** — conversations, attachments, personal context, memory, and prompt templates now use consistent conflict, recovery, deletion-marker, and purge semantics +- **Cloud synchronization lifecycle** — automatic observation, foreground refresh, manual synchronization, and enablement preflight now share one authoritative runtime coordinator + +### Removed + +- Default system prompt restriction that discouraged structured response formats unless explicitly requested + +### Fixed + +- Stale devices can no longer resurrect records removed by individual deletion, delete-all markers, or global purge operations +- Equal-revision conflicts preserve losing representations for recovery and converge deterministically across devices +- iCloud serialization now preserves canonical date precision across local files, cloud files, deletion markers, and retries +- Pending iCloud downloads and partial category failures no longer overwrite valid local or cloud data + ## [1.6.10-build-67] - 2026-08-09 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ccf2fe8c..81ce6071 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,7 +37,8 @@ xcodebuild build -project openclient-llm.xcodeproj -scheme openclient-llm \ -destination 'platform=iOS Simulator,name=iPhone 17 Pro Max' ``` -If you use VS Code with the **XcodeBuildMCP** extension, the `build-lint`, `run-app`, and `run-tests` agent prompts handle build, launch, and testing automatically — with or without MCP installed. +When using OpenCode with **XcodeBuildMCP**, the project skills under `.opencode/skills/` guide builds, launches, tests, +SwiftLint fixes, test coverage, and opt-in LiteLLM integration checks. ## How to Contribute diff --git a/README.md b/README.md index fcdd2f5c..6ba4abe5 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,57 @@

- OpenClient + + OpenClient — Native Apple Client for OpenAI-compatible APIs +

-

OpenClient

-

License Platform Swift - SwiftUI - Version 1.6.10 - Xcode + Version 1.6.15

-## Description - OpenClient connects directly to the AI server you configure, without an OpenClient-hosted proxy or subscription. Requests may still reach providers configured behind your server, and the optional in-app feedback screen uses Votice. It works with [LiteLLM](https://github.com/BerriAI/litellm), [Ollama](https://ollama.com), and OpenAI-compatible servers that provide the endpoints used by your selected features; point the app at your URL and use the models it exposes. +

+ Website +  ·  + App Store +  ·  + macOS Releases +

+ +

+ + Download on the App Store + +

+ +## Screenshots + +

+ Models   + New Chat   + Chat   + Chats +

+ +## Highlights + +| | | +|---|---| +| **Your server** | Connect to LiteLLM, Ollama, or another server that implements the OpenAI-compatible endpoints you use. | +| **Rich conversations** | Stream Markdown, attach images and PDFs, use speech features, and generate images with compatible models. | +| **Tools and agents** | Use server-configured web search, function calling, and MCP tools exposed through LiteLLM. | +| **Apple ecosystem** | Sync with iCloud and use Share Sheet, Shortcuts, widgets, Control Center, and the macOS menu bar companion. | +| **Open source** | Review, modify, and contribute to the complete project under the GNU AGPL v3.0. | + +## Features + **Chat** - Real-time streaming responses with Markdown and code block rendering - Collapsible Thinking block for reasoning models (DeepSeek, o1, Gemini Thinking, and more) @@ -42,41 +73,29 @@ servers that provide the endpoints used by your selected features; point the app - Deep-link into the app with `openclient://chat?text=…`, `openclient://chat?url=…`, or `openclient://conversation?id=…` for third-party automation - Apple Shortcuts integration: "New Chat", "Search Chats", and "Send File to Chat" actions available in the Shortcuts app and via Siri - Control Center toggle: add a "New Chat" button for instant one-tap access from any screen or the lock screen -- Home-screen widgets: New Chat (small), Search (small), Quick Actions (medium), and Recent Conversations (medium/large) — tap any widget to jump directly into the app -- iCloud sync across all your Apple devices +- Home-screen widgets: New Chat and Search (small); Quick Actions and Continue Chat (medium); and Recent, Pinned, or Tagged Conversations (medium/large) +- Optional iCloud sync for conversations, attachments, personal context, memory, and custom prompt templates across supported iPhone, iPad, and Mac devices - Export individual conversations or full JSON backups, and restore backups on another device ([format specification](specs/conversation-backup-format.instructions.md)) - Private Chat: start a session-only chat whose messages and attachments are discarded when you close it; personal memory is neither read nor changed -- Token usage per message and estimated conversation cost +- Token usage per message and estimated conversation cost when the backend returns usage and pricing metadata **Models** -- Browse all available models with capability badges (vision, tools, JSON mode, image generation...) -- Model detail sheet: context window, pricing (per token), provider, mode, and capabilities at a glance +- Browse available models with capability badges supplied by `/model/info`, plus best-effort Ollama capability detection +- Model detail sheet showing available context, pricing, provider, mode, and capability metadata - Voice selector for Text-to-Speech models - Switch models per conversation **Personalization** - Prompt template library: save and reuse system prompts for any workflow - User profile: set your name and context so every model addresses you personally -- Memory: save facts and preferences (manually or let the model save them automatically); injected into every conversation's system prompt and synced via iCloud +- Memory: save facts and preferences (manually or let the model save them automatically); injected into every conversation's system prompt and synced when iCloud synchronization is enabled +- Localized in English, Spanish, French, Italian, German, Portuguese (Portugal), Japanese, Dutch, Greek, and Swedish **macOS** - Menu bar companion for instant access without opening the main window -🌐 [Project website](https://www.arturocarreterocalvo.com/openclient-llm/) - -[Download on the App Store](https://apps.apple.com/us/app/id6761379499) - **macOS:** Download the latest signed and notarized `.dmg` directly from the [Releases](https://github.com/artcc/openclient-llm/releases) page. -## Screenshots - -

- Models   - New Chat   - Chat   - Chats -

- ## Technologies | Technology | Purpose | @@ -91,7 +110,7 @@ servers that provide the endpoints used by your selected features; point the app | ConfettiSwiftUI | Tip-jar celebration effect | | SF Symbols | Iconography | | AppIntents | Apple Shortcuts, Siri & Control Center integration | -| WidgetKit | Control Center toggle and home-screen widgets (New Chat, Search, Quick Actions, Recent Conversations) | +| WidgetKit | Control Center toggle and seven home-screen widgets for actions, recent chats, pins, and tags | | Votice | In-app feedback & feature requests | This project was developed entirely with Xcode, Visual Studio Code and GitHub Copilot (with Claude Opus / Sonnet 4.6). @@ -128,7 +147,10 @@ See [ARCHITECTURE.md](ARCHITECTURE.md) for the full project tree and layer respo - **Ollama** (direct): `http://your-server:11434/v1` OpenClient appends API paths to this value without adding or removing `/v1`: LiteLLM uses paths such as `/models` and `/chat/completions`, while direct Ollama requires its `/v1` OpenAI-compatible base. -5. **Run** on your device or simulator + If your server requires authentication, enter its API key in the app; OpenClient stores it in Keychain and sends it as + a Bearer token. This server credential is separate from the Votice values in `Secrets.xcconfig`. +5. **Run** with the `openclient-llm` scheme for iOS/iPadOS or `openclient-llm-macOS` for macOS, selecting the corresponding + device, simulator, or Mac destination. ### Requirements @@ -136,6 +158,8 @@ See [ARCHITECTURE.md](ARCHITECTURE.md) for the full project tree and layer respo - iOS 26+ / macOS 26+ - A running [LiteLLM](https://docs.litellm.ai/) server — recommended backend; proxies [Ollama](https://ollama.com) and cloud providers (OpenAI, Anthropic, Google…) under a single endpoint. See [LiteLLM.md](LiteLLM.md). - **Or** a running [Ollama](https://ollama.com) instance directly (OpenAI-compatible `/v1` endpoint). See [Ollama.md](Ollama.md). Note: using Ollama through LiteLLM is preferred as it unlocks multi-provider support, virtual keys, and cost tracking. +- **Or** another OpenAI-compatible server that implements `GET /models` and `POST /chat/completions`; endpoints for + optional features are required only when those features are used. ### Self-hosting guides @@ -168,11 +192,6 @@ To suggest features or report bugs from within the app, go to **Settings** and u - [GitHub Profile](https://github.com/ArtCC) -## Your AI. Your server. Your rules - -

- OpenClient is built on the belief that generative AI should be something you control — not something that controls your data.
- Run local models entirely on your own hardware, or route cloud providers through your own self-hosted proxy.
- Either way, you choose the server and any upstream providers it uses. OpenClient does not relay AI requests through an OpenClient-operated service.

- Open source. No advertising tracking. Full control. -

\ No newline at end of file +

+ Your AI. Your server. Your rules. +

diff --git a/TestFlight/WhatToTest.en-US.txt b/TestFlight/WhatToTest.en-US.txt new file mode 100644 index 00000000..2e175176 --- /dev/null +++ b/TestFlight/WhatToTest.en-US.txt @@ -0,0 +1,23 @@ +Hi there! We've got some great new features for you in this update. + +***1.6.15: + +• Major improvements to iCloud conversation syncing, especially when switching between devices or returning to the app. +• New section in Settings to manage iCloud data. +• General stability, reliability, and performance improvements. +• Minor bug fixes and improvements for a smoother experience. + +***Recent Updates: + +• You can now generate images directly in chat with supported models. +• We’ve added update notifications so you can easily keep the app up to date. +• The app can now display important announcements and news directly on the Home screen. +• We’ve improved how maintenance periods are handled, providing a clearer experience when the service is temporarily unavailable. +• RAW images selected from Photos are now processed correctly before being sent. +• Major improvements to iCloud conversation syncing, especially when switching between devices or returning to the app. +• Fixed several issues with conversations changed or deleted on other devices. +• General stability, reliability, and performance improvements. + +Thanks for your continued support and for helping us build the best possible LLM client together. + +Remember: you can suggest and vote on new features from the Feedback section in Settings. diff --git a/Widgets/App/Widgets/ConversationsOverviewWidget.swift b/Widgets/App/Widgets/ConversationsOverviewWidget.swift index ffd82b19..5c945e9a 100644 --- a/Widgets/App/Widgets/ConversationsOverviewWidget.swift +++ b/Widgets/App/Widgets/ConversationsOverviewWidget.swift @@ -80,6 +80,7 @@ private struct ConversationsOverviewWidgetView: View { VStack(alignment: .leading, spacing: 0) { header Divider() + .padding(.vertical, 5) if entry.conversations.isEmpty { emptyState } else { @@ -98,7 +99,7 @@ private extension ConversationsOverviewWidgetView { .font(.headline) .foregroundStyle(.primary) Spacer() - HStack(spacing: 4) { + HStack(spacing: 5) { if let newChatURL = URL(string: "openclient://new-chat") { Link(destination: newChatURL) { ZStack { @@ -125,7 +126,8 @@ private extension ConversationsOverviewWidgetView { } } } - .padding(2.5) + .frame(height: 30) + .padding(.horizontal, 2.5) } var emptyState: some View { diff --git a/Widgets/App/Widgets/LatestConversationWidget.swift b/Widgets/App/Widgets/LatestConversationWidget.swift index 086681cf..97640a96 100644 --- a/Widgets/App/Widgets/LatestConversationWidget.swift +++ b/Widgets/App/Widgets/LatestConversationWidget.swift @@ -72,6 +72,7 @@ private struct LatestConversationWidgetView: View { VStack(alignment: .leading, spacing: 0) { header Divider() + .padding(.vertical, 5) if let conversation = entry.conversation { conversationCard(conversation) } else { @@ -94,7 +95,8 @@ private extension LatestConversationWidgetView { .foregroundStyle(.primary) Spacer() } - .padding(2.5) + .frame(height: 30) + .padding(.horizontal, 2.5) } @ViewBuilder diff --git a/Widgets/App/Widgets/PinnedConversationsWidget.swift b/Widgets/App/Widgets/PinnedConversationsWidget.swift index 6fde3c7b..ecf323c9 100644 --- a/Widgets/App/Widgets/PinnedConversationsWidget.swift +++ b/Widgets/App/Widgets/PinnedConversationsWidget.swift @@ -74,6 +74,7 @@ private struct PinnedConversationsWidgetView: View { VStack(alignment: .leading, spacing: 0) { header Divider() + .padding(.vertical, 5) if entry.conversations.isEmpty { emptyState } else { @@ -96,7 +97,8 @@ private extension PinnedConversationsWidgetView { .foregroundStyle(.primary) Spacer() } - .padding(2.5) + .frame(height: 30) + .padding(.horizontal, 2.5) } var emptyState: some View { diff --git a/Widgets/App/Widgets/TaggedConversationsWidget.swift b/Widgets/App/Widgets/TaggedConversationsWidget.swift index c76d8775..541a52d4 100644 --- a/Widgets/App/Widgets/TaggedConversationsWidget.swift +++ b/Widgets/App/Widgets/TaggedConversationsWidget.swift @@ -86,6 +86,7 @@ private struct TaggedConversationsWidgetView: View { VStack(alignment: .leading, spacing: 0) { header Divider() + .padding(.vertical, 5) if entry.conversations.isEmpty { emptyState } else { @@ -121,7 +122,8 @@ private extension TaggedConversationsWidgetView { .lineLimit(1) Spacer() } - .padding(2.5) + .frame(height: 30) + .padding(.horizontal, 2.5) } var emptyState: some View { diff --git a/docs/assets/og-image.png b/docs/assets/og-image.png new file mode 100644 index 00000000..717f0cd9 Binary files /dev/null and b/docs/assets/og-image.png differ diff --git a/docs/css/style.css b/docs/css/style.css index 5be64da2..e36467b4 100644 --- a/docs/css/style.css +++ b/docs/css/style.css @@ -7,13 +7,14 @@ --text: #1d1d1f; --muted: #6e6e73; --accent: #007AFF; + --accent-cyan: #00B8D9; --card-bg: rgba(255, 255, 255, 0.72); --card-border: rgba(0, 0, 0, 0.07); --badge-bg: rgba(0, 0, 0, 0.06); --badge-text: #444; --section-alt-bg: rgba(0, 0, 0, 0.025); --header-bg: rgba(245, 245, 247, 0.82); - --hero-glow: radial-gradient(ellipse 55% 40% at 50% 0%, rgba(0, 122, 255, 0.16) 0%, transparent 72%); + --hero-glow: radial-gradient(ellipse 60% 44% at 50% 0%, rgba(0, 184, 217, 0.17) 0%, transparent 72%); --hero-grid: rgba(0, 122, 255, 0.09); } @@ -23,13 +24,14 @@ --text: #f5f5f7; --muted: #98989d; --accent: #0A84FF; + --accent-cyan: #00D7E8; --card-bg: rgba(255, 255, 255, 0.06); --card-border: rgba(255, 255, 255, 0.09); --badge-bg: rgba(255, 255, 255, 0.09); --badge-text: #ccc; --section-alt-bg: rgba(255, 255, 255, 0.03); --header-bg: rgba(0, 0, 0, 0.75); - --hero-glow: radial-gradient(ellipse 55% 40% at 50% 0%, rgba(10, 132, 255, 0.22) 0%, transparent 72%); + --hero-glow: radial-gradient(ellipse 60% 44% at 50% 0%, rgba(0, 215, 232, 0.2) 0%, transparent 72%); --hero-grid: rgba(10, 132, 255, 0.12); } } @@ -242,7 +244,7 @@ a:focus-visible { z-index: -1; inset: -22px; border-radius: 42px; - background: var(--accent); + background: linear-gradient(135deg, var(--accent), var(--accent-cyan)); content: ""; filter: blur(30px); opacity: 0.25; @@ -275,7 +277,7 @@ h1 { } .hero-gradient { - background: linear-gradient(135deg, var(--text) 0%, var(--accent) 100%); + background: linear-gradient(110deg, var(--text) 8%, var(--accent) 58%, var(--accent-cyan) 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; @@ -283,7 +285,7 @@ h1 { @media (prefers-color-scheme: dark) { .hero-gradient { - background: linear-gradient(135deg, #f5f5f7 0%, #0A84FF 100%); + background: linear-gradient(110deg, #f5f5f7 8%, #0A84FF 58%, #00D7E8 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; @@ -331,20 +333,28 @@ h1 { box-shadow: 0 12px 24px rgba(0, 122, 255, 0.2); } -.cta-github { - background: #24292e !important; - color: #fff !important; +.cta-group a.cta-primary { + position: relative; + z-index: 1; + border-radius: 10px; + filter: drop-shadow(0 10px 18px rgba(0, 122, 255, 0.22)); } -.cta-github:hover { - box-shadow: 0 12px 24px rgba(36, 41, 46, 0.22) !important; +.cta-group a.cta-primary:hover { + filter: drop-shadow(0 14px 24px rgba(0, 184, 217, 0.3)); } -@media (prefers-color-scheme: dark) { - .cta-github { - background: #f0f6fc !important; - color: #24292e !important; - } +.cta-github { + background: var(--card-bg) !important; + color: var(--text) !important; + border-color: var(--card-border) !important; + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); +} + +.cta-github:hover { + border-color: rgba(0, 122, 255, 0.25) !important; + box-shadow: 0 12px 24px rgba(0, 122, 255, 0.12) !important; } .cta-group a.cta-appstore-badge { @@ -367,19 +377,17 @@ h1 { } .cta-testflight { - background: #fff !important; - color: #1a1a1a !important; - border: 2px solid #3A8FE8 !important; + background: transparent !important; + color: var(--muted) !important; + border-color: var(--card-border) !important; gap: 10px; - padding: 0 20px !important; + padding: 0 16px !important; } -@media (prefers-color-scheme: dark) { - .cta-testflight { - background: #111 !important; - color: #fff !important; - border-color: #3A8FE8 !important; - } +.cta-testflight:hover { + color: var(--text) !important; + border-color: rgba(0, 122, 255, 0.22) !important; + box-shadow: none !important; } .cta-testflight-logo { @@ -486,6 +494,18 @@ h1 { transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease; } +.feature-primary { + padding: 28px 24px; + background: linear-gradient(145deg, var(--card-bg), rgba(0, 184, 217, 0.055)); + border-color: color-mix(in srgb, var(--icon-color) 22%, var(--card-border)); + box-shadow: 0 16px 42px rgba(0, 122, 255, 0.07); +} + +.feature-compact { + padding: 20px; + border-radius: 17px; +} + .feature::after { position: absolute; top: 0; @@ -515,11 +535,37 @@ h1 { width: 44px; height: 44px; border-radius: 12px; - font-size: 1.3rem; margin-bottom: 16px; background: var(--icon-bg); } +.feature-icon-wrap svg { + width: 23px; + height: 23px; + fill: none; + stroke: var(--icon-color); + stroke-width: 1.8; + stroke-linecap: round; + stroke-linejoin: round; +} + +.feature-primary .feature-icon-wrap { + width: 48px; + height: 48px; +} + +.feature-compact .feature-icon-wrap { + width: 38px; + height: 38px; + margin-bottom: 13px; + border-radius: 10px; +} + +.feature-compact .feature-icon-wrap svg { + width: 20px; + height: 20px; +} + .feature h3 { font-size: 0.95rem; font-weight: 600; @@ -533,6 +579,10 @@ h1 { line-height: 1.55; } +.feature-primary p { + font-size: 0.88rem; +} + @supports not (backdrop-filter: blur(1px)) { .feature { background: rgba(255, 255, 255, 0.95); @@ -559,8 +609,9 @@ h1 { .steps li { counter-increment: steps; - display: flex; - align-items: baseline; + display: grid; + grid-template-columns: 24px minmax(0, 1fr); + align-items: start; gap: 12px; font-size: 0.95rem; color: var(--muted); @@ -582,6 +633,11 @@ h1 { flex-shrink: 0; } +.steps li > span { + min-width: 0; + padding-top: 1px; +} + .steps a { color: var(--accent); text-decoration: none; diff --git a/docs/index.html b/docs/index.html index ca3e128a..60a207af 100644 --- a/docs/index.html +++ b/docs/index.html @@ -4,17 +4,62 @@ - OpenClient — Native Apple Client for OpenAI-compatible APIs + OpenClient — Private AI Chat for Apple Platforms + content="OpenClient is a native, open-source iOS, iPadOS, and macOS client for LiteLLM, Ollama, and OpenAI-compatible servers. Connect directly to your AI server."> + + - + - + content="A native, open-source Apple client for LiteLLM, Ollama, and OpenAI-compatible servers. Connect directly to your AI server."> + + + + + + + + + + + + + + @@ -28,7 +73,7 @@ OpenClient