diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 73859289..dea69645 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -223,3 +223,180 @@ jobs: path: test-results/e2e-safe-receipt.json if-no-files-found: error retention-days: 7 + + linux: + name: Linux ${{ matrix.arch }} + strategy: + fail-fast: false + matrix: + include: + - arch: x64 + runner: ubuntu-24.04 + - arch: arm64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} + timeout-minutes: 60 + steps: + - name: Check out source + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 + + - name: Use Node.js 22 + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 + with: + node-version: 22.22.3 + cache: npm + + - name: Install Linux test dependencies + run: sudo apt-get update && sudo apt-get install --yes rpm xvfb + + - name: Install locked dependencies + run: npm ci + + - name: Verify TypeScript and lint + run: npm run type-check && npm run lint + + - name: Run Pi extension, Linux contract, and native helper tests + run: npm run test:pi-extensions && npm run test:linux-contracts && npm run test:linux-native + + - name: Build, install, and verify Linux distributions + run: | + set -euo pipefail + npm run dist:linux + node scripts/verify-linux-package.mjs release/linux-distribution + sudo apt-get install --yes ./release/linux-distribution/*.deb + dpkg --verify aiden-agent + ! ldd "/opt/Aiden Agent/aiden-agent" | grep -q "not found" + test -f /usr/share/applications/com.sambitcreate.aiden-agent.desktop + grep -F 'StartupWMClass=com.sambitcreate.aiden-agent' /usr/share/applications/com.sambitcreate.aiden-agent.desktop + expected_version="$(node -p 'require("./package.json").version')" + test "$(aiden-agent --no-sandbox --version)" = "$expected_version" + chmod +x ./release/linux-distribution/*.AppImage + appimage_output="$(./release/linux-distribution/*.AppImage --appimage-extract-and-run --no-sandbox --version)" + grep -F "$expected_version" <<<"$appimage_output" + + - name: Prepare baseline-verified RPM for Fedora acceptance + if: matrix.arch == 'x64' + shell: bash + run: | + set -euo pipefail + shopt -s nullglob + rpm_files=(release/linux-distribution/*.rpm) + test "${#rpm_files[@]}" -eq 1 + rpm_name="$(basename "${rpm_files[0]}")" + ( + cd release/linux-distribution + sha256sum "$rpm_name" > rpm.sha256 + ) + + - name: Upload baseline-verified RPM for Fedora acceptance + if: matrix.arch == 'x64' + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: linux-rpm-x64-${{ github.run_id }}-${{ github.run_attempt }} + path: | + release/linux-distribution/*.rpm + release/linux-distribution/rpm.sha256 + if-no-files-found: error + retention-days: 1 + + - name: Smoke the installed GUI without a keyring session + shell: bash + run: | + set -euo pipefail + smoke_root="$RUNNER_TEMP/aiden-linux-smoke-${{ matrix.arch }}" + mkdir -p "$smoke_root/home" "$smoke_root/config" "$smoke_root/cache" "$smoke_root/data" + set +e + HOME="$smoke_root/home" \ + XDG_CONFIG_HOME="$smoke_root/config" \ + XDG_CACHE_HOME="$smoke_root/cache" \ + XDG_DATA_HOME="$smoke_root/data" \ + timeout --signal=KILL 15s xvfb-run --auto-servernum aiden-agent --no-sandbox \ + >"$smoke_root/output.log" 2>&1 + status=$? + set -e + # A living GUI is the success condition. Kill the whole timeout + # process group instead of asking Electron to perform a production + # shutdown inside an incomplete headless desktop session. + if [[ "$status" -ne 137 ]]; then + cat "$smoke_root/output.log" + exit 1 + fi + if grep -Eiq '(FATAL|symbol lookup error|error while loading shared libraries|Failed to start Aiden Agent)' "$smoke_root/output.log"; then + cat "$smoke_root/output.log" + exit 1 + fi + + - name: Run deterministic Electron E2E gate + if: matrix.arch == 'x64' + run: xvfb-run --auto-servernum npm run test:e2e + + - name: Build sanitized E2E failure receipt + if: ${{ failure() && matrix.arch == 'x64' }} + run: npm run diagnostics:failure-receipt -- test-results/e2e-safe-receipt.json electron-e2e test-failed + + - name: Upload sanitized E2E failure receipt + if: ${{ failure() && matrix.arch == 'x64' }} + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: playwright-e2e-linux-${{ github.run_id }}-${{ github.run_attempt }} + path: test-results/e2e-safe-receipt.json + if-no-files-found: error + retention-days: 7 + + linux-rpm: + name: Linux x64 · Fedora RPM + needs: linux + runs-on: ubuntu-24.04 + container: fedora:44 + timeout-minutes: 60 + steps: + - name: Install Fedora build prerequisites + run: dnf install --assumeyes git gcc gcc-c++ make python3 + + - name: Check out source + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 + + - name: Use Node.js 22 + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 + with: + node-version: 22.22.3 + cache: npm + + - name: Install locked dependencies + run: npm ci + + - name: Verify Fedora Pi extensions, contracts, and native helpers + run: npm run type-check && npm run test:pi-extensions && npm run test:linux-contracts && npm run test:linux-native + + - name: Download baseline-verified RPM + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e + with: + name: linux-rpm-x64-${{ github.run_id }}-${{ github.run_attempt }} + path: release/linux-rpm-smoke + + - name: Install and verify baseline-verified RPM package + run: | + set -euo pipefail + shopt -s nullglob + rpm_files=(release/linux-rpm-smoke/*.rpm) + test "${#rpm_files[@]}" -eq 1 + ( + cd release/linux-rpm-smoke + sha256sum --check rpm.sha256 + ) + expected_version="$(node -p 'require("./package.json").version')" + test "$(rpm -qp --queryformat '%{NAME} %{VERSION} %{ARCH}' "${rpm_files[0]}")" = \ + "aiden-agent $expected_version x86_64" + dnf install --assumeyes "${rpm_files[0]}" + node scripts/verify-linux-package.mjs "/opt/Aiden Agent" + rpm_verify_output="$(rpm --verify aiden-agent || true)" + if [ -n "$rpm_verify_output" ]; then + # electron-builder deliberately enables its setuid fallback when + # user namespaces do not work (including containerized CI). Keep + # every other RPM integrity difference fatal and prove the exact + # privileged file owner/mode before accepting that one transition. + test "$rpm_verify_output" = '.M....... /opt/Aiden Agent/chrome-sandbox' + test "$(stat -c '%a:%U:%G' '/opt/Aiden Agent/chrome-sandbox')" = '4755:root:root' + fi + ! ldd "/opt/Aiden Agent/aiden-agent" | grep -q "not found" + test "$(aiden-agent --no-sandbox --version)" = "$expected_version" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ab7a247c..82f1d00a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -name: Release macOS +name: Release desktop on: push: @@ -14,7 +14,7 @@ permissions: contents: write concurrency: - group: aiden-agent-macos-release + group: aiden-agent-desktop-release cancel-in-progress: false jobs: @@ -145,6 +145,171 @@ jobs: if: ${{ steps.version.outputs.publish == 'true' }} run: npm run test:e2e:diagnostics:packaged + - name: Stage verified macOS release assets + if: ${{ steps.version.outputs.publish == 'true' }} + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: release-macos + path: release/distribution + if-no-files-found: error + retention-days: 2 + + linux-release: + if: vars.RELEASES_ENABLED == 'true' + name: Linux ${{ matrix.arch }} release + strategy: + fail-fast: false + matrix: + include: + - arch: x64 + runner: ubuntu-24.04 + - arch: arm64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} + timeout-minutes: 90 + environment: release + steps: + - name: Check out source + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 + + - name: Use Node.js 22 + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 + with: + node-version: 22.22.3 + cache: npm + + - name: Resolve the declared release version + id: version + shell: bash + run: | + set -euo pipefail + base_version="$(node -p "require('./package.json').version")" + base_tag_match="$(git ls-remote --tags origin "refs/tags/v${base_version}")" + base_tag_exists=false + if [[ -n "$base_tag_match" ]]; then + base_tag_exists=true + fi + selection="$(node scripts/prepare-ci-release.mjs "$base_tag_exists")" + release_version="$(node -e 'process.stdout.write(JSON.parse(process.argv[1]).version)' "$selection")" + release_tag="$(node -e 'process.stdout.write(JSON.parse(process.argv[1]).tag)' "$selection")" + should_publish="$(node -e 'process.stdout.write(String(JSON.parse(process.argv[1]).publish))' "$selection")" + echo "version=$release_version" >> "$GITHUB_OUTPUT" + echo "tag=$release_tag" >> "$GITHUB_OUTPUT" + echo "publish=$should_publish" >> "$GITHUB_OUTPUT" + + - name: Install Linux package dependencies + if: ${{ steps.version.outputs.publish == 'true' }} + run: sudo apt-get update && sudo apt-get install --yes rpm xvfb + + - name: Install locked dependencies + if: ${{ steps.version.outputs.publish == 'true' }} + run: npm ci + + - name: Verify Linux contracts, diagnostics, and native helpers + if: ${{ steps.version.outputs.publish == 'true' }} + run: npm run type-check && npm run lint && npm run test:diagnostics && npm run test:linux-contracts && npm run test:linux-native + + - name: Build and verify Linux distributions + if: ${{ steps.version.outputs.publish == 'true' }} + run: | + set -euo pipefail + npm run models:refresh + npm run dist:linux + node scripts/verify-linux-package.mjs release/linux-distribution + sudo apt-get install --yes ./release/linux-distribution/*.deb + dpkg --verify aiden-agent + ! ldd "/opt/Aiden Agent/aiden-agent" | grep -q "not found" + expected_version="$(node -p 'require("./package.json").version')" + test "$(aiden-agent --no-sandbox --version)" = "$expected_version" + chmod +x ./release/linux-distribution/*.AppImage + appimage_output="$(./release/linux-distribution/*.AppImage --appimage-extract-and-run --no-sandbox --version)" + grep -F "$expected_version" <<<"$appimage_output" + + - name: Smoke the exact release GUI without a keyring session + if: ${{ steps.version.outputs.publish == 'true' }} + shell: bash + run: | + set -euo pipefail + smoke_root="$RUNNER_TEMP/aiden-linux-release-smoke-${{ matrix.arch }}" + mkdir -p "$smoke_root/home" "$smoke_root/config" "$smoke_root/cache" "$smoke_root/data" + set +e + HOME="$smoke_root/home" \ + XDG_CONFIG_HOME="$smoke_root/config" \ + XDG_CACHE_HOME="$smoke_root/cache" \ + XDG_DATA_HOME="$smoke_root/data" \ + timeout --signal=KILL 15s xvfb-run --auto-servernum aiden-agent --no-sandbox \ + >"$smoke_root/output.log" 2>&1 + status=$? + set -e + # A living GUI is the success condition. Kill the whole timeout + # process group instead of asking Electron to perform a production + # shutdown inside an incomplete headless desktop session. + if [[ "$status" -ne 137 ]]; then + cat "$smoke_root/output.log" + exit 1 + fi + if grep -Eiq '(FATAL|symbol lookup error|error while loading shared libraries|Failed to start Aiden Agent)' "$smoke_root/output.log"; then + cat "$smoke_root/output.log" + exit 1 + fi + + - name: Stage verified Linux release assets + if: ${{ steps.version.outputs.publish == 'true' }} + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: release-linux-${{ matrix.arch }} + path: | + release/linux-distribution/*.AppImage + release/linux-distribution/*.deb + release/linux-distribution/*.rpm + if-no-files-found: error + retention-days: 2 + + publish: + if: vars.RELEASES_ENABLED == 'true' + name: Publish verified desktop release + needs: + - release + - linux-release + runs-on: ubuntu-24.04 + timeout-minutes: 20 + environment: release + steps: + - name: Check out source + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 + + - name: Use Node.js 22 + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 + with: + node-version: 22.22.3 + + - name: Resolve release identity + id: version + shell: bash + run: | + set -euo pipefail + base_version="$(node -p "require('./package.json').version")" + base_tag_match="$(git ls-remote --tags origin "refs/tags/v${base_version}")" + base_tag_exists=false + if [[ -n "$base_tag_match" ]]; then + base_tag_exists=true + fi + selection="$(node scripts/prepare-ci-release.mjs "$base_tag_exists")" + release_version="$(node -e 'process.stdout.write(JSON.parse(process.argv[1]).version)' "$selection")" + release_tag="$(node -e 'process.stdout.write(JSON.parse(process.argv[1]).tag)' "$selection")" + should_publish="$(node -e 'process.stdout.write(String(JSON.parse(process.argv[1]).publish))' "$selection")" + echo "version=$release_version" >> "$GITHUB_OUTPUT" + echo "tag=$release_tag" >> "$GITHUB_OUTPUT" + echo "publish=$should_publish" >> "$GITHUB_OUTPUT" + + - name: Download verified desktop assets + if: ${{ steps.version.outputs.publish == 'true' }} + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e + with: + pattern: release-* + path: release/distribution + merge-multiple: true + - name: Publish verified release assets if: ${{ steps.version.outputs.publish == 'true' }} shell: bash @@ -152,6 +317,4 @@ jobs: GH_TOKEN: ${{ github.token }} RELEASE_TAG: ${{ steps.version.outputs.tag }} RELEASE_VERSION: ${{ steps.version.outputs.version }} - run: | - set -euo pipefail - bash scripts/publish-github-release.sh release/distribution + run: bash scripts/publish-github-release.sh release/distribution diff --git a/.papercuts/troubleshooting.md b/.papercuts/troubleshooting.md index 89a93140..df145f22 100644 --- a/.papercuts/troubleshooting.md +++ b/.papercuts/troubleshooting.md @@ -155,3 +155,24 @@ with a loopback Chromium debugging port, invoke the public action through the real preload bridge, use System Events to click the real native button by its exact accessibility label, and verify a post-start diagnostic event plus `uploadToServer: false`. + +## Linux desktop reconciliation + +- A long-lived platform branch can contain hundreds of duplicated feature commits while the platform port itself is one checkpoint. Preserve both histories in a merge commit, but build the result from the latest shared tree plus that checkpoint's intent; a normal textual merge lets stale parallel history overwrite newer features and multiplies conflicts. +- A Linux Electron artifact with native Node modules cannot be truthfully accepted from a macOS build host. Build and inspect it on native x64/arm64 Linux runners, then install the generated DEB/RPM and smoke the executable; cross-target metadata checks alone do not prove the active `node-pty`, Sherpa ONNX, or C-helper layout. +- Remote Access E2E must not assume Aiden's default private port is globally unused. Assert disabled state through the test process's own IPC snapshot, then derive the committed health-check port from that same installation after enablement so another running Aiden profile cannot create a false pass or failure. +- Native Linux acceptance can look green while exercising stale macOS-built outputs or skipping behind a Darwin-only guard. The Linux suite must build both production and test helpers, run the real adapter-to-helper boundary, and launch a freshly installed package from an empty XDG profile. +- A native generation token derived from `stat` is platform-shaped: Linux has no birth-time fields and emits seven components, while macOS emits nine. Validate those two exact wire shapes at the TypeScript boundary instead of assuming the macOS token or accepting every intermediate field count. +- Compiling native helpers on a new distro can silently raise the package's glibc floor even when the Electron shell still launches. Inspect every packaged helper, `.node`, and `.so` symbol table and fail packaging above the declared baseline; avoid C-library parsers whose symbol version was retargeted by newer libc headers when a bounded local parser is straightforward. +- Debian virtual packages can satisfy an unversioned `libasound2` dependency with an OSS compatibility shim that lacks Electron's required ALSA symbols. Use a versioned `libasound2t64 | libasound2` alternative so apt selects the real ALSA implementation on both time64 and older Debian families. +- The legacy arm64 AppImage launcher links against the unversioned development name `libz.so`, so a normal desktop with only `libz.so.1` silently fails before Electron starts. Pin electron-builder's static AppImage runtime and execute the release artifact on a clean target-architecture system; installing `zlib` development files would only hide the defect. +- A packaged dependency can retain Mach-O and PE files with a `.node` suffix beside the active Linux prebuild. Native compatibility verification must identify ELF magic before invoking `objdump`; the suffix alone is not an operating-system contract. +- macOS-to-Linux Docker source transfers can materialize AppleDouble `._*` metadata sidecars. Playwright must ignore those names explicitly or it will parse a `._*.spec.ts` binary sidecar as JavaScript even though the real tests and application build are healthy. +- A hermetic Electron E2E fixture can preserve `PATH` and locale yet still discard the display transport established by `xvfb-run`. On Linux, pass through only the X11/Wayland connection variables (`DISPLAY`, `XAUTHORITY`, `WAYLAND_DISPLAY`, `XDG_RUNTIME_DIR`, and `XDG_SESSION_TYPE`); when checking the child, account for the exact non-secret desktop/accessibility variables Chromium/GTK inject after launch while continuing to reject ambient credential variables. +- electron-builder's RPM post-install script changes `chrome-sandbox` from the archived `0755` mode to `4755` when user namespaces are unavailable, so a clean `rpm --verify` reports one expected mode difference in containers. Accept only that exact line after separately proving `4755:root:root`; any additional RPM verification output remains fatal. +- Fail-closed Linux secret storage must not make keyless local providers unusable. A keyless-to-keyless provider transition cannot expose or bind a secret, so save and portable-config reconciliation may bypass the secret backend for that exact case; any transition from or to a keyed connection still requires credential reconciliation. +- Electron's conventional Linux `window-all-closed` quit policy conflicts with an explicitly enabled background Remote Access listener. Let a synchronously observed running listener own application lifetime after the last window closes; normal Linux last-window close still quits when no background service is active, and an explicit Quit still performs the complete shutdown drain. +- A freshly authenticated Linux Tailscale daemon lets an unprivileged desktop app read status but rejects Serve writes until `sudo tailscale set --operator=$USER` is granted once. Recognize that exact CLI rejection only after confirming the Serve fingerprint stayed unchanged, then surface the remediation instead of collapsing it into an uncertain mutation. +- Probing a Linux Tailscale node's own MagicDNS HTTPS name from that same OrbStack VM can time out even while another tailnet peer reaches it immediately. Hold the scoped route open and probe from a separate peer when accepting the iOS network direction; always verify the temporary handler and listener are gone afterward. +- Darwin can deliver or coalesce a directory's already-queued creation notification after `fs.watch` registration. Before a watcher integration test acquires the lease whose invalidation it means to observe, establish a quiet baseline so a rapid follow-up edit is the event under test rather than a registration race. +- Playwright's `fill("")` and `clear()` use the same select-and-delete path as a manual text-clearing test, so swapping among those APIs does not avoid an intermittent Electron/Xvfb deletion miss. When the product benefits from it, expose an accessible clear action and exercise that real user path while retaining an exact empty-value assertion. diff --git a/AGENTS.md b/AGENTS.md index 65b7263e..94c8f0a4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,7 +20,7 @@ Text-entry controls must not add an accent border, outline, or ring when focused ## Release model metadata -models.dev may be contacted only by `npm run models:refresh`, the release refresh invoked by `npm run dist`, the scoped post-merge catalog workflow, or the user-initiated foreground **Update model catalogs** action in Settings → Providers. The live action may request only the fixed `https://models.dev/api.json` endpoint without credentials, cookies, prompts, chats, selections, custom endpoints, or a device identifier; its validated device-local cache is display-only and must never change runtime limits, routing, or selectable inventory. Never add a models.dev call to startup, normal development, unpacked builds, ordinary live-app reads, onboarding navigation, or background polling. Artificial Analysis data and credentials must never be bundled: the live Electron app may contact its fixed Free endpoint only after the user explicitly chooses Connect & fetch or Fetch latest with their own key, then reads the normalized device-local cache offline. +`npm run models:refresh` is the explicit development refresh, and `npm run dist` invokes the same release step before packaging. Those are the only paths that may contact models.dev. Never add a models.dev call to normal development, unpacked builds, or ordinary live-app reads. Artificial Analysis data and credentials must never be bundled: the live Electron app may contact its fixed Free endpoint only after the user explicitly chooses Connect & fetch or Fetch latest with their own key, then reads the normalized device-local cache offline. OpenRouter benchmark insights are also manual-only. The live app may contact only the fixed `/api/v1/benchmarks?source=artificial-analysis&max_results=100` endpoint after the user explicitly chooses Connect & fetch or Fetch latest, using the dedicated encrypted Model Pad credential rather than any inference-provider credential. Never send prompts or model traffic during that action, never import OpenRouter's model catalog, never bundle the returned data, and serve ordinary model-info reads only from the normalized device-local cache. diff --git a/README.md b/README.md index a7fd8ec1..ba04ad42 100644 --- a/README.md +++ b/README.md @@ -5,18 +5,22 @@
- A native-feeling macOS workspace for chatting with local or hosted AI models and safely working inside the folders you choose. + A native-feeling desktop workspace for chatting with local or hosted AI models and safely working inside the folders you choose.
```sh brew install --cask sambitcreate/tap/aiden-agent ``` +Linux x64 and arm64 downloads are available as AppImage, `.deb`, and `.rpm` +packages from [GitHub Releases](https://github.com/sambitcreate/aiden-agent/releases). +See the [Linux install and compatibility guide](docs/linux.md). +  ## Why Aiden -I don't come from a coding background. I'd been bouncing between the coding agents that exist, and each one had a piece of what I wanted without any of them being the whole thing. I loved **Codex** for its restrained, lovely desktop UX, **Opencode** for letting me bring whatever model and provider I wanted and also looking great in both the terminal and desktop, and **Cursor** for its nimbleness and UI, and I used **Claude Code** for the models lol. But the one terminal agent kept coming back to was **Pi**, by **Mario Zechner**, for the plugin system I could shape to my own workflow. What **Pi** lacked was a GUI, and I wanted the extensibility with a real interface on top of it. The first version was a native **SwiftUI** app, but within two weeks it was clear that building a coding agent inside **SwiftUI** was the wrong fight for someone who doesn't already write code, so the project pivoted to **Electron**. I was playing around with **Glaze**, **Raycast**'s AI app maker. I figured, let me just recreate Aiden in Glaze, ran out of credits inside an hour, used **Codex** to grab the code out of Glaze, and a week later this is what happened. **Aiden** runs on the **Pi** agent runtime and gives it a Mac-native workspace. +I don't come from a coding background. I'd been bouncing between the coding agents that exist, and each one had a piece of what I wanted without any of them being the whole thing. I loved **Codex** for its restrained, lovely desktop UX, **Opencode** for letting me bring whatever model and provider I wanted and also looking great in both the terminal and desktop, and **Cursor** for its nimbleness and UI, and I used **Claude Code** for the models lol. But the one terminal agent kept coming back to was **Pi**, by **Mario Zechner**, for the plugin system I could shape to my own workflow. What **Pi** lacked was a GUI, and I wanted the extensibility with a real interface on top of it. The first version was a native **SwiftUI** app, but within two weeks it was clear that building a coding agent inside **SwiftUI** was the wrong fight for someone who doesn't already write code, so the project pivoted to **Electron**. I was playing around with **Glaze**, **Raycast**'s AI app maker. I figured, let me just recreate Aiden in Glaze, ran out of credits inside an hour, used **Codex** to grab the code out of Glaze, and a week later this is what happened. **Aiden** runs on the **Pi** agent runtime and gives it a native-feeling desktop workspace. ## Features @@ -24,10 +28,12 @@ I don't come from a coding background. I'd been bouncing between the coding agen - **Command palette and shortcuts** - `⌘K` searches commands, chats, models, providers, Settings, and appearance actions. One typed command system also powers native menus, visible shortcut labels, transactional global hotkeys, and the searchable Keyboard Shortcuts editor. - **Commands and explicit skills** - type `/` at the start of the composer to search Aiden app commands, or `$` to search the active workspace's available skills. Commands reuse canonical app workflows; an explicitly selected skill is revalidated for the active workspace, applies to one accepted message, and persists only safe display provenance. - **Native Subagents** - a foreground chat can delegate up to four fresh `scout`, `planner`, or `reviewer` tasks. Children are read/search-only, inherit the approved workspace and model, stop with the parent, and appear as live chips plus an inspectable **Subagents** view in Environment. +- **Bots and Web Search** - macOS can create durable Bots with explicit capability grants, persistent conversations, image understanding, and Telegram control. Every desktop can configure explicit Web Search routes from the expanded provider catalog; Bots remain hidden on Linux until their native security bindings are supported there. - **Workspaces and managed worktrees** - use folders, scratch workspaces, or isolated managed worktrees with three access levels, workspace-scoped tools, Ask-mode approvals, guarded creation/deletion, and crash-aware cleanup. - **Models and the Model Pad** - choose from Pi's native hosted-provider catalog, local Ollama or LM Studio models, and declarative compatible endpoints. Arrange a personal capability-and-pace map, optionally enrich hosted models with explicitly fetched Artificial Analysis scores through a benchmark-only OpenRouter key, and keep benchmark evidence visibly separate from runtime limits and availability. - **Terminal, Git, and review** - keep a terminal drawer beside the conversation, inspect files and diffs in Environment, edit with dirty-file protection, compare branches, commit or push checked snapshots, and open the workspace in a discovered external editor. -- **macOS integration and appearance** - native menus, **Keychain**, **Parakeet**, the dictation pill, Apple **Foundation Models**, the signed **Rust** Computer Use broker, semantic themes, high contrast, reduced motion, and consistent light/dark rendering. +- **Rich responses and local diagnostics** - render sandboxed HTML, chart, math, and raster artifacts with recovery/export controls, and inspect or export bounded local diagnostics without an automatic upload path. +- **Desktop integration and appearance** - native menus, encrypted system credential storage, **Parakeet**, the dictation pill, semantic themes, high contrast, reduced motion, and consistent light/dark rendering. Apple **Foundation Models**, Accessibility auto-paste, and the signed **Rust** Computer Use broker remain macOS-only. - **Extensibility and background work** - use skills, **MCP**, **Exa** search, scheduled tasks, voice, and attachments through typed, allowlisted boundaries. - **Aiden On The Go** - opt in to a pinned local-network connection or an explicit non-Funnel Tailscale Serve route, pair each iPhone or iPad separately, and revoke devices from [Remote Access settings](docs/aiden-on-the-go-remote-access.md). - **Updates and release safety** - signed builds use the verified GitHub release feed. Once an update is downloaded, Aiden shows the version above Profile with **Later** and **Restart now**, then follows the normal save and shutdown guards before relaunching. @@ -45,11 +51,11 @@ The roadmap is maintained in [the plan index](docs/plans/README.md). These bulle ## Privacy and trust -Aiden stores chats, settings, workspace metadata, and downloaded speech models locally. Provider credentials and MCP OAuth sessions are encrypted with macOS secure storage. The renderer is sandboxed, has no direct Node.js access, and communicates with Electron through an allowlisted bridge. +Aiden stores chats, settings, workspace metadata, and downloaded speech models locally. Provider credentials and MCP OAuth sessions use the operating system's encrypted credential storage. Linux refuses to store secrets if only Electron's reversible `basic_text` backend is available. The renderer is sandboxed, has no direct Node.js access, and communicates with Electron through an allowlisted bridge. Network access happens only when the selected feature needs it: hosted models receive the conversation content sent to them, cloud transcription receives selected audio, Exa receives search queries, remote MCP servers receive tool requests, and model downloads contact their upstream host. A fully local session can use a local model, on-device voice, no remote MCP servers, and web search disabled. -Computer Use is an opt-in beta with a global switch, a separate per-chat switch, macOS permission checks, exact target binding, and one-use approval for every mutation. See the [Computer Use security design](docs/computer-use-integration.md) for the complete boundary. +On macOS, Computer Use is an opt-in beta with a global switch, a separate per-chat switch, permission checks, exact target binding, and one-use approval for every mutation. It is omitted from Linux builds. See the [Computer Use security design](docs/computer-use-integration.md) for the complete boundary. ## Architecture @@ -65,7 +71,7 @@ Electron main process ├── workspace-scoped tools, Git, review, and terminal ├── encrypted credentials and local JSON stores ├── MCP, attachments, search, and voice - └── signed native helpers for Apple models and Computer Use + └── hardened native helpers, plus macOS-only Apple model and Computer Use helpers ``` Core technologies include Electron 43, React 19, TypeScript, Vite, Tailwind CSS, TanStack Router and Query, Radix UI, the Pi agent runtime, Swift, and Rust. @@ -74,11 +80,11 @@ Core technologies include Electron 43, React 19, TypeScript, Vite, Tailwind CSS, ### Requirements -- macOS +- macOS or a glibc 2.34+ x64/arm64 Linux desktop - Node.js 22.19 or newer and npm -- Rust and Cargo -- A full Xcode 26 or newer for the Apple Foundation Models helper -- An Apple Development or Developer ID Application identity for packaged builds +- Rust and Cargo only for macOS Computer Use helper tests +- A full Xcode 26 or newer only for macOS Apple Foundation Models builds +- An Apple signing identity only for packaged macOS distribution builds ### Run locally @@ -93,7 +99,7 @@ The native Aiden On The Go iPhone and iPad client lives in [`ios/`](ios/README.m The iPhone and iPad app is distributed through **TestFlight only**. GitHub releases do not publish an IPA; [`ios/README.md`](ios/README.md) documents local development and device validation. Android validation builds remain separate from the macOS release and are uploaded by CI as installable APK artifacts. -The development launcher prepares a cached, ad-hoc-signed **Aiden Agent Dev** runtime that can run beside the installed **Aiden Agent** app. Development uses separate Application Support, Chromium session, log, crash, and `~/.aiden-dev` roots; it does not copy production data, register global shortcuts, or check the production update feed by default. Set `AIDEN_DEV_GLOBAL_SHORTCUTS=1` only when a development run intentionally needs the global bindings. +The development launcher prepares a platform-appropriate **Aiden Agent Dev** runtime that can run beside the installed **Aiden Agent** app. Development uses separate app-data, Chromium session, log, crash, and `~/.aiden-dev` roots; it does not copy production data, register global shortcuts, or check the production update feed by default. Set `AIDEN_DEV_GLOBAL_SHORTCUTS=1` only when a development run intentionally needs the global bindings. Native builds discover the newest compatible full Xcode without changing the machine-wide `xcode-select` setting; `DEVELOPER_DIR` remains available as a per-command override. @@ -114,13 +120,17 @@ npm run package npm run package:verify ``` -Distribution builds use `npm run dist` and require Developer ID signing plus notarization. The release pipeline fails closed, verifies the app, DMG, and ZIP, checks the deployed Homebrew and website consumers, and publishes updater metadata only with the matching verified artifacts. Read [macOS releases and automatic updates](docs/releasing.md) before enabling publication. +On Linux, use `npm run package:linux` and `npm run package:linux:verify` for an +unpacked development package, or `npm run dist:linux` for AppImage, Debian, and +RPM artifacts. Linux packages are built on their target architecture. + +Distribution builds use `npm run dist`; macOS artifacts require Developer ID signing plus notarization. The release pipeline fails closed, verifies the platform artifacts, checks the deployed Homebrew and website consumers, and publishes updater metadata only with the matching verified artifacts. Read [macOS releases and automatic updates](docs/releasing.md) before enabling publication. -The checked-in models.dev snapshot is refreshed only through `npm run models:refresh` or the guarded distribution path. Artificial Analysis credentials and data are never bundled. Direct Artificial Analysis suggestions require an explicit user fetch. OpenRouter benchmark insights use a separate encrypted Model Pad key only after an explicit Connect & fetch or Fetch latest action, then read normalized public scores from a device-local offline cache. That key never configures an inference provider or imports OpenRouter's model catalog. +The checked-in models.dev snapshot is refreshed only through `npm run models:refresh` or the guarded distribution path; Linux and macOS packaging invoke that explicit release step. Artificial Analysis credentials and data are never bundled. OpenRouter benchmark insights use a separate encrypted Model Pad key only after an explicit Connect & fetch or Fetch latest action, then read normalized public scores from a device-local offline cache. That key never configures an inference provider or imports OpenRouter's model catalog. ## Project status -Aiden Agent is a beta macOS release. Signed DMG and ZIP builds, checksums, and automatic-update metadata are published through [GitHub Releases](https://github.com/sambitcreate/aiden-agent/releases). The release workflow is fail-closed: it verifies signing, notarization, package contents, updater metadata, and version monotonicity before publishing. See [the release guide](docs/releasing.md) for the complete process. +Aiden Agent is a beta desktop release for macOS and Linux. Signed macOS DMG/ZIP builds and Linux AppImage/DEB/RPM builds for x64 and arm64 are published with checksums through [GitHub Releases](https://github.com/sambitcreate/aiden-agent/releases). The release workflow verifies both platform sets before publication. macOS keeps automatic updates; Linux uses explicit package replacement. See [the macOS release guide](docs/releasing.md) and [Linux guide](docs/linux.md). The canonical website download is the stable [`Aiden-Agent-Beta-arm64.dmg`](https://github.com/sambitcreate/aiden-agent/releases/latest/download/Aiden-Agent-Beta-arm64.dmg) diff --git a/android/app/src/main/java/sbtbiswas/AidenOnTheGo/config/AidenVoiceInput.kt b/android/app/src/main/java/sbtbiswas/AidenOnTheGo/config/AidenVoiceInput.kt index 231afd2d..0568a854 100644 --- a/android/app/src/main/java/sbtbiswas/AidenOnTheGo/config/AidenVoiceInput.kt +++ b/android/app/src/main/java/sbtbiswas/AidenOnTheGo/config/AidenVoiceInput.kt @@ -7,7 +7,7 @@ import kotlinx.coroutines.flow.asStateFlow enum class AidenVoiceInputMode(val wireValue: String, val title: String) { ON_DEVICE("on-device", "On this device"), - PAIRED_MAC("paired-mac", "Paired Mac"); + PAIRED_MAC("paired-mac", "Paired desktop"); companion object { fun fromWireValue(value: String?): AidenVoiceInputMode = diff --git a/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/bots/AidenBotCustomAccessFlowScreen.kt b/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/bots/AidenBotCustomAccessFlowScreen.kt index c294c481..96082fbe 100644 --- a/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/bots/AidenBotCustomAccessFlowScreen.kt +++ b/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/bots/AidenBotCustomAccessFlowScreen.kt @@ -184,7 +184,7 @@ fun AidenBotCustomAccessFlowScreen( val freshCat = cl.botCapabilityCatalog(detail.id) selectedBotDetail = fresh catalog = freshCat - saveError = "Access policy was changed on your Mac. Review the latest policy and try again." + saveError = "Access policy was changed on your paired desktop. Review the latest policy and try again." } catch (_: Exception) { saveError = e.message ?: "Conflict updating access" } @@ -305,7 +305,7 @@ fun AidenBotCustomAccessFlowScreen( // File Scopes Section item { - Text("Mac Files", style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.Bold, color = palette.secondary) + Text("Desktop Files", style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.Bold, color = palette.secondary) Spacer(modifier = Modifier.height(6.dp)) Card( modifier = Modifier.fillMaxWidth(), @@ -370,7 +370,7 @@ fun AidenBotCustomAccessFlowScreen( ) { Column(modifier = Modifier.weight(1f)) { Text("Execute Shell Commands", style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.SemiBold, color = palette.foreground) - Text("Allows bot to run terminal commands on Mac", style = MaterialTheme.typography.bodySmall, color = palette.secondary) + Text("Allows bot to run terminal commands on the paired desktop", style = MaterialTheme.typography.bodySmall, color = palette.secondary) } Switch( checked = curDraft.shellEnabled, diff --git a/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/bots/AidenBotEditorScreen.kt b/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/bots/AidenBotEditorScreen.kt index e9cfb74d..2164cc2e 100644 --- a/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/bots/AidenBotEditorScreen.kt +++ b/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/bots/AidenBotEditorScreen.kt @@ -703,7 +703,7 @@ fun AidenBotEditorScreen( Divider(color = palette.canvas) // File scopes - Text("Mac File Scopes", style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.SemiBold, color = palette.foreground) + Text("Desktop File Scopes", style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.SemiBold, color = palette.foreground) currentCat.fileScopes.forEach { scopeItem -> Row( verticalAlignment = Alignment.CenterVertically, diff --git a/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/bots/AidenBotGeneratedAvatarLifecycle.kt b/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/bots/AidenBotGeneratedAvatarLifecycle.kt index 84736a9d..c3defe98 100644 --- a/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/bots/AidenBotGeneratedAvatarLifecycle.kt +++ b/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/bots/AidenBotGeneratedAvatarLifecycle.kt @@ -38,7 +38,7 @@ sealed class AidenBotGeneratedAvatarError(val messageText: String) : Exception(m object SourceTooLarge : AidenBotGeneratedAvatarError("That image is too large. Choose another image.") object UnsupportedImage : AidenBotGeneratedAvatarError("That image format can't be used for a Bot photo.") object InvalidImage : AidenBotGeneratedAvatarError("Aiden couldn't prepare that image. Choose another image.") - object Unavailable : AidenBotGeneratedAvatarError("Reconnect to your Mac before saving this Bot photo.") + object Unavailable : AidenBotGeneratedAvatarError("Reconnect to your paired desktop before saving this Bot photo.") } enum class AidenBotGeneratedAvatarPhase { diff --git a/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/bots/AidenBotImagePlaygroundView.kt b/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/bots/AidenBotImagePlaygroundView.kt index 9e70b179..4f85d898 100644 --- a/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/bots/AidenBotImagePlaygroundView.kt +++ b/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/bots/AidenBotImagePlaygroundView.kt @@ -232,7 +232,7 @@ fun AidenBotImagePlaygroundSheet( } Spacer(modifier = Modifier.height(8.dp)) Text( - text = "Bot photos generated on macOS can be synchronized to Android. You can also customize your Bot with the built-in Semantic Avatar studio.", + text = "Bot photos generated on a supported Apple device can be synchronized to Android. You can also customize your Bot with the built-in Semantic Avatar studio.", style = MaterialTheme.typography.bodySmall, color = palette.secondary ) diff --git a/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/bots/AidenBotsHomeScreen.kt b/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/bots/AidenBotsHomeScreen.kt index 4102b800..dacfca8a 100644 --- a/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/bots/AidenBotsHomeScreen.kt +++ b/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/bots/AidenBotsHomeScreen.kt @@ -187,7 +187,7 @@ fun aidenBotInboxActivityStatus( if (canRespondToApproval) { AidenBotInboxActivityStatus("Approval needed", "verified_user") } else { - AidenBotInboxActivityStatus("Waiting for approval on Mac", "computer") + AidenBotInboxActivityStatus("Waiting for desktop approval", "computer") } } AidenBotConversationActivityState.RECONCILING -> AidenBotInboxActivityStatus("Updating", "sync") @@ -435,7 +435,7 @@ fun AidenBotsHomeScreen( AidenEmptyState( icon = Icons.Default.WifiOff, title = "Bots couldn’t load", - body = errorMessage ?: "Reconnect to your Mac and try again.", + body = errorMessage ?: "Reconnect to your paired desktop and try again.", modifier = Modifier.padding(top = 36.dp), action = { Button( @@ -456,7 +456,7 @@ fun AidenBotsHomeScreen( body = if (connectionState == AidenConnectionState.CONNECTED) "Create a familiar helper with one persistent conversation and its own capabilities." else - "Reconnect to your Mac to load Bots.", + "Reconnect to your paired desktop to load Bots.", modifier = Modifier.padding(top = 36.dp), action = if (connectionState == AidenConnectionState.CONNECTED) { { diff --git a/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/chat/AidenChatDetailScreen.kt b/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/chat/AidenChatDetailScreen.kt index 896a8d8d..79d2570a 100644 --- a/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/chat/AidenChatDetailScreen.kt +++ b/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/chat/AidenChatDetailScreen.kt @@ -308,7 +308,7 @@ fun AidenChatDetailScreen( ) { pendingApproval?.let { approval -> val isAutomation = AidenApprovalPresentation.isAutomation(approval.toolName) - val requiresMacConfirmation = AidenApprovalPresentation.requiresMacConfirmation(approval) + val requiresDesktopConfirmation = AidenApprovalPresentation.requiresDesktopConfirmation(approval) Card( modifier = Modifier .fillMaxWidth() @@ -353,10 +353,10 @@ fun AidenChatDetailScreen( style = MaterialTheme.typography.bodySmall, color = palette.secondary ) - } else if (requiresMacConfirmation) { + } else if (requiresDesktopConfirmation) { Spacer(modifier = Modifier.height(8.dp)) Text( - text = "Review the full unattended access scope and confirm in Aiden on your Mac. You can deny it here.", + text = "Review the full unattended access scope and confirm in Aiden on your paired desktop. You can deny it here.", style = MaterialTheme.typography.bodySmall, color = palette.secondary ) diff --git a/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/chat/AidenChatViewModel.kt b/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/chat/AidenChatViewModel.kt index cb0e626d..6fa62aa8 100644 --- a/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/chat/AidenChatViewModel.kt +++ b/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/chat/AidenChatViewModel.kt @@ -689,10 +689,10 @@ class AidenChatViewModel( if (!capabilities.canWriteSchedules) { "Schedule write access is required to approve this task." } else { - "Confirm this automation in Aiden on your Mac after reviewing its full access scope." + "Confirm this automation in Aiden on your paired desktop after reviewing its full access scope." } } else { - "This action must be confirmed in Aiden on your Mac." + "This action must be confirmed in the Aiden desktop app." } return } diff --git a/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/chat/ComposerVoiceInputController.kt b/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/chat/ComposerVoiceInputController.kt index b7de26fb..cb529c72 100644 --- a/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/chat/ComposerVoiceInputController.kt +++ b/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/chat/ComposerVoiceInputController.kt @@ -198,7 +198,7 @@ class ComposerVoiceInputController(private val context: Context) { private fun startMac(client: AidenRemoteClient?, session: Long) { if (client == null) { - fail("Connect to your paired Mac before using Mac transcription.", session, AidenDiagnosticCode.NETWORK) + fail("Connect to your paired desktop before using desktop transcription.", session, AidenDiagnosticCode.NETWORK) return } state = ComposerVoiceInputState.PREPARING @@ -208,18 +208,18 @@ class ComposerVoiceInputController(private val context: Context) { val status = client.speechStatus() ensureActive() if (!isCurrent(session)) return@launch - if (!status.engine.ready) throw IllegalStateException(status.engine.error ?: "The Mac speech engine is unavailable.") + if (!status.engine.ready) throw IllegalStateException(status.engine.error ?: "The desktop speech engine is unavailable.") val selected = status.models.firstOrNull { it.id == status.selectedModelId && it.installed } ?: status.models.firstOrNull { it.installed && it.recommended } ?: status.models.firstOrNull { it.installed } - ?: throw IllegalStateException("Download a Mac speech model in Settings before using this option.") + ?: throw IllegalStateException("Download a desktop speech model in Settings before using this option.") if (status.selectedModelId != selected.id) client.selectSpeechModel(selected.id) ensureActive() if (!isCurrent(session)) return@launch activeModelId = selected.id beginMacRecording(session) } catch (error: Exception) { - if (isCurrent(session)) fail(error.message ?: "Mac transcription is unavailable.", session, AidenDiagnosticCode.NETWORK) + if (isCurrent(session)) fail(error.message ?: "Desktop transcription is unavailable.", session, AidenDiagnosticCode.NETWORK) } finally { if (isCurrent(session)) preparationJob = null } @@ -294,7 +294,7 @@ class ComposerVoiceInputController(private val context: Context) { val client = activeClient val modelId = activeModelId if (client == null || modelId == null) { - fail("Mac transcription stopped because the connection changed.", session, AidenDiagnosticCode.NETWORK) + fail("Desktop transcription stopped because the connection changed.", session, AidenDiagnosticCode.NETWORK) return } state = ComposerVoiceInputState.TRANSCRIBING @@ -307,7 +307,7 @@ class ComposerVoiceInputController(private val context: Context) { updateTranscript(result.text, session) clearSession(session) } catch (error: Exception) { - if (isCurrent(session)) fail(error.message ?: "The Mac could not transcribe this recording.", session, AidenDiagnosticCode.NETWORK) + if (isCurrent(session)) fail(error.message ?: "The paired desktop could not transcribe this recording.", session, AidenDiagnosticCode.NETWORK) } finally { if (isCurrent(session)) transcriptionJob = null } diff --git a/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/remote/AidenBotChatToolsView.kt b/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/remote/AidenBotChatToolsView.kt index 14f786b1..60c966bb 100644 --- a/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/remote/AidenBotChatToolsView.kt +++ b/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/remote/AidenBotChatToolsView.kt @@ -201,7 +201,7 @@ class AidenBotChatToolsModel( fun readOnlyMessage(connected: Boolean, canWriteBots: Boolean, hostAllowsMutations: Boolean): String? { if (bot?.health == AidenBotHealth.ARCHIVED) return "Archived bots are read-only until restored." if (bot?.health == AidenBotHealth.DEGRADED || bot?.health == AidenBotHealth.UNAVAILABLE) { - return "This bot's access needs repair on your Mac before it can work." + return "This bot's access needs repair on your paired desktop before it can work." } if (!connected) return "Offline — reconnect to change this chat's access." if (!canWriteBots) return "This phone can view Bot access but is not approved to change it." diff --git a/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/remote/AidenPairingScreen.kt b/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/remote/AidenPairingScreen.kt index ab75319a..20db885e 100644 --- a/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/remote/AidenPairingScreen.kt +++ b/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/remote/AidenPairingScreen.kt @@ -79,7 +79,7 @@ fun AidenPairingScreen( Scaffold( topBar = { TopAppBar( - title = { Text("Paired Macs", fontWeight = FontWeight.Bold) }, + title = { Text("Paired desktops", fontWeight = FontWeight.Bold) }, navigationIcon = { IconButton(onClick = onDismiss) { Icon(Icons.Default.Close, contentDescription = "Close", tint = palette.foreground) @@ -183,7 +183,7 @@ fun AidenPairingScreen( // Pair New Mac Section Text( - text = "Pair New Mac", + text = "Pair New Desktop", style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.Bold, color = palette.secondary @@ -299,7 +299,7 @@ fun AidenPairingScreen( ) { CircularProgressIndicator(color = palette.accent, modifier = Modifier.size(20.dp)) Spacer(modifier = Modifier.width(8.dp)) - Text("Pairing with Mac...", style = MaterialTheme.typography.bodyMedium, color = palette.foreground) + Text("Pairing with desktop...", style = MaterialTheme.typography.bodyMedium, color = palette.foreground) } } } @@ -325,7 +325,7 @@ fun AidenPairingScreen( colors = sbtbiswas.AidenOnTheGo.ui.theme.aidenTextFieldColors(), value = endpointUrl, onValueChange = { endpointUrl = it }, - label = { Text("Mac Address (HTTPS Endpoint)") }, + label = { Text("Desktop Address (HTTPS Endpoint)") }, singleLine = true, shape = RoundedCornerShape(12.dp), modifier = Modifier.fillMaxWidth() @@ -417,7 +417,7 @@ fun AidenPairingScreen( onDismissRequest = { installationPendingRemoval = null }, title = { Text("Remove ${installation.name}?") }, text = { - Text("This removes the pairing credential and all cached chats, Bots, usage, drafts, and workspace data for this Mac from this device.") + Text("This removes the pairing credential and all cached chats, Bots, usage, drafts, and workspace data for this desktop from this device.") }, confirmButton = { TextButton( diff --git a/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/remote/AidenQRCodeScanner.kt b/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/remote/AidenQRCodeScanner.kt index 061ae1fc..e4c392ff 100644 --- a/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/remote/AidenQRCodeScanner.kt +++ b/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/remote/AidenQRCodeScanner.kt @@ -89,7 +89,7 @@ fun AidenQRCodeScanner( ) Spacer(modifier = Modifier.height(8.dp)) Text( - text = "Point your camera at the QR code displayed in Aiden on your Mac to pair instantly.", + text = "Point your camera at the QR code displayed in Aiden on your desktop to pair instantly.", style = MaterialTheme.typography.bodySmall, color = palette.secondary, textAlign = TextAlign.Center diff --git a/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/scheduled/AidenScheduledTasksScreen.kt b/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/scheduled/AidenScheduledTasksScreen.kt index 4f3472ae..8e814f95 100644 --- a/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/scheduled/AidenScheduledTasksScreen.kt +++ b/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/scheduled/AidenScheduledTasksScreen.kt @@ -290,7 +290,7 @@ fun AidenScheduledTasksScreen( refresh() loadRuns(selectedTask.id) } catch (_: CancellationException) { - // Cancellation does not prove whether the Mac accepted the run. + // Cancellation does not prove whether the paired desktop accepted the run. } catch (error: Exception) { pendingRunKeys.failed(selectedTask.id, error) if (isCurrentRequest(activeClient, AidenRemoteCapability.SCHEDULE_WRITE)) { @@ -408,7 +408,7 @@ private fun AidenScheduledTaskList( ) Spacer(Modifier.height(5.dp)) Text( - "Aiden shows a permission review before saving unattended work. If a proposal can't be fully reviewed here, Aiden asks you to confirm it on your Mac.", + "Aiden shows a permission review before saving unattended work. If a proposal can't be fully reviewed here, Aiden asks you to confirm it on your paired desktop.", style = MaterialTheme.typography.bodySmall, color = palette.secondary ) @@ -684,7 +684,7 @@ private fun AidenScheduledTaskDetail( ) { Text("Delete automation") } if (!isConnected) { - Text("Connect to your Mac to run or change this task.", style = MaterialTheme.typography.bodySmall, color = palette.secondary) + Text("Connect to your paired desktop to run or change this task.", style = MaterialTheme.typography.bodySmall, color = palette.secondary) } else if (!canManage) { Text("This paired device has read-only scheduled task access.", style = MaterialTheme.typography.bodySmall, color = palette.secondary) } diff --git a/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/settings/AidenAppearanceSettingsScreen.kt b/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/settings/AidenAppearanceSettingsScreen.kt index 30fbd41a..912e117c 100644 --- a/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/settings/AidenAppearanceSettingsScreen.kt +++ b/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/settings/AidenAppearanceSettingsScreen.kt @@ -52,14 +52,14 @@ fun AidenAppearanceSettingsScreen( } runCatching { client.speechStatus() } .onSuccess { speechStatus = it; speechError = null } - .onFailure { speechError = it.message ?: "Mac transcription is unavailable." } + .onFailure { speechError = it.message ?: "Desktop transcription is unavailable." } } fun runSpeechAction(action: suspend () -> AidenSpeechStatus) { scope.launch { runCatching { action() } .onSuccess { speechStatus = it; speechError = null } - .onFailure { speechError = it.message ?: "Mac transcription is unavailable." } + .onFailure { speechError = it.message ?: "Desktop transcription is unavailable." } } } @@ -100,7 +100,7 @@ fun AidenAppearanceSettingsScreen( Spacer(Modifier.width(12.dp)) Column(Modifier.weight(1f)) { Text("Installations", style = MaterialTheme.typography.titleMedium, color = palette.foreground) - Text("Pair or switch your Aiden Agent Mac", style = MaterialTheme.typography.bodySmall, color = palette.secondary) + Text("Pair or switch your Aiden Agent desktop", style = MaterialTheme.typography.bodySmall, color = palette.secondary) } } } @@ -115,7 +115,7 @@ fun AidenAppearanceSettingsScreen( ) Spacer(Modifier.height(8.dp)) Text( - text = "Choose where speech is transcribed. Paired Mac sends microphone audio over Aiden's encrypted pinned connection and does not retain it.", + text = "Choose where speech is transcribed. Paired desktop sends microphone audio over Aiden's encrypted pinned connection and does not retain it.", style = MaterialTheme.typography.bodySmall, color = palette.secondary ) @@ -131,7 +131,7 @@ fun AidenAppearanceSettingsScreen( Column(Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) { Text(mode.title, style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.SemiBold, color = palette.foreground) Text( - if (mode == AidenVoiceInputMode.ON_DEVICE) "Android SpeechRecognizer; speech stays on this device." else "Parakeet on your connected Aiden Agent Mac; final text appears after you stop.", + if (mode == AidenVoiceInputMode.ON_DEVICE) "Android SpeechRecognizer; speech stays on this device." else "Parakeet on your connected Aiden Agent desktop; final text appears after you stop.", style = MaterialTheme.typography.bodySmall, color = palette.secondary ) @@ -167,7 +167,7 @@ fun AidenAppearanceSettingsScreen( } else { val status = speechStatus if (remoteClient == null) { - Text("Connect to a paired Mac to configure transcription.", style = MaterialTheme.typography.bodySmall, color = palette.warning) + Text("Connect to a paired desktop to configure transcription.", style = MaterialTheme.typography.bodySmall, color = palette.warning) } else if (status == null && speechError == null) { LinearProgressIndicator(Modifier.fillMaxWidth()) } else if (status != null) { diff --git a/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/workspaces/AidenUsageSheet.kt b/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/workspaces/AidenUsageSheet.kt index 57791ad4..d7774951 100644 --- a/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/workspaces/AidenUsageSheet.kt +++ b/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/workspaces/AidenUsageSheet.kt @@ -232,7 +232,7 @@ fun AidenUsageSheet( Icon(Icons.Default.Shield, null, tint = palette.accent, modifier = Modifier.size(28.dp)) Spacer(Modifier.width(12.dp)) Text( - "Privacy-safe aggregates are recorded by Aiden Agent on your Mac. Prompts, responses, chat IDs, workspace IDs, and file paths are not included.", + "Privacy-safe aggregates are recorded by Aiden Agent on your paired desktop. Prompts, responses, chat IDs, workspace IDs, and file paths are not included.", style = MaterialTheme.typography.bodySmall, color = palette.secondary ) diff --git a/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/workspaces/AidenWorkspaceEnvironmentScreen.kt b/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/workspaces/AidenWorkspaceEnvironmentScreen.kt index d2bc004a..2363f26e 100644 --- a/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/workspaces/AidenWorkspaceEnvironmentScreen.kt +++ b/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/workspaces/AidenWorkspaceEnvironmentScreen.kt @@ -458,7 +458,7 @@ fun AidenWorkspaceEnvironmentScreen( onDismissRequest = { showConflictDialog = false }, title = { Text("Conflict Detected", fontWeight = FontWeight.Bold) }, text = { - Text("This file on your Mac was modified since you opened it. Would you like to reload the latest version from your Mac?") + Text("This file on your paired desktop was modified since you opened it. Would you like to reload the latest version from your desktop?") }, confirmButton = { Button( @@ -478,7 +478,7 @@ fun AidenWorkspaceEnvironmentScreen( }, colors = ButtonDefaults.buttonColors(containerColor = palette.accent) ) { - Text("Reload from Mac", color = Color.White) + Text("Reload from desktop", color = Color.White) } }, dismissButton = { diff --git a/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/workspaces/AidenWorkspaceHomeScreen.kt b/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/workspaces/AidenWorkspaceHomeScreen.kt index 635d47e3..95c4d9fd 100644 --- a/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/workspaces/AidenWorkspaceHomeScreen.kt +++ b/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/workspaces/AidenWorkspaceHomeScreen.kt @@ -287,7 +287,7 @@ private fun AidenWorkspaceHome( viewModel.load(force = true) scope.launch { snackbarHostState.showSnackbar( - usageErrorMessage ?: "Loading Usage from your Mac…" + usageErrorMessage ?: "Loading Usage from your paired desktop…" ) } } diff --git a/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/workspaces/AidenWorkspaceShellScreen.kt b/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/workspaces/AidenWorkspaceShellScreen.kt index d0eab0ce..15e77ce4 100644 --- a/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/workspaces/AidenWorkspaceShellScreen.kt +++ b/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/workspaces/AidenWorkspaceShellScreen.kt @@ -440,7 +440,7 @@ fun AidenWorkspaceDirectoryScreen( } ) DropdownMenuItem( - text = { Text("Add Mac Folder...") }, + text = { Text("Add Desktop Folder...") }, leadingIcon = { Icon(Icons.Default.Folder, contentDescription = null) }, onClick = { showCreateMenu = false @@ -516,7 +516,7 @@ fun AidenWorkspaceDirectoryScreen( icon = if (selectedTab == 0) Icons.Default.FolderOpen else Icons.Default.Archive, title = if (selectedTab == 0) "No active workspaces" else "No archived workspaces", body = if (selectedTab == 0) - "Create a workspace or add an approved folder from your Mac." + "Create a workspace or add an approved folder from your desktop." else "Workspaces archived on this device will appear here." ) @@ -751,7 +751,7 @@ fun AidenWorkspaceDirectoryScreen( onDismissRequest = { showScratchConfirmDialog = false }, title = { Text("Create Managed Scratch?", fontWeight = FontWeight.Bold) }, text = { - Text("Aiden will create an isolated scratch workspace in an ephemeral location on your Mac.") + Text("Aiden will create an isolated scratch workspace in an ephemeral location on your paired desktop.") }, confirmButton = { Button( @@ -827,7 +827,7 @@ fun AidenWorkspaceDirectoryScreen( onDismissRequest = { showArchiveDisclosureDialog = false }, title = { Text("Archive on this Device", fontWeight = FontWeight.Bold) }, text = { - Text("Archiving a workspace hides it only on this device. Your Mac, files, and other devices remain completely unaffected.") + Text("Archiving a workspace hides it only on this device. Your paired desktop, files, and other devices remain completely unaffected.") }, confirmButton = { Button( @@ -856,7 +856,7 @@ fun AidenWorkspaceDirectoryScreen( onDismissRequest = { showRemoveDialog = false }, title = { Text("Remove Workspace?", fontWeight = FontWeight.Bold) }, text = { - Text("Are you sure you want to remove \"${target.name}\" from Aiden? Local files on your Mac are preserved.") + Text("Are you sure you want to remove \"${target.name}\" from Aiden? Local files on your paired desktop are preserved.") }, confirmButton = { Button( @@ -888,7 +888,7 @@ fun AidenWorkspaceDirectoryScreen( onDismissRequest = { showDeleteWorktreeDialog = false }, title = { Text("Delete Managed Worktree?", fontWeight = FontWeight.Bold) }, text = { - Text("This will permanently remove the managed worktree folder and git worktree on your Mac.") + Text("This will permanently remove the managed worktree folder and git worktree on your paired desktop.") }, confirmButton = { Button( @@ -1014,7 +1014,7 @@ fun AidenFolderBrowserSheet( modifier = Modifier.fillMaxWidth() ) { Text( - text = "Browse Mac Folders", + text = "Browse Desktop Folders", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, color = palette.foreground, @@ -1345,8 +1345,8 @@ fun AidenWorkspaceSettingsSheet( title = { Text(if (workspace.isManagedWorktree) "Delete Worktree?" else "Remove Workspace?", fontWeight = FontWeight.Bold) }, text = { Text( - if (workspace.isManagedWorktree) "This will permanently remove the managed worktree folder and git worktree on your Mac." - else "Are you sure you want to remove \"${workspace.name}\" from Aiden? Local files on your Mac are preserved." + if (workspace.isManagedWorktree) "This will permanently remove the managed worktree folder and git worktree on your paired desktop." + else "Are you sure you want to remove \"${workspace.name}\" from Aiden? Local files on your paired desktop are preserved." ) }, confirmButton = { diff --git a/android/app/src/main/java/sbtbiswas/AidenOnTheGo/models/AidenChat.kt b/android/app/src/main/java/sbtbiswas/AidenOnTheGo/models/AidenChat.kt index 708f4358..7b149a24 100644 --- a/android/app/src/main/java/sbtbiswas/AidenOnTheGo/models/AidenChat.kt +++ b/android/app/src/main/java/sbtbiswas/AidenOnTheGo/models/AidenChat.kt @@ -355,7 +355,7 @@ object AidenAgentActivityPresentation { "web_search" to Pair("Searching the web", "Searched the web"), "schedule_task" to Pair("Scheduling", "Scheduled"), "edit_automation" to Pair("Editing automation", "Edited automation"), - "computer_use" to Pair("Using Mac", "Used Mac"), + "computer_use" to Pair("Using Computer Use", "Used Computer Use"), "compact_context" to Pair("Compacting context", "Compacted context") ) @@ -456,7 +456,7 @@ object AidenAgentActivityPresentation { if (changes > 0) clauses.add("${if (running) "editing" else "edited"} $changes file${if (changes == 1) "" else "s"}") if (commands > 0) clauses.add("${if (running) "running" else "ran"} $commands command${if (commands == 1) "" else "s"}") if (web > 0) clauses.add("$web web search${if (web == 1) "" else "es"}") - if (mac > 0) clauses.add("$mac Mac action${if (mac == 1) "" else "s"}") + if (mac > 0) clauses.add("$mac Computer Use action${if (mac == 1) "" else "s"}") if (compactions > 0) clauses.add(if (running) "compacting context" else "compacted context") if (other > 0) clauses.add("$other tool call${if (other == 1) "" else "s"}") if (clauses.isEmpty()) return if (running) "Working" else "Used ${tools.size} tool${if (tools.size == 1) "" else "s"}" @@ -733,7 +733,7 @@ object AidenApprovalPresentation { else -> "Approval Required" } - fun requiresMacConfirmation(approval: AidenPendingApproval): Boolean = + fun requiresDesktopConfirmation(approval: AidenPendingApproval): Boolean = isAutomation(approval.toolName) && approval.canRespond && approval.hasRequiredWriteCapability && !approval.hostCanAllow } diff --git a/android/app/src/main/java/sbtbiswas/AidenOnTheGo/persistence/AidenInstallationStore.kt b/android/app/src/main/java/sbtbiswas/AidenOnTheGo/persistence/AidenInstallationStore.kt index 0f37c19c..ef516aed 100644 --- a/android/app/src/main/java/sbtbiswas/AidenOnTheGo/persistence/AidenInstallationStore.kt +++ b/android/app/src/main/java/sbtbiswas/AidenOnTheGo/persistence/AidenInstallationStore.kt @@ -70,7 +70,7 @@ class AidenInstallationStore( val installation = AidenInstallation( instanceId = exchange.instanceId, deviceId = exchange.deviceId, - name = exchange.displayName ?: "Aiden Mac", + name = exchange.displayName ?: "Aiden desktop", endpoint = exchange.endpoint, serverSpkiSha256 = exchange.serverSpkiSha256, pairingTrust = trust, diff --git a/android/app/src/main/java/sbtbiswas/AidenOnTheGo/protocol/AidenRemoteExceptions.kt b/android/app/src/main/java/sbtbiswas/AidenOnTheGo/protocol/AidenRemoteExceptions.kt index 573c9694..546519a8 100644 --- a/android/app/src/main/java/sbtbiswas/AidenOnTheGo/protocol/AidenRemoteExceptions.kt +++ b/android/app/src/main/java/sbtbiswas/AidenOnTheGo/protocol/AidenRemoteExceptions.kt @@ -20,7 +20,7 @@ sealed class AidenBotContractException(val reason: String, message: String = rea class InvalidCombination(val combination: String) : AidenBotContractException( combination, when (combination) { - "no available provider and model" -> "Set up a provider and model on your Mac. In Aiden Agent, open Settings → Providers, connect or refresh a provider, and make at least one chat model available. Then tap Try Again." + "no available provider and model" -> "Set up a provider and model on your paired desktop. In Aiden Agent, open Settings → Providers, connect or refresh a provider, and make at least one chat model available. Then tap Try Again." "unavailable custom access" -> "One or more selected AI, Files, Connections, or Skills are no longer available. Review this Bot’s access choices and try again." "chat access exceeds bot" -> "This chat is asking for more access than the Bot currently allows. Reduce the chat’s access or expand the Bot’s access, then try again." "full access notice" -> "Review and accept the Full Access notice before giving this Bot full access." @@ -30,7 +30,7 @@ sealed class AidenBotContractException(val reason: String, message: String = rea } sealed class AidenManualPairingException(message: String) : Exception(message) { - object InvalidCode : AidenManualPairingException("Enter the 20-character setup code shown on your Mac.") + object InvalidCode : AidenManualPairingException("Enter the 20-character setup code shown on your desktop.") object InvalidBootstrap : AidenManualPairingException("Aiden Agent returned an invalid manual pairing response.") object DecryptionFailed : AidenManualPairingException("The setup code is incorrect or belongs to a different pairing window.") object EndpointMismatch : AidenManualPairingException("The setup code belongs to a different Aiden Agent address.") @@ -63,7 +63,7 @@ sealed class AidenSSEParserException(message: String) : Exception(message) { sealed class AidenRemoteClientException(message: String, cause: Throwable? = null) : Exception(message, cause) { object MissingCredential : AidenRemoteClientException("No credential available for this installation.") object MissingTrustConfiguration : AidenRemoteClientException("This Aiden installation must be paired again to establish secure server trust.") - object InstallationChanged : AidenRemoteClientException("The active Aiden Agent changed. Try again on the selected Mac.") + object InstallationChanged : AidenRemoteClientException("The active Aiden Agent changed. Try again on the selected desktop.") object InvalidEndpoint : AidenRemoteClientException("The Aiden Agent address is invalid.") class UnexpectedStatus(val statusCode: Int) : AidenRemoteClientException("Aiden Agent returned HTTP status $statusCode.") data class Server(val statusCode: Int, val body: AidenRemoteErrorEnvelope.Body) : AidenRemoteClientException(body.message) { diff --git a/android/app/src/test/java/sbtbiswas/AidenOnTheGo/AidenChatTest.kt b/android/app/src/test/java/sbtbiswas/AidenOnTheGo/AidenChatTest.kt index 54e1a255..e419b48c 100644 --- a/android/app/src/test/java/sbtbiswas/AidenOnTheGo/AidenChatTest.kt +++ b/android/app/src/test/java/sbtbiswas/AidenOnTheGo/AidenChatTest.kt @@ -405,7 +405,7 @@ class AidenChatTest { } ) assertEquals( - "1 web search, 1 Mac action, compacted context, 1 tool call", + "1 web search, 1 Computer Use action, compacted context, 1 tool call", AidenAgentActivityPresentation.summary(multiTimeline) ) @@ -583,8 +583,8 @@ class AidenChatTest { assertTrue(AidenApprovalPresentation.isAutomation(approval.toolName)) assertEquals("Create this automation?", AidenApprovalPresentation.title(approval.toolName)) assertEquals("Create a daily report", AidenApprovalPresentation.oneLineSummary(approval.summary)) - assertTrue(AidenApprovalPresentation.requiresMacConfirmation(approval)) - assertFalse(AidenApprovalPresentation.requiresMacConfirmation(approval.copy(hostCanAllow = true, canAllow = true))) + assertTrue(AidenApprovalPresentation.requiresDesktopConfirmation(approval)) + assertFalse(AidenApprovalPresentation.requiresDesktopConfirmation(approval.copy(hostCanAllow = true, canAllow = true))) assertEquals("Approval Required", AidenApprovalPresentation.title("run_command")) } @@ -624,7 +624,7 @@ class AidenChatTest { assertTrue(readOnlySchedule!!.canRespond) assertFalse(readOnlySchedule.hasRequiredWriteCapability) assertFalse(readOnlySchedule.canAllow) - assertFalse(AidenApprovalPresentation.requiresMacConfirmation(readOnlySchedule)) + assertFalse(AidenApprovalPresentation.requiresDesktopConfirmation(readOnlySchedule)) val cannotRespond = AidenPendingApprovalResolution.resolve( valid, diff --git a/docs/linux.md b/docs/linux.md new file mode 100644 index 00000000..a0f67c2e --- /dev/null +++ b/docs/linux.md @@ -0,0 +1,114 @@ +# Linux desktop support + +Aiden Agent ships native x64 and arm64 Linux builds as AppImage, Debian, and +RPM packages. The `.deb` and `.rpm` formats are recommended because the distro +package manager installs Electron's runtime libraries and owns replacement or +removal. AppImage is the portable fallback. + +## Install + +Download the package for your architecture from +[GitHub Releases](https://github.com/sambitcreate/aiden-agent/releases). + +Debian, Ubuntu, Linux Mint, Pop!_OS, and related distributions: + +```sh +sudo apt install ./Aiden-Agent-*-linux.deb +``` + +Fedora, RHEL, Rocky Linux, and other RPM-based distributions: + +```sh +sudo dnf install ./Aiden-Agent-*-linux.rpm +``` + +Portable AppImage: + +```sh +chmod +x Aiden-Agent-*-linux.AppImage +./Aiden-Agent-*-linux.AppImage +``` + +The AppImage uses a pinned static launcher, so it does not depend on the legacy +FUSE 2 userspace library. A container, locked-down host, or other environment +without a usable FUSE mount can still use AppImage's extraction fallback: + +```sh +./Aiden-Agent-*-linux.AppImage --appimage-extract-and-run +``` + +## Desktop requirements + +- A glibc 2.34 or newer x64 or arm64 desktop distribution supported by + Electron. This includes RHEL/Rocky Linux 9, Debian 12, Ubuntu 22.04, and newer + releases in those families. +- A working graphical session under X11 or Wayland. +- A Secret Service or KWallet credential backend when saving provider, MCP, + ChatGPT, or model-data credentials. GNOME Keyring, KDE Wallet, and compatible + desktop keyrings provide this on common desktop installations. +- `tar` for on-device speech-model installation and `openssl` only when the + optional nearby Aiden On The Go listener creates its local TLS identity. +- Tailscale only when the optional private-tailnet remote route is selected. + +Aiden deliberately refuses to save secrets when Electron reports the Linux +`basic_text` backend. Unlock or configure the desktop keyring and restart Aiden; +the app will not silently downgrade credentials to reversible local storage. +Keyless local connections such as LM Studio and Ollama do not use secret +storage and remain available when no keyring session is running. + +## Tailscale remote access + +Tailscale installs its CLI at `/usr/bin/tailscale` on mainstream Linux +packages. After signing in, grant your desktop user one-time permission to +manage Serve routes: + +```sh +sudo tailscale set --operator=$USER +``` + +Aiden changes only its scoped `/api/aiden/v1` HTTPS Serve path and preserves +unrelated Serve configuration. Without the operator grant, status remains +readable but Aiden reports the permission requirement instead of claiming an +uncertain connection. + +## Platform behavior + +The workspace agent, providers, local models, MCP, skills, Web Search, +schedules, terminal, Git, file editor, generative UI artifacts, diagnostics, +Gemini voice transcription, remote access, notifications, profile, themes, and +native subagents use the same contracts as macOS. Linux-specific integrations include: + +- native distro window chrome and conventional File/Edit/View/Window/Help menus; +- Ctrl-based app and global shortcuts, including the Wayland Global Shortcuts + portal on desktops that implement it; +- editor discovery through `PATH`, Snap command locations, JetBrains Toolbox + scripts, and common Flatpak application IDs; +- opening folders with the default desktop file manager; +- profile snapshot export through a Save dialog; +- bundled Node mDNS publication for nearby Aiden On The Go discovery, without + requiring Apple's `dns-sd` utility. + +Computer Use, Apple Foundation Models, and Bots are not included in the Linux +build. Their settings, navigation, onboarding promises, helper bundles, and +chat controls are omitted. Global dictation remains available when the desktop can register its +shortcut, but the transcript is copied to the clipboard instead of using the +macOS Accessibility auto-paste transaction. Wayland compositors own final +placement of the dictation pill, so exact bottom-center positioning may vary. + +Provider inventories may refresh only from the provider services the user has +configured. Descriptive model metadata comes from Aiden's bundled release +snapshot; ordinary app reads expose no live models.dev refresh action. + +## Updates and troubleshooting + +Linux builds do not apply macOS-style in-app updates. Install the newer `.deb` +or `.rpm` over the existing package, replace the AppImage, or follow the package +manager that owns the installation. Settings → About links directly to the +release page. + +If a global shortcut is unavailable, check the desktop's shortcut portal or +conflicts with another application and assign another chord under Settings → +Keyboard shortcuts. If an AppImage does not start, prefer the native distro +package or use the extraction command above. When reporting a Linux issue, +include the distribution, architecture, desktop environment, X11/Wayland +session type, package format, and the exact error shown by Aiden. diff --git a/docs/plans/README.md b/docs/plans/README.md index afdc4351..94a3419b 100644 --- a/docs/plans/README.md +++ b/docs/plans/README.md @@ -4,55 +4,56 @@ This directory is the source of truth for Aiden's implementation plans. The engi ## Active and partial -| Plan | Status | Current state | -| -------------------------------------------------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [Aiden Assistant](aiden-assistant-plan.md) | Partial | The dock, Markdown rendering, and confirmed provider-connection/model-pinned project-or-MCP automation creation/editing ship; settings tools and proactivity remain planned. | -| [Aiden On The Go](aiden-on-the-go-plan.md) | Active | Version 0.1.0 build 22 is `VALID` and `IN_BETA_TESTING` for Internal Testers. Android matches iOS's app-icon switcher, Workspace hierarchy, warm scoped Bots/Usage/SSE lifecycle, Usage dashboard, image showcase/gallery, keyboard-safe elevated composer, and split Photo/File pickers. Both clients support native in-process dictation or bounded no-retention transcription by the paired Mac's local Parakeet model. iOS also ships progressive onboarding, bidirectional media, reliable mobile approvals, typed activity timelines, semantic haptics, and one-chat-per-Bot conversations with companion vision for text-only models. Physical iPad/manual permission-system-UI acceptance, privacy publication, final store assets, and external/public-release decisions remain open. | -| [Bot-First Aiden On The Go](bot-first-aiden-on-the-go-plan.md) | Active | Phases 0–9 are implemented. Every Bot has one persistent chat and one contact row; Favorites are a pinned placement, Bot chat reuses the shared runtime with Messages-inspired identity/bubbles and Aiden's existing composer, and New/Edit Bot exclusively own its durable model. Remote open-or-create, immediate exact-cache chat entry, optimistic favorites, shaped skeleton loading, stable photos, atomic desktop creation, fresh-inventory save retries, conflict-safe Mac/iOS draft rebasing, final-only Bot replies with expandable progress, native-or-companion image handling, and internal TestFlight build 22 are green. Eligible Apple Intelligence hardware, physical iPad, multi-device/Mac, packaged rollback, live Telegram, wider staged TestFlight, Xcode 27, accessibility, and App Store owner gates remain open. | -| [Aiden Manual Pairing](aiden-manual-pairing-plan.md) | Implemented | The reviewed 100-bit setup-code path, shared one-use QR window, staged iOS activation, and adversarial coverage ship; hands-on LAN/Tailscale UI and physical-iPad acceptance remain open. | -| [Compaction](compaction-plan.md) | Partial | Pi-native checkpoints, lifecycle/crash recovery, and exact audited-upstream compatibility ship; durable memory and provider-native paths remain open. | -| [Designer Mode](designer-mode-plan.md) | Planned | Phase 0 validation has not started in the runtime. | -| [Dynamic Model Catalog](dynamic-model-catalog-plan.md) | Implemented | Validated pi.dev overlays, offline `0600` cache hydration, scoped setup refresh, four-hour launch refresh, force refresh, Pi metadata fallback, and Mac/iOS projection ship on pinned Pi 0.80.10. | -| [Generative UI Artifacts](generative-ui-artifacts-plan.md) | Active | Phases 0–6 shipped: chat-scoped `render_artifact`, strict sandboxed preview/export hosts, verified vendored Chart.js/Plotly/KaTeX, permission-aware `/visualize`, crash-recoverable authoritative storage/copies, descriptor-relative workspace reads, one-iframe handoff/expansion, visible failure states, and route-stable Responding/Visualizing activity. Three-agent PR review findings are remediated with focused regression coverage. | -| [Generation Progress Notes](generation-progress-notes-plan.md) | Planned | No implementation yet. | -| [Logging and Diagnostics Upgrade](logging-and-diagnostics-upgrade-plan.md) | Implemented | Phases 0–7 are implemented: bounded typed desktop journals, main-owned renderer evidence, local support export/delete, native categorical parity, and CI/release gates. Signed/notarized `v0.35.0` passed packaged diagnostics acceptance; physical-device termination receipts remain. | -| [Model Insights](model-insights-plan.md) | Partial | A dedicated benchmark-only OpenRouter key, manual fetch, exact source-aware offline cache, metric-selectable collision-free capability suggestions, progressive canvas-first Pad UX, axis provenance, attribution, and direct-AA retirement ship; device-local pace signals remain. | -| [Onboarding Authentication and Provider Validation](onboarding-auth-and-provider-validation-plan.md) | Active | Codex uses its dedicated auth surface, every interactive Pi provider can be configured during onboarding, OpenAI/Anthropic keys receive stronger catalog validation, completion is main-owned, and provider deferral stays explicit. | -| [Performance, Stability, Battery, and Efficiency](performance-stability-efficiency-plan.md) | Planned | Whole-app source audit is complete; implementation starts with instrumentation, durable state, and hard memory bounds. | -| [Pi Provider Integration](pi-provider-integration-plan.md) | Partial | Pi built-ins, stores, auth, native routing, custom provider composition, canonical assistant provenance, voice credential lookup, and attended structured questions ship; scalable UX and rollout cleanup remain. | -| [rpiv-advisor integration](rpiv-advisor-integration-plan.md) | Implemented | A bounded, tool-free, provider/auth-aware second opinion now uses an ephemeral per-consultation Ask User Question choice when the prompt does not name a reviewer, with no persistent Advisor settings or IPC. | -| [rpiv-todo Integration](rpiv-todo-integration-plan.md) | Partial | Attended desktop chats have a journal-replayed native todo tool, strict fail-closed snapshots, owner-fenced IPC, and a self-hiding floating progress chip with portal details; packaged visual/accessibility acceptance remains open. | -| [rpiv-btw Integration](rpiv-btw-integration-plan.md) | Partial | Attended desktop chats have bounded read-only side questions, ephemeral fingerprinted follow-ups, foreground-safe admission, exact provider dispatch, content-free usage accounting, and a native slash/card surface; packaged visual/accessibility acceptance remains open. | -| [Subagent Orchestration Expansion](subagent-orchestration-expansion-plan.md) | Active | Phases 0–6, Phase 7A durable lifecycle, the Phase 7B1 storage seam, startup/clone-boundary repairs, Pi-normalized foreground presentation and recovery, and parent-only iOS/Android projection hardening are complete; app-lifetime coordinator activation is next. | -| [Taracodlab Learnings](taracodlab-learnings-plan.md) | Partial | Phases A–B and D, plus core Phase E, are implemented; the remaining roadmap is open. | -| [Telegram First-Class Agent Parity](telegram-first-class-agent-parity-plan.md) | Active | Controls, compaction, skills, rich inbound, drafts/activity, buttons, documents, Settings, and onboarding are green; Threaded Mode/profiles/extensions/TTS/live smoke remain. | -| [Update, Microphone, and Computer Use Hardening](update-microphone-computer-use-hardening-plan.md) | Partial | Installed acceptance found a silent stalled download; observable full-download recovery is implemented, while repaired-build → next-release and clean-TCC acceptance remain. | -| [Web Access Rehaul](web-access-rehaul-plan.md) | Partial | Implementation is complete through Phase 5: fresh profiles get request-free onboarding disclosure and default-on anonymous Exa, Settings exposes 17 reviewed shipped providers with fenced Fixed/Automatic routing, autonomous authority remains explicit, and a startup-bound Exa-only rollback preserves hidden state. Focused suites, Settings E2E, build, development packaging, and hardened package verification pass; the credential-backed live installed matrix remains a release-owner acceptance gate. | +| Plan | Status | Current state | +| ---------------------------------------------------------------------------------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [Aiden Assistant](aiden-assistant-plan.md) | Partial | The dock, Markdown rendering, and confirmed provider-connection/model-pinned project-or-MCP automation creation/editing ship; settings tools and proactivity remain planned. | +| [Aiden On The Go](aiden-on-the-go-plan.md) | Active | Version 0.1.0 build 22 is `VALID` and `IN_BETA_TESTING` for Internal Testers. Android matches iOS's app-icon switcher, Workspace hierarchy, warm scoped Bots/Usage/SSE lifecycle, Usage dashboard, image showcase/gallery, keyboard-safe elevated composer, and split Photo/File pickers. Both clients support native in-process dictation or bounded no-retention transcription by the paired Mac's local Parakeet model. iOS also ships progressive onboarding, bidirectional media, reliable mobile approvals, typed activity timelines, semantic haptics, and one-chat-per-Bot conversations with companion vision for text-only models. Physical iPad/manual permission-system-UI acceptance, privacy publication, final store assets, and external/public-release decisions remain open. | +| [Bot-First Aiden On The Go](bot-first-aiden-on-the-go-plan.md) | Active | Phases 0–9 are implemented. Every Bot has one persistent chat and one contact row; Favorites are a pinned placement, Bot chat reuses the shared runtime with Messages-inspired identity/bubbles and Aiden's existing composer, and New/Edit Bot exclusively own its durable model. Remote open-or-create, immediate exact-cache chat entry, optimistic favorites, shaped skeleton loading, stable photos, atomic desktop creation, fresh-inventory save retries, conflict-safe Mac/iOS draft rebasing, final-only Bot replies with expandable progress, native-or-companion image handling, and internal TestFlight build 22 are green. Eligible Apple Intelligence hardware, physical iPad, multi-device/Mac, packaged rollback, live Telegram, wider staged TestFlight, Xcode 27, accessibility, and App Store owner gates remain open. | +| [Aiden Manual Pairing](aiden-manual-pairing-plan.md) | Implemented | The reviewed 100-bit setup-code path, shared one-use QR window, staged iOS activation, and adversarial coverage ship; hands-on LAN/Tailscale UI and physical-iPad acceptance remain open. | +| [Compaction](compaction-plan.md) | Partial | Pi-native checkpoints, lifecycle/crash recovery, and exact audited-upstream compatibility ship; durable memory and provider-native paths remain open. | +| [Designer Mode](designer-mode-plan.md) | Planned | Phase 0 validation has not started in the runtime. | +| [Dynamic Model Catalog](dynamic-model-catalog-plan.md) | Implemented | Validated pi.dev overlays, offline `0600` cache hydration, scoped setup refresh, four-hour launch refresh, force refresh, Pi metadata fallback, and Mac/iOS projection ship on pinned Pi 0.80.10. | +| [Generative UI Artifacts](generative-ui-artifacts-plan.md) | Active | Phases 0–6 shipped: chat-scoped `render_artifact`, strict sandboxed preview/export hosts, verified vendored Chart.js/Plotly/KaTeX, permission-aware `/visualize`, crash-recoverable authoritative storage/copies, descriptor-relative workspace reads, one-iframe handoff/expansion, visible failure states, and route-stable Responding/Visualizing activity. Three-agent PR review findings are remediated with focused regression coverage. | +| [Generation Progress Notes](generation-progress-notes-plan.md) | Planned | No implementation yet. | +| [Logging and Diagnostics Upgrade](logging-and-diagnostics-upgrade-plan.md) | Implemented | Phases 0–7 are implemented: bounded typed desktop journals, main-owned renderer evidence, local support export/delete, native categorical parity, and CI/release gates. Signed/notarized `v0.35.0` passed packaged diagnostics acceptance; physical-device termination receipts remain. | +| [Model Insights](model-insights-plan.md) | Partial | A dedicated benchmark-only OpenRouter key, manual fetch, exact source-aware offline cache, metric-selectable collision-free capability suggestions, progressive canvas-first Pad UX, axis provenance, attribution, and direct-AA retirement ship; device-local pace signals remain. | +| [Onboarding Authentication and Provider Validation](onboarding-auth-and-provider-validation-plan.md) | Active | Codex uses its dedicated auth surface, every interactive Pi provider can be configured during onboarding, OpenAI/Anthropic keys receive stronger catalog validation, completion is main-owned, and provider deferral stays explicit. | +| [Performance, Stability, Battery, and Efficiency](performance-stability-efficiency-plan.md) | Planned | Whole-app source audit is complete; implementation starts with instrumentation, durable state, and hard memory bounds. | +| [Pi Provider Integration](pi-provider-integration-plan.md) | Partial | Pi built-ins, stores, auth, native routing, custom provider composition, canonical assistant provenance, voice credential lookup, and attended structured questions ship; scalable UX and rollout cleanup remain. | +| [rpiv-todo Integration](rpiv-todo-integration-plan.md) | Partial | Attended desktop chats have a journal-replayed native todo tool, strict fail-closed snapshots, owner-fenced IPC, and a self-hiding floating progress chip with portal details; packaged visual/accessibility acceptance remains open. | +| [rpiv-btw Integration](rpiv-btw-integration-plan.md) | Partial | Attended desktop chats have bounded read-only side questions, ephemeral fingerprinted follow-ups, foreground-safe admission, exact provider dispatch, content-free usage accounting, and a native slash/card surface; packaged visual/accessibility acceptance remains open. | +| [Subagent Orchestration Expansion](subagent-orchestration-expansion-plan.md) | Active | Phases 0–6, Phase 7A durable lifecycle, the Phase 7B1 storage seam, startup/clone-boundary repairs, Pi-normalized foreground presentation and recovery, and parent-only iOS/Android projection hardening are complete; app-lifetime coordinator activation is next. | +| [Taracodlab Learnings](taracodlab-learnings-plan.md) | Partial | Phases A–B and D, plus core Phase E, are implemented; the remaining roadmap is open. | +| [Telegram First-Class Agent Parity](telegram-first-class-agent-parity-plan.md) | Active | Controls, compaction, skills, rich inbound, drafts/activity, buttons, documents, Settings, and onboarding are green; Threaded Mode/profiles/extensions/TTS/live smoke remain. | +| [Update, Microphone, and Computer Use Hardening](update-microphone-computer-use-hardening-plan.md) | Partial | Installed acceptance found a silent stalled download; observable full-download recovery is implemented, while repaired-build → next-release and clean-TCC acceptance remain. | +| [Web Access Rehaul](web-access-rehaul-plan.md) | Partial | Implementation is complete through Phase 5: fresh profiles get request-free onboarding disclosure and default-on anonymous Exa, Settings exposes 17 reviewed shipped providers with fenced Fixed/Automatic routing, autonomous authority remains explicit, and a startup-bound Exa-only rollback preserves hidden state. Focused suites, Settings E2E, build, development packaging, and hardened package verification pass; the credential-backed live installed matrix remains a release-owner acceptance gate. | ## Completed -| Plan | Status | Completion note | -| ---------------------------------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -| [Provider Model Visibility and Catalog Refresh](completed/provider-model-catalog-controls-plan.md) | Complete | Provider-wide visibility, explicit dual-source updates, bounded device-local models.dev metadata, native all-hidden behavior, and credential-isolated main refresh automation ship with two-review remediation. | -| [Aiden Remote Multi-Instance Hardening](completed/aiden-remote-multi-instance-hardening-plan.md) | Complete | Authenticated pairing completion, multi-device/Mac isolation, transactional listeners, explicit packaged Tailscale CLI mode, exact route ownership, durable revocation, and physical-iPhone acceptance all pass. | -| [Bots Mode](completed/bots-mode-plan.md) | Complete | Reusable Pi-backed bots now have first-class conversations, authoritative personas, soft archive, and exact one-to-one Telegram DM/topic control. | -| [Bot Avatar Studio](completed/bot-avatar-studio-plan.md) | Complete | Mouthless pastel faces now have live customization, crash-reconciled rollback storage, cancellable bounded Pi suggestions, and cohesive onboarding art. | -| [Companion Vision Models](completed/companion-vision-model-plan.md) | Complete | Vision primaries keep the native fast path; text-only Bots use one explicit exact-bound companion through a current-chat-only tool across Mac and iOS. | -| [Keyboard Command System](completed/keyboard-command-system-plan.md) | Complete | One command catalog now powers transactional global hotkeys, scoped app shortcuts, native menus, canonical settings, and the `Command-K` palette. | -| [Slash Commands and Skill Invocation](completed/slash-commands-and-skill-invocation-plan.md) | Complete | Separate `/` command and `$` skill palettes dispatch canonical app actions and fresh workspace-bound one-turn skills with safe provenance. | -| [Aiden-Native Subagents](completed/aiden-native-subagents-plan.md) | Complete | All five phases passed focused/package gates, two final fresh reviews, and the default 100-cycle packaged lifecycle soak. | -| [Development and Production Coexistence](completed/development-production-coexistence-plan.md) | Complete | Development now has a visibly distinct app identity, isolated state roots, opt-in global shortcuts, and production-only updates. | -| [Gemini Native Upgrade](completed/gemini-native-upgrade-plan.md) | Complete | Its funded delivery phases shipped; deliberately deferred Gemini tracks remain future work. | -| [Gemini 3.5 Voice Transcription](completed/gemini-3-5-voice-transcription-plan.md) | Complete | Dedicated Gemini 3.5 Live and batch transcription replace legacy Flash voice models, with ordered PCM streaming and WAV fallback. | -| [Gemini Voice Setup and Delivery Hardening](completed/gemini-voice-hardening-plan.md) | Complete | Purpose-scoped Gemini setup, enforced transcription-only policy, cancelable bounded dictation, explicit Accessibility recovery, and real offline Parakeet v3 inference are complete. | -| [Scheduled Tasks](completed/scheduled-tasks-plan.md) | Complete | Implemented through the plan's original Phase 4 scope. | -| [Scheduled Tasks Experience and Chat Creation](completed/scheduled-tasks-experience-plan.md) | Complete | Natural-language creation from eligible chats, human schedule controls, exact capability approvals, revision-safe lifecycle management, native parity, and migration hardening ship. | -| [Pi-native Compaction](completed/pi-native-compaction-plan.md) | Complete | Pi `0.80.10` session checkpoints, retained-tail reconstruction, overflow recovery, private journals, child parity, and activity milestones ship. | -| [Telegram Remote Control](completed/telegram-remote-control-plan.md) | Complete | Long-polling remote control ships with owner pairing, queueing, Markdown delivery, explicit optional folder-workspace authority via Settings or `/workspace`, isolated backing chats, Settings UI, focused tests, and onboarding coverage. | -| [Dictation Capture and Delivery](completed/dictation-capture-delivery-plan.md) | Complete | Trailing-audio flush, provider-wide shortcut/Accessibility, safer paste restore, hold-to-talk, isolated on-device STT, optional silence-stop, cleanup, and sounds. | -| [Pi Runtime Harness](completed/pi-runtime-harness-plan.md) | Complete | One durable Pi-shaped runtime now owns foreground/child lifecycle, contributions, effect evidence, killable child inference, and safe startup retry. | -| [Sidebar Chat Activity](completed/sidebar-chat-activity-plan.md) | Complete | Complete, revisioned per-chat activity appears as an accessible static ring with no polling or perpetual animation. | -| [Pi Compaction Compatibility](completed/compaction-reliability-plan.md) | Complete | Aiden's adapter now matches the audited Pi baseline 1:1 and persists only closed, privacy-safe provider-failure metadata for durable UI. | -| [Pi Thinking Disclosure](completed/pi-thinking-disclosure-plan.md) | Complete | Provider-neutral readable Pi thinking, a one-second inspectable preview, and a durable local presentation toggle now match the audited Pi contract. | +| Plan | Status | Completion note | +| -------------------------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| [Linux Desktop Support](completed/linux-desktop-support-plan.md) | Complete | Linux x64/arm64 AppImage, DEB, and RPM packages, explicit platform capability tradeoffs, native helpers, and hosted Ubuntu/Fedora package and E2E acceptance all pass. | +| [rpiv-advisor Integration](completed/rpiv-advisor-integration-plan.md) | Complete | Bounded, tool-free second opinions now use ephemeral provider/model selection, strict foreground authority, privacy-safe context projection, no-replay journaling, and content-free usage accounting. | +| [Provider Model Visibility and Catalog Refresh](completed/provider-model-catalog-controls-plan.md) | Complete | Provider-wide visibility, explicit dual-source updates, bounded device-local models.dev metadata, native all-hidden behavior, and credential-isolated main refresh automation ship with two-review remediation. | +| [Aiden Remote Multi-Instance Hardening](completed/aiden-remote-multi-instance-hardening-plan.md) | Complete | Authenticated pairing completion, multi-device/Mac isolation, transactional listeners, explicit packaged Tailscale CLI mode, exact route ownership, durable revocation, and physical-iPhone acceptance all pass. | +| [Bots Mode](completed/bots-mode-plan.md) | Complete | Reusable Pi-backed bots now have first-class conversations, authoritative personas, soft archive, and exact one-to-one Telegram DM/topic control. | +| [Bot Avatar Studio](completed/bot-avatar-studio-plan.md) | Complete | Mouthless pastel faces now have live customization, crash-reconciled rollback storage, cancellable bounded Pi suggestions, and cohesive onboarding art. | +| [Companion Vision Models](completed/companion-vision-model-plan.md) | Complete | Vision primaries keep the native fast path; text-only Bots use one explicit exact-bound companion through a current-chat-only tool across Mac and iOS. | +| [Keyboard Command System](completed/keyboard-command-system-plan.md) | Complete | One command catalog now powers transactional global hotkeys, scoped app shortcuts, native menus, canonical settings, and the `Command-K` palette. | +| [Slash Commands and Skill Invocation](completed/slash-commands-and-skill-invocation-plan.md) | Complete | Separate `/` command and `$` skill palettes dispatch canonical app actions and fresh workspace-bound one-turn skills with safe provenance. | +| [Aiden-Native Subagents](completed/aiden-native-subagents-plan.md) | Complete | All five phases passed focused/package gates, two final fresh reviews, and the default 100-cycle packaged lifecycle soak. | +| [Development and Production Coexistence](completed/development-production-coexistence-plan.md) | Complete | Development now has a visibly distinct app identity, isolated state roots, opt-in global shortcuts, and production-only updates. | +| [Gemini Native Upgrade](completed/gemini-native-upgrade-plan.md) | Complete | Its funded delivery phases shipped; deliberately deferred Gemini tracks remain future work. | +| [Gemini 3.5 Voice Transcription](completed/gemini-3-5-voice-transcription-plan.md) | Complete | Dedicated Gemini 3.5 Live and batch transcription replace legacy Flash voice models, with ordered PCM streaming and WAV fallback. | +| [Gemini Voice Setup and Delivery Hardening](completed/gemini-voice-hardening-plan.md) | Complete | Purpose-scoped Gemini setup, enforced transcription-only policy, cancelable bounded dictation, explicit Accessibility recovery, and real offline Parakeet v3 inference are complete. | +| [Scheduled Tasks](completed/scheduled-tasks-plan.md) | Complete | Implemented through the plan's original Phase 4 scope. | +| [Scheduled Tasks Experience and Chat Creation](completed/scheduled-tasks-experience-plan.md) | Complete | Natural-language creation from eligible chats, human schedule controls, exact capability approvals, revision-safe lifecycle management, native parity, and migration hardening ship. | +| [Pi-native Compaction](completed/pi-native-compaction-plan.md) | Complete | Pi `0.80.10` session checkpoints, retained-tail reconstruction, overflow recovery, private journals, child parity, and activity milestones ship. | +| [Telegram Remote Control](completed/telegram-remote-control-plan.md) | Complete | Long-polling remote control ships with owner pairing, queueing, Markdown delivery, explicit optional folder-workspace authority via Settings or `/workspace`, isolated backing chats, Settings UI, focused tests, and onboarding coverage. | +| [Dictation Capture and Delivery](completed/dictation-capture-delivery-plan.md) | Complete | Trailing-audio flush, provider-wide shortcut/Accessibility, safer paste restore, hold-to-talk, isolated on-device STT, optional silence-stop, cleanup, and sounds. | +| [Pi Runtime Harness](completed/pi-runtime-harness-plan.md) | Complete | One durable Pi-shaped runtime now owns foreground/child lifecycle, contributions, effect evidence, killable child inference, and safe startup retry. | +| [Sidebar Chat Activity](completed/sidebar-chat-activity-plan.md) | Complete | Complete, revisioned per-chat activity appears as an accessible static ring with no polling or perpetual animation. | +| [Pi Compaction Compatibility](completed/compaction-reliability-plan.md) | Complete | Aiden's adapter now matches the audited Pi baseline 1:1 and persists only closed, privacy-safe provider-failure metadata for durable UI. | +| [Pi Thinking Disclosure](completed/pi-thinking-disclosure-plan.md) | Complete | Provider-neutral readable Pi thinking, a one-second inspectable preview, and a durable local presentation toggle now match the audited Pi contract. | Move a plan to `completed/` only when its original delivery scope is complete. Keep the original plan as historical documentation; follow-on work belongs in a new active plan. diff --git a/docs/plans/completed/linux-desktop-support-plan.md b/docs/plans/completed/linux-desktop-support-plan.md new file mode 100644 index 00000000..60ac38d1 --- /dev/null +++ b/docs/plans/completed/linux-desktop-support-plan.md @@ -0,0 +1,191 @@ +# Linux Desktop Support + +Status: Complete — implementation, native acceptance, and hosted x64/arm64/Fedora acceptance complete 2026-08-30 + +## Goal + +Ship Aiden Agent as a first-class Linux desktop application that installs and +runs on the common Debian/Ubuntu, Fedora/RHEL, and openSUSE families, with a +portable AppImage option. Preserve the existing macOS experience while making +platform-specific behavior explicit, secure, tested, and maintainable. + +## Supported baseline + +- Architectures: x86_64 and arm64. +- Packages: AppImage, `.deb`, and `.rpm`. +- Runtime baseline: glibc 2.34 or newer; package verification rejects native + executables or modules that raise that floor. +- Display servers: X11 and Wayland. Window placement that Wayland deliberately + forbids is treated as a capability limitation rather than emulated. +- Desktop integration: native Linux window frame/menu, desktop notifications, + default file manager, common installed editors, and Secret Service/KWallet + credential encryption. +- Computer Use, Apple Foundation Models, and Bots remain macOS-only. Linux omits + their helpers, settings/navigation actions, onboarding promises, and runtime + tool exposure while retaining the shared implementations for capable hosts. + +## Research-backed tradeoffs + +1. Electron supports Linux x64 and arm64, while electron-builder directly + supports AppImage, Debian, and RPM targets. Native dependencies and Aiden's + own helper executables must be built and verified on the target OS rather + than copied from macOS. +2. Linux `safeStorage` can fall back to Electron's `basic_text` backend. Aiden + will fail closed for provider and OAuth secrets when no desktop keyring is + available, with an actionable error, instead of silently storing secrets + with the hard-coded fallback key. +3. Wayland does not allow applications to position or programmatically focus + windows in all compositors. Global dictation therefore guarantees capture + and clipboard delivery on Linux, while exact floating-pill placement and + automatic paste remain macOS conveniences. +4. Linux uses a conventional native frame. macOS keeps hidden-inset traffic + lights, vibrancy, and the Dock-icon preference; Linux does not expose those + controls. +5. Native profile sharing becomes a Save dialog on Linux. Opening a workspace + uses the system file manager, and supported editors are discovered from + executable paths/Flatpak installations rather than macOS bundles/Spotlight. +6. Linux packages initially use explicit download/install updates. The current + signed macOS updater stays unchanged; silently treating `.deb`/`.rpm` + replacement as equivalent would bypass distribution ownership and package + manager expectations. + +Primary references: + +- [Electron supported platforms](https://www.electronjs.org/docs/latest/tutorial/installation) +- [Electron safeStorage](https://www.electronjs.org/docs/latest/api/safe-storage) +- [Electron Linux notifications](https://www.electronjs.org/docs/latest/tutorial/notifications) +- [Electron custom title bars](https://www.electronjs.org/docs/latest/tutorial/custom-title-bar) +- [electron-builder Linux targets](https://www.electron.build/docs/linux/) +- [electron-builder cross-platform builds](https://www.electron.build/docs/features/multi-platform-build/) + +## Delivery phases + +### Phase 1 — audit, research, and support contract + +- Inventory macOS assumptions in startup, windows, menus, permissions, + packaging, helper binaries, onboarding, settings, and release automation. +- Establish the supported distro/package matrix and deliberate limitations. +- Add this plan to the canonical plan inventory. + +Review gate: confirm that every default-on service either has a Linux path or +is explicitly capability-gated before implementation begins. + +### Phase 2 — Linux build, package, and runtime foundation + +- Make development startup platform-neutral. +- Add Linux AppImage, Debian, and RPM packaging with x64/arm64 metadata, + Linux runtime dependencies, icons, native helpers, unpacked native modules, + and fuse/package verification. +- Port the worktree remover, subagent run store, file mutator, and shell runner + to Linux without weakening their path, identity, or atomicity contracts. +- Add platform-safe window construction, menu construction, terminal shell + selection, and secure-storage selection. + +Review gate: build and test all Linux native helpers, create an unpacked Linux +package, inspect its resources/fuses/permissions, and run focused runtime tests. + +### Phase 3 — platform capabilities and service adaptations + +- Advertise typed host capabilities to the renderer. +- Omit Computer Use and Apple Foundation Models on Linux. +- Adapt microphone permission, dictation delivery, profile sharing, external + editors/file manager, Tailscale discovery, and LAN service publication. +- Remove or replace macOS-only controls and promises in Settings/onboarding + while retaining clear explanations for deliberate Linux limitations. + +Review gate: exercise every changed IPC boundary and verify that unsupported +features cannot be enabled or invoked through stale renderer state. + +### Phase 4 — CI, distribution, documentation, and acceptance + +- Add Linux unit/integration, native-helper, package-contract, and Electron E2E + coverage under Xvfb. +- Add x64 and arm64 Linux artifact workflows. +- Document install, keyring, Wayland, package ownership, and update behavior. +- Run type-check, lint, focused suites, full tests, Linux package verification, + and smoke launch acceptance. + +Review gate: freeze the diff, perform a final platform/security regression +review, and archive this plan only when the complete acceptance matrix passes. + +## Implementation and review record + +- Phase 1 complete: audited startup, windows, menus, permissions, helpers, + packaging, onboarding, settings, Remote Access, companion copy, and release + automation. Every default-on service now has a Linux implementation or an + explicit main-owned capability gate. +- Phase 2 complete: Linux development startup, x64/arm64 AppImage/DEB/RPM + configuration, package/fuse verification, native C helper portability, + Linux PTY layouts, conventional window/menu behavior, and fail-closed + keyring selection are implemented. Clean target-architecture builds produced + and verified all three package formats and every native helper/module. +- Phase 3 complete: Computer Use and Apple Foundation Models are inaccessible + on Linux; Settings, onboarding, profile export, dictation, shortcuts, + editors/file manager, Tailscale, nearby mobile discovery, and Aiden On The + Go copy follow the advertised platform capabilities. Review additionally + fixed Linux safe-save recovery ownership and Remote Access publisher races. +- Latest-main parity reconciliation retains Web Search, diagnostics, Gemini + voice transcription, Model Pad, companion projections, raster and sandboxed + generative UI artifacts, and response/accessibility improvements on Linux. + Chat-native Scheduled Tasks, including revision-bound remote runs and shared + desktop/mobile presentation, are also platform-neutral on Linux. + The attended-chat Ask User Question composer and the native todo, BTW, and + Advisor Pi extensions are also platform-neutral and covered by both Ubuntu + and Fedora Linux CI gates. + The shared capability projection now also hides Bots and all Bot entry points + until Linux receives equivalent native security bindings. Settings and + Command-K share the same availability filter, dictation never invokes macOS + Accessibility on Linux, and model metadata remains a release-bundled offline + snapshot with no live models.dev UI action. +- Phase 4 implementation complete: CI builds x64 and arm64 artifacts, installs + and verifies DEB on Ubuntu 24.04 and RPM on Fedora 44, and runs Electron E2E + under Xvfb. Release publication requires both Linux architectures alongside + the verified macOS artifacts. Linux installation and limitation guidance is + documented in `docs/linux.md`. +- Native acceptance passes on both x64 and arm64 Ubuntu 24.04: package + verification, DEB installation, RPM metadata, AppImage execution, native + linkage, desktop association, and fresh empty-XDG X11 startup. Debian 12 + additionally proves the glibc 2.36/legacy-ALSA path; Fedora 44 and openSUSE + Tumbleweed prove RPM dependency resolution; headless Weston proves Wayland + startup. The complete deterministic Electron E2E suite also runs under Xvfb. +- The final adversarial pass fixed several defects that metadata-only checks + missed: Linux's seven-field native generation token, a glibc 2.38 symbol + leak, Debian's false ALSA virtual provider, architecture-specific release + filenames, a dynamically linked arm64 AppImage launcher, non-ELF `.node` + verifier inputs, AppleDouble test sidecars, stripped X11/Wayland launch + variables, keyless onboarding incorrectly requiring a desktop keyring, and + RPM integrity checks rejecting electron-builder's exact sandbox-mode fallback. + It also corrected Linux last-window shutdown so enabled Remote Access retains + background ownership without changing the ordinary last-window quit policy. +- Local regression acceptance passes `type-check`, E2E type-check, lint, Linux + contracts, helper/parser adversarial suites, package/publisher tests, and the + production TypeScript-to-native run-store boundary. +- Live OrbStack acceptance now also exercises the installed Linux + `/usr/bin/tailscale` client from the real Aiden controller. A separate + tailnet peer reached the exact HTTPS `/api/aiden/v1/health` contract, and + teardown removed only Aiden's scoped Serve route. This pass added strict + `Running`/online status checks and actionable detection of Linux's required + one-time Tailscale operator grant. + +Hosted run `33338723528` completed the release-infrastructure gate: Linux x64, +Linux arm64, the Fedora RPM artifact consumer, deterministic Electron E2E, +Android, and the full verification job all passed. The final CI hardening uses +SIGKILL for bounded Electron smoke teardown and hands Fedora a checksum-verified +RPM built against the Ubuntu baseline, preserving the strict glibc 2.34 floor. +Together with the native acceptance matrix above, this completes the plan's +original delivery scope. + +## Acceptance criteria + +- A fresh supported Linux desktop can install an appropriate Aiden package and + complete onboarding without encountering a macOS-only control or helper. +- Chat, providers, local runtimes, MCP, terminal, workspaces/worktrees, + subagents, schedules, notifications, local voice, Telegram, and Aiden Remote + retain their existing contracts on Linux or disclose a documented platform + limitation before the user acts. +- Secret material is never written through Electron's Linux `basic_text` + backend. +- AppImage, Debian, and RPM contents include the correct-architecture native + modules/helpers and exclude Computer Use and Apple-only helper artifacts. +- macOS packaging, signing, notarization, UI, and feature availability remain + regression-tested and unchanged except for shared platform abstractions. diff --git a/docs/plans/rpiv-advisor-integration-plan.md b/docs/plans/completed/rpiv-advisor-integration-plan.md similarity index 99% rename from docs/plans/rpiv-advisor-integration-plan.md rename to docs/plans/completed/rpiv-advisor-integration-plan.md index 006818f9..0b7343c9 100644 --- a/docs/plans/rpiv-advisor-integration-plan.md +++ b/docs/plans/completed/rpiv-advisor-integration-plan.md @@ -1,6 +1,6 @@ # rpiv-advisor integration -Status: Implemented (2026-08-30) +Status: Complete (2026-08-30) ## Objective diff --git a/ios/APP_STORE_METADATA.md b/ios/APP_STORE_METADATA.md index 0606c99b..50ef60a2 100644 --- a/ios/APP_STORE_METADATA.md +++ b/ios/APP_STORE_METADATA.md @@ -30,15 +30,15 @@ Developer Tools is the closest current Apple category: Apple describes it as app Description draft: -> Aiden On The Go is the native iPhone and iPad companion for Aiden Agent on your Mac. Pair directly with a Mac you control over your local network or Tailscale, then continue Aiden chats and manage workspaces from your mobile device. +> Aiden On The Go is the native iPhone and iPad companion for Aiden Agent on your macOS or Linux desktop. Pair directly with a desktop you control over your local network or Tailscale, then continue Aiden chats and manage workspaces from your mobile device. > -> Review conversations, stream responses, handle approval requests, inspect workspace files, work with supported Git flows, and manage scheduled tasks. App Intents provide quick navigation, Live Activities show bounded run status, and optional voice dictation can use the device's native recognizer or a local speech model on your paired Mac. Read-aloud stays on device. +> Review conversations, stream responses, handle approval requests, inspect workspace files, work with supported Git flows, and manage scheduled tasks. App Intents provide quick navigation, Live Activities show bounded run status, and optional voice dictation can use the device's native recognizer or a local speech model on your paired desktop. Read-aloud stays on device. > -> Your Mac remains the execution authority. Remote Access is off by default, each mobile device uses a revocable credential, and provider credentials remain on the Mac. +> Your desktop remains the execution authority. Remote Access is off by default, each mobile device uses a revocable credential, and provider credentials remain on the desktop. Review-notes draft: -> Aiden On The Go requires the companion Aiden Agent desktop app. In Aiden Agent, open Settings → Remote Access, enable the listener, and open a short-lived pairing session. On the iPhone or iPad, scan the pairing QR code or use the approved manual connection flow. The reviewer must be supplied with a reachable review Mac and any required setup instructions; no production credential is embedded in the app. +> Aiden On The Go requires the companion Aiden Agent desktop app. In Aiden Agent, open Settings → Remote Access, enable the listener, and open a short-lived pairing session. On the iPhone or iPad, scan the pairing QR code or use the approved manual connection flow. The reviewer must be supplied with a reachable review desktop and any required setup instructions; no production credential is embedded in the app. ## Age-rating questionnaire draft @@ -75,11 +75,11 @@ Current Apple references: Evidence for that answer in the current distribution candidate: - The app contains no analytics, advertising, crash-reporting, account, or Aiden-hosted relay SDK. -- Pairing credentials and custom headers stay in Keychain. Cached chats/settings stay on the user's device; authoritative chats/files remain on the Mac the user pairs. +- Pairing credentials and custom headers stay in Keychain. Cached chats/settings stay on the user's device; authoritative chats/files remain on the desktop the user pairs. - QR camera frames are processed for pairing and are not uploaded to Aiden's developer. -- Dictation uses either Apple’s native recognizer or, only when the person selects **Paired Mac**, a bounded recording sent directly over the authenticated pinned-TLS connection to the Mac-local Parakeet model. Neither endpoint retains that recording. Read-aloud uses Apple system frameworks locally. Aiden's developer operates no speech collection service. -- Photos/files selected by the user are sent directly to their paired Mac and may then be sent to model providers the user configured on that Mac. Aiden's developer cannot access them. Provider processing remains governed by each selected provider and should be described in the public policy. -- Optional Bot image generation uses Apple's system Image Playground on supported devices and may use Private Cloud Compute. Aiden disables person/Photos personalization and supplies only the visible Bot name and purpose as starting concepts. Aiden's developer runs no image-generation or proxy service and cannot access those concepts, rejected candidates, or results. When the person chooses **Use this image**, the app sends only that normalized image directly to the paired Mac, which stores the canonical Bot photo. +- Dictation uses either Apple’s native recognizer or, only when the person selects **Paired desktop**, a bounded recording sent directly over the authenticated pinned-TLS connection to the desktop-local Parakeet model. Neither endpoint retains that recording. Read-aloud uses Apple system frameworks locally. Aiden's developer operates no speech collection service. +- Photos/files selected by the user are sent directly to their paired desktop and may then be sent to model providers the user configured there. Aiden's developer cannot access them. Provider processing remains governed by each selected provider and should be described in the public policy. +- Optional Bot image generation uses Apple's system Image Playground on supported devices and may use Private Cloud Compute. Aiden disables person/Photos personalization and supplies only the visible Bot name and purpose as starting concepts. Aiden's developer runs no image-generation or proxy service and cannot access those concepts, rejected candidates, or results. When the person chooses **Use this image**, the app sends only that normalized image directly to the paired desktop, which stores the canonical Bot photo. - Local Network or Tailscale traffic goes directly to the paired installation. Aiden does not run a central account, synchronization service, analytics endpoint, or proxy. - Live Activity state is device-local and response excerpts are off by default. - External transcript media can contact the media host without forwarding Aiden credentials; the public policy should disclose that a remote host can observe an ordinary network request when its media is displayed. @@ -111,7 +111,7 @@ Current Apple reference: `https://developer.apple.com/help/app-store-connect/ref - Owner/legal-review and publish `app-store/MOBILE_PRIVACY_SUPPORT_COPY.md` at the resolved privacy URL. - Make the prepared working support contact visible at the resolved support URL. - Required physical-iPhone and physical-iPad screenshots captured from the final distribution candidate at accepted dimensions. -- App Review phone number, notes, and a reachable companion-Mac review environment. The name/email are resolved above. +- App Review phone number, notes, and a reachable companion-desktop review environment. The name/email are resolved above. - Availability, price, territories, and release mode. Do not replace unresolved values with placeholders in App Store Connect. @@ -120,10 +120,10 @@ Do not replace unresolved values with placeholders in App Store Connect. Provide these notes only with a reachable, reviewer-safe paired Aiden Agent environment and the final approved contact details: -1. Pair the iPhone or iPad with the supplied Aiden Agent Mac, then tap the Aiden logo and choose **Bots**. -2. Accept the one-time Full Access notice or choose **Customize first**. Full Access uses only capabilities already enabled on the paired Mac; Custom can reduce Files, commands, Connections, and Skills. +1. Pair the iPhone or iPad with the supplied Aiden Agent desktop, then tap the Aiden logo and choose **Bots**. +2. Accept the one-time Full Access notice or choose **Customize first**. Full Access uses only capabilities already enabled on the paired desktop; Custom can reduce Files, commands, Connections, and Skills. 3. Create a Bot with the built-in semantic avatar, save it, and start a chat. Apple Intelligence is not required for this complete path. -4. On eligible Apple Intelligence hardware with iOS/iPadOS 18.4 or later, **Create with Apple Intelligence** opens Apple's system Image Playground. Apple controls generation and may use Private Cloud Compute. Aiden disables person/Photos personalization and sends the paired Mac only the image explicitly accepted and saved. +4. On eligible Apple Intelligence hardware with iOS/iPadOS 18.4 or later, **Create with Apple Intelligence** opens Apple's system Image Playground. Apple controls generation and may use Private Cloud Compute. Aiden disables person/Photos personalization and sends the paired desktop only the image explicitly accepted and saved. 5. On unsupported hardware, including iPhone 13 Pro, the editor honestly keeps the semantic avatar available and has no dead Image Playground action. Do not claim successful Image Playground generation in review notes until it has passed on supported physical hardware. Do not include pairing credentials, private prompts, paths, or provider secrets in metadata or notes. diff --git a/ios/AidenOnTheGo/Auth/KeychainStore.swift b/ios/AidenOnTheGo/Auth/KeychainStore.swift index dd645be7..bad497e3 100644 --- a/ios/AidenOnTheGo/Auth/KeychainStore.swift +++ b/ios/AidenOnTheGo/Auth/KeychainStore.swift @@ -7,7 +7,7 @@ protocol KeychainStoring { func delete(_ key: KeychainStore.Key) throws // A device credential is scoped to Aiden's stable installation identifier. - // Switching or removing one paired Mac must never read or clear another + // Switching or removing one paired desktop must never read or clear another // installation's credential. func save(_ value: String, forKey key: KeychainStore.Key, scope: String) throws func load(_ key: KeychainStore.Key, scope: String) throws -> String? diff --git a/ios/AidenOnTheGo/Config/AidenVoiceInput.swift b/ios/AidenOnTheGo/Config/AidenVoiceInput.swift index ee77d3cd..c1662873 100644 --- a/ios/AidenOnTheGo/Config/AidenVoiceInput.swift +++ b/ios/AidenOnTheGo/Config/AidenVoiceInput.swift @@ -7,7 +7,7 @@ enum AidenVoiceInputMode: String, CaseIterable, Identifiable, Codable, Sendable static let defaultsKey = "aiden.voiceInput.mode" var id: String { rawValue } - var title: String { self == .onDevice ? String(localized: "On this device") : String(localized: "Paired Mac") } + var title: String { self == .onDevice ? String(localized: "On this device") : String(localized: "Paired desktop") } static var selected: AidenVoiceInputMode { AidenVoiceInputMode(rawValue: UserDefaults.standard.string(forKey: defaultsKey) ?? "") ?? .onDevice diff --git a/ios/AidenOnTheGo/Features/Bots/AidenBotCustomAccessFlowView.swift b/ios/AidenOnTheGo/Features/Bots/AidenBotCustomAccessFlowView.swift index b1b545e0..b4f81cb3 100644 --- a/ios/AidenOnTheGo/Features/Bots/AidenBotCustomAccessFlowView.swift +++ b/ios/AidenOnTheGo/Features/Bots/AidenBotCustomAccessFlowView.swift @@ -360,7 +360,7 @@ struct AidenBotCustomAccessFlowView: View { } header: { Text("Bot") } footer: { - Text("Custom Access can only reduce what Aiden and your Mac already allow. Change the AI Provider or Model in Edit Bot.") + Text("Custom Access can only reduce what Aiden and the paired desktop already allow. Change the AI Provider or Model in Edit Bot.") } if isLoadingBot, draft == nil { @@ -419,7 +419,7 @@ struct AidenBotCustomAccessFlowView: View { .disabled(!canWrite) optionSection( title: "Other Capabilities", - description: "Optional Aiden capabilities available on this Mac.", + description: "Optional Aiden capabilities available on the paired desktop.", options: catalog.otherCapabilities, keyPath: \.otherCapabilityIDs ) @@ -462,7 +462,7 @@ struct AidenBotCustomAccessFlowView: View { Toggle("Run commands", isOn: shellBinding(catalog)) .disabled(!catalog.shellAvailable && !(draft?.shellEnabled ?? false)) - .accessibilityHint("Allows the bot to use Aiden’s existing shell tool on your Mac.") + .accessibilityHint("Allows the bot to use Aiden’s existing shell tool on the paired desktop.") } header: { Text("Files and Commands") } footer: { @@ -483,7 +483,7 @@ struct AidenBotCustomAccessFlowView: View { ) return Section { if visibleOptions.isEmpty { - Text("None configured on this Mac") + Text("None configured on the paired desktop") .foregroundStyle(palette.secondary) } else { ForEach(visibleOptions) { option in @@ -709,7 +709,7 @@ struct AidenBotCustomAccessFlowView: View { capturedContext == request.context, selectedBotID == request.botID else { return } guard let loadedDraft = AidenBotCustomAccessDraft(access: detail.access, catalog: catalog) else { - botError = "No available AI provider and model can be selected on your Mac." + botError = "No available AI provider and model can be selected on your paired desktop." return } selectedBot = detail @@ -800,7 +800,7 @@ struct AidenBotCustomAccessFlowView: View { access: authoritative.access, catalog: refreshedCatalog ) else { - saveError = "Access may have changed on your Mac. Close and reopen this screen to refresh." + saveError = "Access may have changed on your paired desktop. Close and reopen this screen to refresh." return } selectedBot = authoritative diff --git a/ios/AidenOnTheGo/Features/Bots/AidenBotEditorView.swift b/ios/AidenOnTheGo/Features/Bots/AidenBotEditorView.swift index faf34b25..0e2a8fb0 100644 --- a/ios/AidenOnTheGo/Features/Bots/AidenBotEditorView.swift +++ b/ios/AidenOnTheGo/Features/Bots/AidenBotEditorView.swift @@ -562,7 +562,7 @@ struct AidenBotEditorView: View { .disabled(draft?.usesFullAccess == true) optionSection( title: "Other Capabilities", - description: "Additional capabilities available on this Mac.", + description: "Additional capabilities available on the paired desktop.", options: catalog.otherCapabilities, keyPath: \.otherCapabilityIDs ) @@ -685,14 +685,14 @@ struct AidenBotEditorView: View { .accessibilityHint("Custom Access can reduce the capabilities this Bot may use.") if draft?.usesFullAccess == true { - Label("Uses everything Aiden and your Mac currently allow.", systemImage: "checkmark.shield") + Label("Uses everything Aiden and the paired desktop currently allow.", systemImage: "checkmark.shield") .foregroundStyle(palette.secondary) } } header: { Text("Access") } footer: { if !AidenBotEditorDraft.fullAccessAccepted(in: catalog) { - Text("Full Access is unavailable because Customize First was selected for this Mac.") + Text("Full Access is unavailable because Customize First was selected for this desktop.") } else { Text("Connections and Skills are the most important controls when using Custom Access.") } @@ -730,7 +730,7 @@ struct AidenBotEditorView: View { if visionProviders(in: catalog).isEmpty { Label( - "No image-capable model is connected. Add one in Aiden Agent on your Mac, then refresh this Bot.", + "No image-capable model is connected. Add one in Aiden Agent on your paired desktop, then refresh this Bot.", systemImage: "exclamationmark.triangle" ) .foregroundStyle(palette.secondary) @@ -759,7 +759,7 @@ struct AidenBotEditorView: View { } header: { Text("AI Provider and Model") } footer: { - Text("This Bot uses this Provider and Model in every chat. Credentials stay on your Mac.") + Text("This Bot uses this Provider and Model in every chat. Credentials stay on the paired desktop.") } } @@ -781,7 +781,7 @@ struct AidenBotEditorView: View { } header: { Text("Files and Commands") } footer: { - Text("Choose which files the Bot may work with and whether it may run commands on the paired Mac.") + Text("Choose which files the Bot may work with and whether it may run commands on the paired desktop.") } } @@ -793,7 +793,7 @@ struct AidenBotEditorView: View { ) -> some View { Section { if options.isEmpty { - Text("None configured on this Mac") + Text("None configured on the paired desktop") .foregroundStyle(palette.secondary) } else { ForEach(options) { option in @@ -823,7 +823,7 @@ struct AidenBotEditorView: View { ) if draft?.usesFullAccess == true { Label( - "Full Access: files, commands, Connections, and Skills allowed by the paired Mac", + "Full Access: files, commands, Connections, and Skills allowed by the paired desktop", systemImage: "checkmark.shield" ) } else if let access = draft?.customAccess { @@ -1051,7 +1051,7 @@ struct AidenBotEditorView: View { private var readOnlyMessage: String { if baselineBot?.health == .archived { return "Archived Bots are read-only until restored." } - if coordinator.connectionState != .connected { return "Reconnect to your Mac to save this Bot." } + if coordinator.connectionState != .connected { return "Reconnect to your paired desktop to save this Bot." } return "This phone can view Bots but is not approved to change them." } @@ -1354,7 +1354,7 @@ struct AidenBotEditorView: View { onSaved(authoritative) dismiss() } else { - saveError = "Aiden checked the Bot on your Mac. Review any remaining changes, then save again." + saveError = "Aiden checked the Bot on your paired desktop. Review any remaining changes, then save again." } } catch is CancellationError { return @@ -1365,7 +1365,7 @@ struct AidenBotEditorView: View { ) { return } guard isCurrent(attempt) else { return } capturedContext = nil - saveError = "Aiden couldn’t verify which changes reached your Mac. Close and reopen this Bot before editing again." + saveError = "Aiden couldn’t verify which changes reached your paired desktop. Close and reopen this Bot before editing again." } } } diff --git a/ios/AidenOnTheGo/Features/Bots/AidenBotGeneratedAvatarLifecycle.swift b/ios/AidenOnTheGo/Features/Bots/AidenBotGeneratedAvatarLifecycle.swift index e098048f..d2672307 100644 --- a/ios/AidenOnTheGo/Features/Bots/AidenBotGeneratedAvatarLifecycle.swift +++ b/ios/AidenOnTheGo/Features/Bots/AidenBotGeneratedAvatarLifecycle.swift @@ -21,7 +21,7 @@ enum AidenBotGeneratedAvatarError: Error, LocalizedError, Equatable { case .invalidImage: "Aiden couldn’t prepare that image. Choose another image." case .unavailable: - "Reconnect to your Mac before saving this Bot photo." + "Reconnect to your paired desktop before saving this Bot photo." } } } @@ -464,7 +464,7 @@ final class AidenBotGeneratedAvatarModel { self.candidateBytes = nil candidateImage = nil phase = .idle - errorMessage = "The Bot photo changed on your Mac. Review it before choosing a new image." + errorMessage = "The Bot photo changed on your paired desktop. Review it before choosing a new image." return } attempt = .init( @@ -569,7 +569,7 @@ final class AidenBotGeneratedAvatarModel { } guard fresh.avatar.asset?.assetRevision == observedAssetRevision else { phase = .idle - errorMessage = "The Bot photo changed on your Mac. Review it before removing it." + errorMessage = "The Bot photo changed on your paired desktop. Review it before removing it." return } attempt = .init( @@ -730,7 +730,7 @@ final class AidenBotGeneratedAvatarModel { candidateBytes = nil candidateImage = nil phase = .idle - errorMessage = "The Bot photo changed on your Mac. Review the current photo before replacing it." + errorMessage = "The Bot photo changed on your paired desktop. Review the current photo before replacing it." } } catch is CancellationError { if isCurrent(attempt.context, generation: generation), uploadAttempt == attempt { @@ -745,7 +745,7 @@ final class AidenBotGeneratedAvatarModel { } guard isCurrent(attempt.context, generation: generation), uploadAttempt == attempt else { return } phase = .ready - errorMessage = "Aiden couldn’t verify which photo reached your Mac. Reconnect, then retry this same upload." + errorMessage = "Aiden couldn’t verify which photo reached your paired desktop. Reconnect, then retry this same upload." } } @@ -799,7 +799,7 @@ final class AidenBotGeneratedAvatarModel { } else { deleteAttempt = nil phase = .idle - errorMessage = "The Bot photo changed on your Mac. Review it before trying again." + errorMessage = "The Bot photo changed on your paired desktop. Review it before trying again." } } catch is CancellationError { if isCurrent(attempt.context, generation: generation), deleteAttempt == attempt { @@ -1020,7 +1020,7 @@ struct AidenBotGeneratedAvatarLifecycleView: View { } Button("Cancel", role: .cancel) { } } message: { - Text("This removes the generated Bot photo from your paired Mac.") + Text("This removes the generated Bot photo from your paired desktop.") } } @@ -1043,8 +1043,8 @@ struct AidenBotGeneratedAvatarLifecycleView: View { } private var statusCopy: String { - if model.hasCandidate { return "Only this accepted image will be sent to your paired Mac." } - if model.hasGeneratedAvatar { return "Saved on your paired Mac." } + if model.hasCandidate { return "Only this accepted image will be sent to your paired desktop." } + if model.hasGeneratedAvatar { return "Saved on your paired desktop." } return "Your semantic avatar is always available." } diff --git a/ios/AidenOnTheGo/Features/Bots/AidenBotProfileView.swift b/ios/AidenOnTheGo/Features/Bots/AidenBotProfileView.swift index d94dbda0..2d601bc7 100644 --- a/ios/AidenOnTheGo/Features/Bots/AidenBotProfileView.swift +++ b/ios/AidenOnTheGo/Features/Bots/AidenBotProfileView.swift @@ -427,7 +427,7 @@ struct AidenBotProfileView: View { .accessibilityHint( detail.health == .ready ? "Opens this Bot’s persistent conversation." - : "Repair this Bot’s access on the paired Mac before starting its conversation." + : "Repair this Bot’s access on the paired desktop before starting its conversation." ) } diff --git a/ios/AidenOnTheGo/Features/Bots/AidenBotsHomeView.swift b/ios/AidenOnTheGo/Features/Bots/AidenBotsHomeView.swift index c95e8031..fef6118b 100644 --- a/ios/AidenOnTheGo/Features/Bots/AidenBotsHomeView.swift +++ b/ios/AidenOnTheGo/Features/Bots/AidenBotsHomeView.swift @@ -278,7 +278,7 @@ func aidenBotInboxActivityStatus( case .waitingForApproval: canRespondToApproval ? .init(label: "Approval needed", symbol: "checkmark.shield") - : .init(label: "Waiting for approval on Mac", symbol: "desktopcomputer") + : .init(label: "Waiting for desktop approval", symbol: "desktopcomputer") case .reconciling: .init(label: "Updating", symbol: "arrow.triangle.2.circlepath") } } @@ -564,7 +564,7 @@ struct AidenBotsHomeView: View { Text( coordinator.connectionState == .connected ? "Create a Bot to give a familiar helper one persistent conversation and its own capabilities." - : "Reconnect to your Mac to load Bots." + : "Reconnect to your paired desktop to load Bots." ) } actions: { if coordinator.connectionState == .connected { diff --git a/ios/AidenOnTheGo/Features/Bots/Prototype/BotFirstPrototype.swift b/ios/AidenOnTheGo/Features/Bots/Prototype/BotFirstPrototype.swift index a841252e..b07c6a79 100644 --- a/ios/AidenOnTheGo/Features/Bots/Prototype/BotFirstPrototype.swift +++ b/ios/AidenOnTheGo/Features/Bots/Prototype/BotFirstPrototype.swift @@ -120,7 +120,7 @@ private enum AidenBotPrototypeChatAccess: String, CaseIterable, Identifiable, Ha } private enum AidenBotPrototypeFileAccess: String, CaseIterable, Identifiable, Hashable { - case fullMac = "Full Mac" + case fullMac = "Full desktop" case botFolderOnly = "Bot folder only" case chosenLocations = "Chosen locations" case off = "Off" @@ -711,9 +711,9 @@ private struct AidenBotPrototypeFullAccessNoticeView: View { .background(palette.accent.opacity(0.12), in: Circle()) VStack(alignment: .leading, spacing: 10) { - Text("Bots can use your Mac") + Text("Bots can use your paired desktop") .font(.largeTitle.bold()) - Text("By default, bots can work with files, run commands, and use connections, skills, and AI configured on the paired Mac. Capabilities you enable later in Aiden are also available to Full Access bots. You can choose Custom Access now or reduce access in Bot Settings anytime.") + Text("By default, bots can work with files, run commands, and use connections, skills, and AI configured on the paired desktop. Capabilities you enable later in Aiden are also available to Full Access bots. You can choose Custom Access now or reduce access in Bot Settings anytime.") .font(.body) .foregroundStyle(palette.secondary) } @@ -790,7 +790,7 @@ private struct AidenBotPrototypeWorkspacesView: View { } header: { Text("Workspaces") } footer: { - Text("This fixture root stays mounted separately from Bots and never connects to a Mac.") + Text("This fixture root stays mounted separately from Bots and never connects to a desktop.") } } .scrollContentBackground(.hidden) @@ -1091,7 +1091,7 @@ private struct AidenBotPrototypeInboxView: View { AidenBotPrototypeBanner( symbol: "exclamationmark.triangle", title: "Some selected access is unavailable.", - detail: "Review it on your Mac.", + detail: "Review it on your paired desktop.", tone: palette.warning ) default: @@ -1140,7 +1140,7 @@ private struct AidenBotPrototypeInboxView: View { AidenBotPrototypeEmptyView( symbol: "arrow.clockwise.circle", title: "Bots didn’t load", - detail: "The paired Mac did not return a complete Bot list.", + detail: "The paired desktop did not return a complete Bot list.", actionTitle: "Retry", action: { fixtureState = .ready } ) @@ -1506,7 +1506,7 @@ private struct AidenBotPrototypeProfileView: View { .multilineTextAlignment(.center) .frame(maxWidth: 380) Label( - isArchived ? "Archived bots are read-only until restored." : "Ready on your Mac", + isArchived ? "Archived bots are read-only until restored." : "Ready on your paired desktop", systemImage: isArchived ? "archivebox.fill" : "checkmark.circle.fill" ) .font(.caption.weight(.semibold)) @@ -1801,7 +1801,7 @@ private struct AidenBotPrototypeEditorView: View { } header: { Text("How this bot helps") } footer: { - Text("Write this in everyday language. Aiden adds the private operating details on your Mac.") + Text("Write this in everyday language. Aiden adds the private operating details on your paired desktop.") } Section { @@ -1847,7 +1847,7 @@ private struct AidenBotPrototypeEditorView: View { LabeledContent("Look", value: lookStyle.rawValue) LabeledContent("Access", value: accessPolicy.mode.title) Text(accessPolicy.mode == .full - ? "Can use your Mac, shell, enabled connections, and skills." + ? "Can use your paired desktop, shell, enabled connections, and skills." : "Uses only the access you select. This chat can reduce it further.") .font(.caption) .foregroundStyle(palette.secondary) @@ -1941,9 +1941,9 @@ private struct AidenBotPrototypeAccessView: View { } private static let locationCatalog = [ - CatalogItem(id: "documents", title: "Documents", detail: "Chosen on your Mac"), - CatalogItem(id: "desktop", title: "Desktop", detail: "Chosen on your Mac"), - CatalogItem(id: "downloads", title: "Downloads", detail: "Chosen on your Mac"), + CatalogItem(id: "documents", title: "Documents", detail: "Chosen on your paired desktop"), + CatalogItem(id: "desktop", title: "Desktop", detail: "Chosen on your paired desktop"), + CatalogItem(id: "downloads", title: "Downloads", detail: "Chosen on your paired desktop"), ] private static let connectionCatalog = [ CatalogItem(id: "calendar", title: "Calendar", detail: "Events and availability"), @@ -2033,7 +2033,7 @@ private struct AidenBotPrototypeAccessView: View { } if showsCustomCapabilities { - Section("Mac files") { + Section("Desktop files") { Picker("Files", selection: $files) { ForEach(AidenBotPrototypeFileAccess.allCases) { option in Text(option.rawValue) @@ -2077,7 +2077,7 @@ private struct AidenBotPrototypeAccessView: View { } header: { Text("Connections") } footer: { - Text("Choose external apps and services already configured in Aiden. Some connections are powered by MCP; account details stay on your Mac.") + Text("Choose external apps and services already configured in Aiden. Some connections are powered by MCP; account details stay on your paired desktop.") } Section { @@ -2109,7 +2109,7 @@ private struct AidenBotPrototypeAccessView: View { if scope == .bot { Label("Full Access", systemImage: "checkmark.shield.fill") .foregroundStyle(palette.accent) - Text("Can use your Mac, shell, enabled connections, and skills.") + Text("Can use your paired desktop, shell, enabled connections, and skills.") .font(.subheadline) .foregroundStyle(palette.secondary) } else { diff --git a/ios/AidenOnTheGo/Features/Remote/AidenBotChatToolsView.swift b/ios/AidenOnTheGo/Features/Remote/AidenBotChatToolsView.swift index d81f7d45..87f3e74c 100644 --- a/ios/AidenOnTheGo/Features/Remote/AidenBotChatToolsView.swift +++ b/ios/AidenOnTheGo/Features/Remote/AidenBotChatToolsView.swift @@ -246,7 +246,7 @@ final class AidenBotChatToolsModel { func readOnlyMessage(coordinator: AidenRemoteCoordinator, hostAllowsMutations: Bool) -> String? { if bot?.health == .archived { return "Archived bots are read-only until restored." } if bot?.health == .degraded || bot?.health == .unavailable { - return "This bot's access needs repair on your Mac before it can work." + return "This bot's access needs repair on your paired desktop before it can work." } if coordinator.connectionState != .connected { return "Offline — reconnect to change this chat's access." } if coordinator.installationStore.activeInstallation?.canWriteBots != true { @@ -593,7 +593,7 @@ struct AidenBotChatAccessSheetView: View { if model.draft?.mode == .custom { optionSection( title: "Connections", - description: "Connected apps and services already configured on your Mac.", + description: "Connected apps and services already configured on your paired desktop.", options: catalog.connections, keyPath: \.connectionIDs, ceiling: bot.access.custom.map { Set($0.connectionIds) } @@ -611,7 +611,7 @@ struct AidenBotChatAccessSheetView: View { .disabled(!canChangeDraft) optionSection( title: "Other abilities", - description: "Additional capabilities enabled for this bot on your Mac.", + description: "Additional capabilities enabled for this bot on your paired desktop.", options: catalog.otherCapabilities, keyPath: \.otherCapabilityIDs, ceiling: bot.access.custom.map { Set($0.otherCapabilityIds) } @@ -664,7 +664,7 @@ struct AidenBotChatAccessSheetView: View { ContentUnavailableView( "Access Unavailable", systemImage: "lock.trianglebadge.exclamationmark", - description: Text(model.errorMessage ?? "Reconnect to your Mac to load this chat's access.") + description: Text(model.errorMessage ?? "Reconnect to your paired desktop to load this chat's access.") ) } } @@ -713,7 +713,7 @@ struct AidenBotChatAccessSheetView: View { ) -> some View { Section { if options.isEmpty { - Text("None configured on this Mac").foregroundStyle(palette.secondary) + Text("None configured on the paired desktop").foregroundStyle(palette.secondary) } else { ForEach(options) { option in Toggle(isOn: optionBinding( @@ -937,7 +937,7 @@ final class AidenBotConversationFilesModel { guard coordinator.isCurrent(grant.context) else { return } if case AidenRemoteClientError.server(_, let body) = error, body.code.rawValue == "revision_conflict" { - errorMessage = "This file changed on the Mac. Reload it before saving again." + errorMessage = "This file changed on the paired desktop. Reload it before saving again." } else if error is AidenRemoteClientError { errorMessage = "Files access changed. Return to the chat and open Files again." } else { @@ -1104,8 +1104,8 @@ private struct AidenBotConversationFileEditorView: View { } else if let message = model.errorMessage { VStack(spacing: 8) { Text(message).font(.footnote).foregroundStyle(.secondary) - if message.contains("changed on the Mac") { - Button("Reload from Mac") { + if message.contains("changed on the paired desktop") { + Button("Reload from desktop") { Task { await model.reloadDocument(coordinator: coordinator) } } } diff --git a/ios/AidenOnTheGo/Features/Remote/AidenChatFeature.swift b/ios/AidenOnTheGo/Features/Remote/AidenChatFeature.swift index d445fb9f..8b14461e 100644 --- a/ios/AidenOnTheGo/Features/Remote/AidenChatFeature.swift +++ b/ios/AidenOnTheGo/Features/Remote/AidenChatFeature.swift @@ -1030,7 +1030,7 @@ final class AidenChatViewModel { throw NSError( domain: "AidenVoiceInput", code: 1, - userInfo: [NSLocalizedDescriptionKey: status.engine.error ?? String(localized: "The Mac speech engine is unavailable.")] + userInfo: [NSLocalizedDescriptionKey: status.engine.error ?? String(localized: "The desktop speech engine is unavailable.")] ) } guard let model = status.models.first(where: { $0.id == status.selectedModelId && $0.installed }) @@ -1039,7 +1039,7 @@ final class AidenChatViewModel { throw NSError( domain: "AidenVoiceInput", code: 2, - userInfo: [NSLocalizedDescriptionKey: String(localized: "Download a Mac speech model in App Settings before using this option.")] + userInfo: [NSLocalizedDescriptionKey: String(localized: "Download a desktop speech model in App Settings before using this option.")] ) } if status.selectedModelId != model.id { _ = try await client.selectSpeechModel(model.id) } @@ -1510,7 +1510,7 @@ final class AidenChatViewModel { case .scheduleWriteRequired: String(localized: "Schedule write access was removed from this paired device. The task was not approved.") case .hostApprovalRequired: - String(localized: "This request can only be approved on your Mac.") + String(localized: "This request can only be approved on your paired desktop.") } streamState = .reconciling coordinator.haptics.play(.warning, scope: hapticScope) @@ -2039,7 +2039,7 @@ final class AidenChatViewModel { return } catch { // Keep the durable stream cursor and continue retrying while - // this Mac connection remains current. Long Tailscale or + // this desktop connection remains current. Long Tailscale or // local-network outages must not erase terminal evidence. } attempt += 1 @@ -3044,7 +3044,7 @@ struct AidenMessageOutcomePresentation: Equatable { case "rate_limit": detail = "The model provider is receiving too many requests. Try again shortly." case "authentication": - detail = "The model provider rejected its credentials. Check Provider Settings on your Mac." + detail = "The model provider rejected its credentials. Check Provider Settings on your desktop." case "quota": detail = "The model provider account has no available quota." case "invalid_request": @@ -4117,7 +4117,7 @@ private struct AidenApprovalCard: View { .font(.caption) .foregroundStyle(palette.secondary) } else if kind == .scheduledTask && !canAllow { - Label("This task must be approved on your Mac.", systemImage: "desktopcomputer") + Label("This task must be approved on your paired desktop.", systemImage: "desktopcomputer") .font(.caption) .foregroundStyle(palette.secondary) } diff --git a/ios/AidenOnTheGo/Features/Remote/AidenPairingView.swift b/ios/AidenOnTheGo/Features/Remote/AidenPairingView.swift index 2ac53bc2..19d502e9 100644 --- a/ios/AidenOnTheGo/Features/Remote/AidenPairingView.swift +++ b/ios/AidenOnTheGo/Features/Remote/AidenPairingView.swift @@ -138,7 +138,7 @@ enum AidenPairingMethod: String, CaseIterable, Identifiable, Hashable { var title: String { switch self { case .scanQRCode: return String(localized: "Scan QR Code") - case .nearbyMac: return String(localized: "Nearby Mac + Setup Code") + case .nearbyMac: return String(localized: "Nearby Desktop + Setup Code") case .privateAddress: return String(localized: "Private Address + Setup Code") case .pastePayload: return String(localized: "Paste Pairing Payload") } @@ -149,9 +149,9 @@ enum AidenPairingMethod: String, CaseIterable, Identifiable, Hashable { case .scanQRCode: return String(localized: "Scan the one-time QR shown by Aiden Agent.") case .nearbyMac: - return String(localized: "Find your Mac on local Wi-Fi, then enter its setup code.") + return String(localized: "Find your desktop on local Wi-Fi, then enter its setup code.") case .privateAddress: - return String(localized: "Enter the private Tailscale address and setup code shown on your Mac.") + return String(localized: "Enter the private Tailscale address and setup code shown on your desktop.") case .pastePayload: return String(localized: "Use the complete one-time payload when the camera is unavailable.") } @@ -202,7 +202,7 @@ enum AidenMobileOnboardingPhase: String, CaseIterable, Identifiable, Hashable { var detail: String { switch self { case .build: - return String(localized: "Use Workspaces for project-focused work with files, commands, review, and Git. When Bots are available on your paired Mac, use them as reusable helpers and tap the Aiden logo to switch.") + return String(localized: "Use Workspaces for project-focused work with files, commands, review, and Git. When Bots are available on your paired desktop, use them as reusable helpers and tap the Aiden logo to switch.") case .extend: return String(localized: "Choose models and thinking levels, attach images, use web search, and extend Aiden with skills and MCP connectors.") case .control: @@ -573,19 +573,19 @@ struct AidenPairingView: View { ScrollView { VStack(alignment: .leading, spacing: 28) { VStack(alignment: .leading, spacing: 8) { - Text("Prepare your Mac").font(.largeTitle.bold()) - Text("Aiden Agent remains the server and keeps provider credentials on your Mac.") + Text("Prepare your desktop").font(.largeTitle.bold()) + Text("Aiden Agent remains the server and keeps provider credentials on your desktop.") .foregroundStyle(palette.secondary) } - pairingStep(number: 1, title: "Open Aiden Agent", detail: "On your Mac, go to Settings → Remote Access.") + pairingStep(number: 1, title: "Open Aiden Agent", detail: "On your desktop, go to Settings → Remote Access.") pairingStep(number: 2, title: "Turn on Remote Access", detail: "Choose Local Network, Tailscale, or both. Tailscale is best when you are away from home.") pairingStep(number: 3, title: "Create a pairing code", detail: "Keep the QR or setup code visible. Both expire after five minutes and can be used once.") VStack(alignment: .leading, spacing: 10) { Label("Per-device credential", systemImage: "key.fill") Label("Pinned HTTPS identity", systemImage: "lock.shield.fill") - Label("Revocable from your Mac", systemImage: "checkmark.shield") + Label("Revocable from your desktop", systemImage: "checkmark.shield") } .font(.subheadline) .foregroundStyle(palette.secondary) @@ -668,11 +668,11 @@ struct AidenPairingView: View { Label("Open Aiden Agent’s Add Device window and keep the one-time QR visible.", systemImage: "desktopcomputer") Label("The QR already contains the selected Local Network or Tailscale address.", systemImage: "network") } header: { - Text("On your Mac") + Text("On your desktop") } Section("Private pairing") { - Text("The QR expires after five minutes and can be used once. Aiden pins the Mac’s HTTPS identity during pairing.") + Text("The QR expires after five minutes and can be used once. Aiden pins the desktop’s HTTPS identity during pairing.") .foregroundStyle(palette.secondary) } } @@ -700,14 +700,14 @@ struct AidenPairingView: View { } } } header: { - Text("Nearby Macs") + Text("Nearby desktops") } footer: { - Text("Your iPhone or iPad and Mac must be on the same local network. Select the Mac shown in Aiden Agent’s Add Device window.") + Text("Your iPhone or iPad and desktop must be on the same local network. Select the desktop shown in Aiden Agent’s Add Device window.") } Section { manualEndpointField( - placeholder: "https://mac-name.local:49220/api/aiden/v1", + placeholder: "https://desktop-name.local:49220/api/aiden/v1", accessibilityLabel: "Nearby Aiden Agent address" ) manualSetupCodeField @@ -715,7 +715,7 @@ struct AidenPairingView: View { } header: { Text("Setup code") } footer: { - Text("If discovery is unavailable, enter the exact nearby Mac address shown in Aiden Agent. The setup code is encrypted and can be used once.") + Text("If discovery is unavailable, enter the exact nearby desktop address shown in Aiden Agent. The setup code is encrypted and can be used once.") } } .scrollContentBackground(.hidden) @@ -726,7 +726,7 @@ struct AidenPairingView: View { Form { Section { manualEndpointField( - placeholder: "https://mac-name.tailnet.ts.net/api/aiden/v1", + placeholder: "https://desktop-name.tailnet.ts.net/api/aiden/v1", accessibilityLabel: "Private Tailscale address" ) manualSetupCodeField @@ -739,7 +739,7 @@ struct AidenPairingView: View { Section("Before pairing") { Label("Sign in to the same Tailscale network on both devices", systemImage: "network") - Label("Keep Aiden Agent open on your Mac", systemImage: "desktopcomputer") + Label("Keep Aiden Agent open on your desktop", systemImage: "desktopcomputer") } } .scrollContentBackground(.hidden) @@ -768,7 +768,7 @@ struct AidenPairingView: View { } header: { Text("One-time pairing payload") } footer: { - Text("Use only the complete payload copied from your own Mac. It contains a one-time secret and expires after five minutes.") + Text("Use only the complete payload copied from your own desktop. It contains a one-time secret and expires after five minutes.") } } .scrollContentBackground(.hidden) @@ -806,8 +806,8 @@ struct AidenPairingView: View { .accessibilityValue(selectedAgentID == agent.id ? "Selected" : "Not selected") .accessibilityAddTraits(selectedAgentID == agent.id ? .isSelected : []) .accessibilityHint(agent.endpoint == nil - ? "This Mac is still resolving its network address." - : "Use this Mac for setup-code pairing.") + ? "This desktop is still resolving its network address." + : "Use this desktop for setup-code pairing.") } private func manualEndpointField( diff --git a/ios/AidenOnTheGo/Features/Remote/AidenProductShellView.swift b/ios/AidenOnTheGo/Features/Remote/AidenProductShellView.swift index 4a48651b..70cc488a 100644 --- a/ios/AidenOnTheGo/Features/Remote/AidenProductShellView.swift +++ b/ios/AidenOnTheGo/Features/Remote/AidenProductShellView.swift @@ -56,9 +56,9 @@ enum AidenBotsAvailability: Equatable, Sendable { case .mobileDisabled: "Bots aren’t available in this version of Aiden On The Go." case .unsupported: - "Bots need a newer version of Aiden Agent on your Mac." + "Bots need a newer version of Aiden Agent on your paired desktop." case .notGranted: - "Approve Bot access on your Mac, or pair this phone again." + "Approve Bot access on your paired desktop, or pair this phone again." } } @@ -140,7 +140,7 @@ func aidenBotSwitcherCoachmarkDetail(canWrite: Bool) -> String { if canWrite { return "Before a Bot can act, Aiden shows a one-time Full Access notice. Choose Continue with Full Access or Customize first." } - return "This Mac shared Bots as read-only. You can open their conversations here, then change Bot access on your Mac if you want to let them act." + return "This desktop shared Bots as read-only. You can open their conversations here, then change Bot access on your paired desktop if you want to let them act." } func aidenBotChatAllowsMutations( @@ -746,7 +746,7 @@ private struct AidenBotShellView: View { guard coordinator.connectionState == .connected else { if cached == nil { path = [] - coordinator.presentedError = "Reconnect to your Mac to open this Bot chat." + coordinator.presentedError = "Reconnect to your paired desktop to open this Bot chat." } return } @@ -1086,10 +1086,10 @@ private struct AidenFullAccessNoticeView: View { .foregroundStyle(.tint) .accessibilityHidden(true) - Text("Bots can use your Mac") + Text("Bots can use your paired desktop") .font(.largeTitle.bold()) - Text("By default, bots can work with files, run commands, and use connections, skills, and AI configured on the paired Mac. Capabilities you enable later in Aiden are also available to Full Access bots. You can choose Custom Access now or reduce access in Bot Settings anytime.") + Text("By default, bots can work with files, run commands, and use connections, skills, and AI configured on the paired desktop. Capabilities you enable later in Aiden are also available to Full Access bots. You can choose Custom Access now or reduce access in Bot Settings anytime.") .font(.body) if includesMigrationCopy { @@ -1116,7 +1116,7 @@ private struct AidenFullAccessNoticeView: View { .disabled(isSaving) if isSaving { - ProgressView("Saving on your Mac…") + ProgressView("Saving on your paired desktop…") .frame(maxWidth: .infinity) } } @@ -1377,7 +1377,7 @@ struct AidenProductShellView: View { case .coaching: EmptyView() case .checking: - ProgressView("Checking Bot access on your Mac…") + ProgressView("Checking Bot access on your paired desktop…") .padding() .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 14)) case .failed(let message): diff --git a/ios/AidenOnTheGo/Features/Remote/AidenScheduledTasksView.swift b/ios/AidenOnTheGo/Features/Remote/AidenScheduledTasksView.swift index 6b94043c..d242f86f 100644 --- a/ios/AidenOnTheGo/Features/Remote/AidenScheduledTasksView.swift +++ b/ios/AidenOnTheGo/Features/Remote/AidenScheduledTasksView.swift @@ -435,7 +435,7 @@ final class AidenScheduledTasksModel { return } pendingRunKeys[task.id] = nil - outcomeMessage = String(localized: "Run accepted (\(accepted.runId.prefix(12))…). It continues on your Mac if this phone disconnects.") + outcomeMessage = String(localized: "Run accepted (\(accepted.runId.prefix(12))…). It continues on your paired desktop if this phone disconnects.") coordinator.haptics.play( .actionStarted, scope: hapticScope, @@ -673,7 +673,7 @@ struct AidenScheduledTasksView: View { ContentUnavailableView( "Schedule Access Required", systemImage: "lock.shield", - description: Text("Enable schedule read access for this paired device on your Mac to view task definitions and cached run history.") + description: Text("Enable schedule read access for this paired device on your desktop to view task definitions and cached run history.") ) .listRowBackground(Color.clear) } else { @@ -702,7 +702,7 @@ struct AidenScheduledTasksView: View { ContentUnavailableView( "No Scheduled Tasks", systemImage: "clock.badge.plus", - description: Text("Ask Aiden in any chat to create unattended work that runs on your Mac.") + description: Text("Ask Aiden in any chat to create unattended work that runs on your desktop.") ) .listRowBackground(Color.clear) } else if visibleTasks.isEmpty && !model.isLoading { @@ -1110,7 +1110,7 @@ private struct AidenScheduledTaskEditor: View { .font(.footnote) .foregroundStyle(.secondary) } - Text("Only enabled server names are shown. Connection details and credentials remain on your Mac.") + Text("Only enabled server names are shown. Connection details and credentials remain on your desktop.") .font(.footnote).foregroundStyle(.secondary) } } @@ -1128,8 +1128,8 @@ private struct AidenScheduledTaskEditor: View { Picker("Permission", selection: $draft.permission) { ForEach(AidenScheduledTaskPermission.allCases, id: \.self) { Text($0.title).tag($0) } } - Toggle("Mac notification", isOn: $draft.notify) - Text("Enabled tasks can run on your Mac while this phone is disconnected. Full permission can edit files and run commands without asking.") + Toggle("Desktop notification", isOn: $draft.notify) + Text("Enabled tasks can run on your desktop while this phone is disconnected. Full permission can edit files and run commands without asking.") .font(.footnote).foregroundStyle(.secondary) } if let validation = reviewValidationMessage { Section { Text(validation).foregroundStyle(.red) } } @@ -1207,7 +1207,7 @@ private struct AidenScheduledTaskEditor: View { } LabeledContent("Notifications", value: draft.notify ? "On" : "Off") } - Section { Text("Confirm only if this unattended work should run on your Mac while the phone is disconnected.") } + Section { Text("Confirm only if this unattended work should run on your desktop while the phone is disconnected.") } } .navigationTitle("Review Task") .toolbar { diff --git a/ios/AidenOnTheGo/Features/Remote/AidenWorkspaceEnvironmentView.swift b/ios/AidenOnTheGo/Features/Remote/AidenWorkspaceEnvironmentView.swift index 27796919..12b5e728 100644 --- a/ios/AidenOnTheGo/Features/Remote/AidenWorkspaceEnvironmentView.swift +++ b/ios/AidenOnTheGo/Features/Remote/AidenWorkspaceEnvironmentView.swift @@ -64,7 +64,7 @@ actor AidenWorkspaceEnvironmentCache { try? FileManager.default.removeItem(at: instanceDirectory) // The legacy flat format encoded instance + workspace identity in its // filename but not its payload. Delete only names attributable from a - // known workspace snapshot; never erase another Mac's unknown cache. + // known workspace snapshot; never erase another desktop's unknown cache. for workspaceId in knownWorkspaceIds { try? FileManager.default.removeItem( at: legacyFile(instanceId: instanceId, workspaceId: workspaceId) @@ -240,7 +240,7 @@ final class AidenWorkspaceFilesModel { guard coordinator.isCurrent(context) else { return false } if case AidenRemoteClientError.server(_, let body) = error, body.code.rawValue == "revision_conflict" { - errorMessage = "This file changed on the Mac. Reload it before saving again." + errorMessage = "This file changed on the paired desktop. Reload it before saving again." coordinator.haptics.play(.warning, scope: hapticScope) } else { errorMessage = error.localizedDescription @@ -357,8 +357,8 @@ private struct AidenWorkspaceFileEditorView: View { if let message = model.errorMessage { VStack(spacing: 8) { Text(message).font(.footnote).foregroundStyle(.secondary) - if message.contains("changed on the Mac") { - Button("Reload from Mac") { + if message.contains("changed on the desktop") { + Button("Reload from desktop") { Task { await model.reloadDocument(coordinator: coordinator) } } } diff --git a/ios/AidenOnTheGo/Features/Remote/AidenWorkspaceShellView.swift b/ios/AidenOnTheGo/Features/Remote/AidenWorkspaceShellView.swift index 14c682dc..bd3cde9a 100644 --- a/ios/AidenOnTheGo/Features/Remote/AidenWorkspaceShellView.swift +++ b/ios/AidenOnTheGo/Features/Remote/AidenWorkspaceShellView.swift @@ -1351,7 +1351,7 @@ private struct AidenWorkspacesDirectoryView: View { searchText.isEmpty ? "No Workspaces" : "No Matching Workspaces", systemImage: searchText.isEmpty ? "folder" : "magnifyingglass", description: Text(searchText.isEmpty - ? "Create a workspace or add a Mac folder to get started." + ? "Create a workspace or add a desktop folder to get started." : "Try a different search term.") ) .listRowBackground(Color.clear) @@ -1401,7 +1401,7 @@ private struct AidenWorkspacesDirectoryView: View { } Button { isShowingFolderBrowser = true } label: { - Label("Add Mac Folder", systemImage: "folder.badge.plus") + Label("Add Desktop Folder", systemImage: "folder.badge.plus") } } label: { Image(systemName: "plus") @@ -1439,7 +1439,7 @@ private struct AidenWorkspacesDirectoryView: View { } .disabled(newWorkspaceName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) } message: { - Text("Creates a workspace registry entry without a Mac folder. You can add a folder later from Aiden Agent.") + Text("Creates a workspace registry entry without a desktop folder. You can add a folder later from Aiden Agent.") } .confirmationDialog( "Create a managed scratch workspace?", @@ -1459,7 +1459,7 @@ private struct AidenWorkspacesDirectoryView: View { } Button("Cancel", role: .cancel) {} } message: { - Text("Aiden Agent will create and manage the worktree on your Mac.") + Text("Aiden Agent will create and manage the worktree on your desktop.") } .alert( "Rename Workspace", @@ -1494,7 +1494,7 @@ private struct AidenWorkspacesDirectoryView: View { || coordinator.isMutating ) } message: { - Text("This updates the workspace name in Aiden Agent on your Mac and paired clients. It does not rename the folder on disk.") + Text("This updates the workspace name in Aiden Agent on your desktop and paired clients. It does not rename the folder on disk.") } .alert( "Archive on This Device?", @@ -1514,7 +1514,7 @@ private struct AidenWorkspacesDirectoryView: View { workspacePendingFirstArchive = nil } } message: { - Text("This hides the workspace and its chats only on this iPhone or iPad. It stays available in Aiden Agent on your Mac and on other devices.") + Text("This hides the workspace and its chats only on this iPhone or iPad. It stays available in Aiden Agent on your desktop and on other devices.") } .alert( "Remove from Aiden Agent?", @@ -1542,7 +1542,7 @@ private struct AidenWorkspacesDirectoryView: View { } .disabled(coordinator.connectionState != .connected || coordinator.isMutating) } message: { - Text("This unregisters the workspace from Aiden Agent and paired clients. Its folder, files, and chats stay on your Mac, but its chats will no longer be listed. Delete the folder separately in Finder if you no longer need it.") + Text("This unregisters the workspace from Aiden Agent and paired clients. Its folder, files, and chats stay on your desktop, but its chats will no longer be listed. Delete the folder separately in your system file manager if you no longer need it.") } } @@ -1884,7 +1884,7 @@ private struct AidenWorkspaceSettingsView: View { } footer: { Text(workspace.isManagedWorktree ? "Deleting an Aiden-managed worktree removes its checkout and may remove its branch when safe." - : "Removing unregisters this workspace from Aiden Agent and paired clients. Its folder, files, and chats stay on your Mac, but its chats will no longer be listed.") + : "Removing unregisters this workspace from Aiden Agent and paired clients. Its folder, files, and chats stay on your desktop, but its chats will no longer be listed.") } } } @@ -1944,7 +1944,7 @@ private struct AidenWorkspaceSettingsView: View { } message: { Text(workspace.isManagedWorktree ? "This destructive Git operation is performed by Aiden Agent using its persisted worktree ownership record." - : "The folder, its files, and chats remain on your Mac, but the chats will no longer be listed. Delete the folder separately in Finder if you no longer need it.") + : "The folder, its files, and chats remain on your desktop, but the chats will no longer be listed. Delete the folder separately in your system file manager if you no longer need it.") } } .onAppear { coordinator.haptics.activate(scope: hapticScope) } @@ -2235,7 +2235,7 @@ private struct AidenUsageView: View { .foregroundStyle(palette.accent) .frame(width: 28) - Text("Privacy-safe aggregates are recorded by Aiden Agent on your Mac. Prompts, responses, chat IDs, workspace IDs, and file paths are not included.") + Text("Privacy-safe aggregates are recorded by Aiden Agent on your desktop. Prompts, responses, chat IDs, workspace IDs, and file paths are not included.") .font(.footnote) .foregroundStyle(palette.secondary) .fixedSize(horizontal: false, vertical: true) @@ -2404,7 +2404,7 @@ private struct AidenAppSettingsView: View { Form { Section("Aiden Agent") { LabeledContent( - "Connected Mac", + "Connected desktop", value: coordinator.installationStore.activeInstallation?.name ?? "Not connected" ) Button { @@ -2441,7 +2441,7 @@ private struct AidenAppSettingsView: View { NavigationLink { AidenMacTranscriptionSettingsView(coordinator: coordinator) } label: { - Label("Mac speech model", systemImage: "desktopcomputer") + Label("Desktop speech model", systemImage: "desktopcomputer") } } } header: { @@ -2449,7 +2449,7 @@ private struct AidenAppSettingsView: View { } footer: { Text( voiceInputModeRaw == AidenVoiceInputMode.pairedMac.rawValue - ? "Microphone audio is sent over Aiden's encrypted pinned connection, processed by Parakeet on your paired Mac, and not retained. Text appears after you stop recording." + ? "Microphone audio is sent over Aiden's encrypted pinned connection, processed by Parakeet on your paired desktop, and not retained. Text appears after you stop recording." : "Uses Apple's on-device Speech framework. Microphone audio stays on this device." ) } @@ -2550,13 +2550,13 @@ private struct AidenMacTranscriptionSettingsView: View { } } } else if isLoading { - ProgressView("Loading Mac speech models…") + ProgressView("Loading desktop speech models…") } if let errorMessage { Section { Text(errorMessage).foregroundStyle(.red) } } } - .navigationTitle("Mac Transcription") + .navigationTitle("Desktop Transcription") .navigationBarTitleDisplayMode(.inline) .task { await refresh() } .task(id: downloadPollKey) { @@ -2894,7 +2894,7 @@ private struct AidenFolderBrowserView: View { } } } - .navigationTitle("Add Mac Folder") + .navigationTitle("Add Desktop Folder") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { diff --git a/ios/AidenOnTheGo/Models/AidenBot.swift b/ios/AidenOnTheGo/Models/AidenBot.swift index 1ff9cc63..c222e682 100644 --- a/ios/AidenOnTheGo/Models/AidenBot.swift +++ b/ios/AidenOnTheGo/Models/AidenBot.swift @@ -8,7 +8,7 @@ enum AidenBotContractError: Error, Equatable, LocalizedError { switch self { case .invalidCombination("no available provider and model"): String( - localized: "Set up a provider and model on your Mac. In Aiden Agent, open Settings → Providers, connect or refresh a provider, and make at least one chat model available. Then tap Try Again." + localized: "Set up a provider and model on your paired desktop. In Aiden Agent, open Settings → Providers, connect or refresh a provider, and make at least one chat model available. Then tap Try Again." ) case .invalidCombination("unavailable custom access"): String( diff --git a/ios/AidenOnTheGo/Models/AidenChat.swift b/ios/AidenOnTheGo/Models/AidenChat.swift index 6a2e9b43..d3405c34 100644 --- a/ios/AidenOnTheGo/Models/AidenChat.swift +++ b/ios/AidenOnTheGo/Models/AidenChat.swift @@ -441,7 +441,7 @@ enum AidenAgentActivityPresentation { "web_search": ("Searching the web", "Searched the web"), "schedule_task": ("Scheduling", "Scheduled"), "edit_automation": ("Editing automation", "Edited automation"), - "computer_use": ("Using Mac", "Used Mac"), + "computer_use": ("Using Computer Use", "Used Computer Use"), "compact_context": ("Compacting context", "Compacted context"), ] @@ -556,7 +556,7 @@ enum AidenAgentActivityPresentation { if changes > 0 { clauses.append("\(running ? "editing" : "edited") \(changes) file\(changes == 1 ? "" : "s")") } if commands > 0 { clauses.append("\(running ? "running" : "ran") \(commands) command\(commands == 1 ? "" : "s")") } if web > 0 { clauses.append("\(web) web search\(web == 1 ? "" : "es")") } - if mac > 0 { clauses.append("\(mac) Mac action\(mac == 1 ? "" : "s")") } + if mac > 0 { clauses.append("\(mac) Computer Use action\(mac == 1 ? "" : "s")") } if compactions > 0 { clauses.append(running ? "compacting context" : "compacted context") } if other > 0 { clauses.append("\(other) tool call\(other == 1 ? "" : "s")") } if clauses.isEmpty { return running ? "Working" : "Used \(tools.count) tool\(tools.count == 1 ? "" : "s")" } diff --git a/ios/AidenOnTheGo/Networking/AidenRemoteClient.swift b/ios/AidenOnTheGo/Networking/AidenRemoteClient.swift index 9349881e..5d02bb4d 100644 --- a/ios/AidenOnTheGo/Networking/AidenRemoteClient.swift +++ b/ios/AidenOnTheGo/Networking/AidenRemoteClient.swift @@ -315,7 +315,7 @@ enum AidenRemoteClientError: Error, LocalizedError { case .missingTrustConfiguration: return "This Aiden installation must be paired again to establish secure server trust." case .installationChanged: - return "The active Aiden Agent changed. Try again on the selected Mac." + return "The active Aiden Agent changed. Try again on the selected desktop." } } } @@ -461,7 +461,7 @@ final class AidenRemoteClient: @unchecked Sendable { ) } catch let AidenRemoteClientError.server(statusCode, body) where statusCode == 400 && body.code.rawValue == "invalid_request" { - // Strict early-v1 Macs reject additive request keys before consuming + // Strict early-v1 desktops reject additive request keys before consuming // the one-time secret. Retry once with the frozen four-field shape. exchange = try await client.send( method: "POST", diff --git a/ios/AidenOnTheGo/Networking/AidenRemoteContract.swift b/ios/AidenOnTheGo/Networking/AidenRemoteContract.swift index db108baf..71d2833f 100644 --- a/ios/AidenOnTheGo/Networking/AidenRemoteContract.swift +++ b/ios/AidenOnTheGo/Networking/AidenRemoteContract.swift @@ -64,7 +64,7 @@ enum AidenManualPairingError: Error, Equatable, LocalizedError { var errorDescription: String? { switch self { case .invalidCode: - return String(localized: "Enter the 20-character setup code shown on your Mac.") + return String(localized: "Enter the 20-character setup code shown on your desktop.") case .invalidBootstrap: return String(localized: "Aiden Agent returned an invalid manual pairing response.") case .decryptionFailed: diff --git a/ios/AidenOnTheGo/Persistence/AidenChatCache.swift b/ios/AidenOnTheGo/Persistence/AidenChatCache.swift index 788bde2c..97abbcc6 100644 --- a/ios/AidenOnTheGo/Persistence/AidenChatCache.swift +++ b/ios/AidenOnTheGo/Persistence/AidenChatCache.swift @@ -344,7 +344,7 @@ actor AidenChatCache { // Older active-stream records did not contain deviceId and cannot // decode with the current schema. Their outer envelope still has // an exact installation identity, so explicit forget/re-pair can - // remove them without touching another Mac's cache. + // remove them without touching another desktop's cache. guard let data = try? Data(contentsOf: url), data.count <= maxCacheFileBytes, let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any], diff --git a/ios/AidenOnTheGoTests/AidenBotGeneratedAvatarTests.swift b/ios/AidenOnTheGoTests/AidenBotGeneratedAvatarTests.swift index 1953e20c..60452c97 100644 --- a/ios/AidenOnTheGoTests/AidenBotGeneratedAvatarTests.swift +++ b/ios/AidenOnTheGoTests/AidenBotGeneratedAvatarTests.swift @@ -723,7 +723,7 @@ final class AidenBotGeneratedAvatarTests: XCTestCase { XCTAssertEqual(model.authoritativeBot?.avatar.asset?.assetRevision, macRevision) XCTAssertNotNil(model.currentImage) XCTAssertEqual(model.phase, .idle) - XCTAssertTrue(model.errorMessage?.contains("changed on your Mac") == true) + XCTAssertTrue(model.errorMessage?.contains("changed on your paired desktop") == true) } @MainActor @@ -804,7 +804,7 @@ final class AidenBotGeneratedAvatarTests: XCTestCase { XCTAssertEqual(model.phase, .idle) XCTAssertEqual(model.authoritativeBot?.avatar.asset?.assetRevision, replacementRevision) XCTAssertNotNil(model.currentImage) - XCTAssertTrue(model.errorMessage?.contains("changed on your Mac") == true) + XCTAssertTrue(model.errorMessage?.contains("changed on your paired desktop") == true) } @MainActor diff --git a/ios/AidenOnTheGoTests/AidenChatTests.swift b/ios/AidenOnTheGoTests/AidenChatTests.swift index 8153c51b..5b09351d 100644 --- a/ios/AidenOnTheGoTests/AidenChatTests.swift +++ b/ios/AidenOnTheGoTests/AidenChatTests.swift @@ -398,7 +398,7 @@ final class AidenChatTests: XCTestCase { ) XCTAssertEqual( AidenAgentActivityPresentation.summary(timeline), - "1 web search, 1 Mac action, compacted context, 1 tool call" + "1 web search, 1 Computer Use action, compacted context, 1 tool call" ) } @@ -1354,7 +1354,7 @@ final class AidenChatTests: XCTestCase { )), .init( title: "Generation failed", - detail: "The model provider rejected its credentials. Check Provider Settings on your Mac.", + detail: "The model provider rejected its credentials. Check Provider Settings on your desktop.", symbol: "exclamationmark.triangle", isFailure: true ) diff --git a/ios/AidenOnTheGoTests/AidenNativeIntegrationTests.swift b/ios/AidenOnTheGoTests/AidenNativeIntegrationTests.swift index 9051f11e..bb667524 100644 --- a/ios/AidenOnTheGoTests/AidenNativeIntegrationTests.swift +++ b/ios/AidenOnTheGoTests/AidenNativeIntegrationTests.swift @@ -591,7 +591,7 @@ final class AidenNativeIntegrationTests: XCTestCase { XCTAssertEqual(AidenVoiceInputMode.defaultsKey, "aiden.voiceInput.mode") XCTAssertEqual(AidenVoiceInputMode.allCases, [.onDevice, .pairedMac]) XCTAssertEqual(AidenVoiceInputMode.onDevice.title, "On this device") - XCTAssertEqual(AidenVoiceInputMode.pairedMac.title, "Paired Mac") + XCTAssertEqual(AidenVoiceInputMode.pairedMac.title, "Paired desktop") } func testVoiceSessionFenceRejectsCallbacksFromAnInvalidatedSession() { diff --git a/ios/AidenOnTheGoTests/AidenProductShellTests.swift b/ios/AidenOnTheGoTests/AidenProductShellTests.swift index fd0629bf..4f7e0fe4 100644 --- a/ios/AidenOnTheGoTests/AidenProductShellTests.swift +++ b/ios/AidenOnTheGoTests/AidenProductShellTests.swift @@ -355,7 +355,7 @@ final class AidenProductShellTests: XCTestCase { state: .waitingForApproval, canRespondToApproval: false )?.label, - "Waiting for approval on Mac" + "Waiting for desktop approval" ) XCTAssertEqual( aidenBotInboxActivityStatus(state: .running, canRespondToApproval: false)?.label, @@ -529,7 +529,7 @@ final class AidenProductShellTests: XCTestCase { ) XCTAssertEqual( aidenBotSwitcherCoachmarkDetail(canWrite: false), - "This Mac shared Bots as read-only. You can open their conversations here, then change Bot access on your Mac if you want to let them act." + "This desktop shared Bots as read-only. You can open their conversations here, then change Bot access on your paired desktop if you want to let them act." ) } diff --git a/ios/CONTEXT.md b/ios/CONTEXT.md index 1fd0bf78..a9393c6a 100644 --- a/ios/CONTEXT.md +++ b/ios/CONTEXT.md @@ -1,9 +1,9 @@ # Aiden On The Go terms -- **Aiden installation:** one paired Aiden Agent Mac, identified by its server-issued instance ID. -- **Device credential:** the per-phone or per-iPad bearer secret stored only in Keychain and revocable on the Mac. +- **Aiden installation:** one paired Aiden Agent desktop, identified by its server-issued instance ID. +- **Device credential:** the per-phone or per-iPad bearer secret stored only in Keychain and revocable on the desktop. - **Workspace:** an Aiden registry entry with `full`, `ask`, or `none` permission. -- **Approved root:** a Mac folder explicitly exposed for remote browsing by a local desktop action. +- **Approved root:** a desktop folder explicitly exposed for remote browsing by a local desktop action. - **Location handle:** a short-lived opaque browser capability; never a filesystem path. - **Selection:** a short-lived, single-use capability consumed atomically when registering a selected folder. - **Remote turn:** one idempotently admitted user message whose generation remains owned by Aiden Agent across network loss. diff --git a/ios/PROJECT_INTENT.md b/ios/PROJECT_INTENT.md index 11a7f5f6..c655eb48 100644 --- a/ios/PROJECT_INTENT.md +++ b/ios/PROJECT_INTENT.md @@ -1,12 +1,12 @@ # Aiden On The Go — Project Intent -Aiden On The Go lets a user securely control their own Aiden Agent Mac from iPhone or iPad. It is a client, not an agent runtime or hosted service. +Aiden On The Go lets a user securely control their own Aiden Agent desktop from iPhone or iPad. It is a client, not an agent runtime or hosted service. Core boundaries: -- Pair explicitly with a Mac using a short-lived QR bootstrap and per-device revocable credential. -- Connect over pinned local HTTPS or the Mac's explicitly configured Tailscale route. -- Match Aiden's chat and workspace behavior without widening permissions or exposing private Mac paths. +- Pair explicitly with a desktop using a short-lived QR bootstrap and per-device revocable credential. +- Connect over pinned local HTTPS or the desktop's explicitly configured Tailscale route. +- Match Aiden's chat and workspace behavior without widening permissions or exposing private desktop paths. - Keep credentials in Keychain and bounded offline presentation state on device. - Use native SwiftUI controls and adaptive Apple navigation. diff --git a/ios/PROJECT_SPEC.md b/ios/PROJECT_SPEC.md index be9f5707..165d57b6 100644 --- a/ios/PROJECT_SPEC.md +++ b/ios/PROJECT_SPEC.md @@ -20,7 +20,7 @@ The complete planned product includes: - Multiple paired Aiden installations with QR or 100-bit setup-code pairing, Keychain credentials, discovery, manual URL entry, switching, and revocation handling. - Chat list/open/create/rename/delete, bounded attachments, provider/model/thinking selection, atomic turn start, resumable streaming, cancel, reasoning/tool/timeline status, and allow/deny approvals. -- Workspace registry list/create/update/unregister, including folderless, managed scratch, and folders selected through a server-approved Mac directory browser. +- Workspace registry list/create/update/unregister, including folderless, managed scratch, and folders selected through a server-approved desktop directory browser. - Workspace Settings from the conversation toolbar ellipsis. Workspace permission is never a composer control. - Device-local Aiden, Slate, Berry, and Moss appearance presets plus supported mobile appearance options. - Aiden workspace file index/read/version-checked write and the existing Aiden Git review/diff/compare/branch/commit/push/managed-worktree operations. @@ -39,13 +39,13 @@ Remove Kanban, Hermes projects/profiles/personalities, Hermes Skills/Memory/Insi ## 3. Security invariants -- Remote Access is off by default and has no listener until enabled on the Mac. +- Remote Access is off by default and has no listener until enabled on the desktop. - Tailscale supplies reachability, never app authorization. Aiden manages only the exact non-Funnel Serve route it owns and never invokes `tailscale serve reset`. - Local-network production transport is HTTPS. QR pairing pins the Aiden installation's stable P-256 SPKI SHA-256 fingerprint. Plain HTTP is development-build-only. - Pairing secrets are high entropy, short lived, single use, rate limited, and never logged. The reviewed manual path uses a uniformly random 100-bit Crockford code only as a local HKDF input for authenticated decryption of the existing certificate-pinned trust envelope; lower-entropy human-sized codes still require a reviewed PAKE/SAS or explicit fingerprint confirmation. -- Device credentials are random, stored as digests on Mac and in Keychain on iOS, capability scoped, revocable, and never placed in URLs, App Group data, App Intents, logs, or Live Activities. +- Device credentials are random, stored as digests on the desktop and in Keychain on iOS, capability scoped, revocable, and never placed in URLs, App Group data, App Intents, logs, or Live Activities. - DTOs are allowlists. Absolute paths, provider/MCP credentials, raw diagnostics, Git admin paths/tokens, schedule runtime internals, and private agent history never cross the API. -- Directory and file handles are opaque server-side capabilities bound to instance, device, workspace/root identity, policy revision, expiry, and snapshot. The client never submits a free-form Mac path. +- Directory and file handles are opaque server-side capabilities bound to instance, device, workspace/root identity, policy revision, expiry, and snapshot. The client never submits a free-form desktop path. - Workspace selection consumption and workspace creation are atomic and idempotent. Filesystem identity and canonical root membership are revalidated immediately before mutation. - Workspace turns honor the workspace's saved `full`, `ask`, or `none` permission. Bot turns instead honor a main-owned, revisioned Full/Custom policy: Full is explicit after the current notice, Custom uses exact reductions, and corrupt, missing-after-migration, or future-version policy state fails closed. Neither transport can mint Assistant/unattended modes or silently enable Computer Use. - Every bot has exactly one main-owned managed home. The Mac sets it as the shell/tool working directory and ordinary save location, does not initialize `.git`, and injects the operating contract after editable bot instructions so a phone, renderer, or prompt cannot replace it. Full Access may inspect other OS-accessible Mac locations only as needed and remains subject to OS permissions, global disables, approvals, and destructive-action safeguards. diff --git a/ios/README.md b/ios/README.md index 7b578224..03eac8a2 100644 --- a/ios/README.md +++ b/ios/README.md @@ -1,6 +1,6 @@ # Aiden On The Go -Aiden On The Go is the native SwiftUI companion for Aiden Agent on macOS. The Mac owns execution, persistence, providers, workspaces, and permissions; iPhone and iPad provide an authenticated remote control surface over a local network or Tailscale. +Aiden On The Go is the native SwiftUI companion for Aiden Agent on macOS and Linux. The paired desktop owns execution, persistence, providers, workspaces, and permissions; iPhone and iPad provide an authenticated remote control surface over a local network or Tailscale. The product and protocol sources of truth are: diff --git a/ios/app-store/MOBILE_PRIVACY_SUPPORT_COPY.md b/ios/app-store/MOBILE_PRIVACY_SUPPORT_COPY.md index 380e6afe..92c0fdb7 100644 --- a/ios/app-store/MOBILE_PRIVACY_SUPPORT_COPY.md +++ b/ios/app-store/MOBILE_PRIVACY_SUPPORT_COPY.md @@ -2,37 +2,37 @@ Status: ready for owner/legal review and publication on `chatwithaiden.com`. The website source is not part of this repository, so this file does not claim that the live page has changed. -The current public policy says Aiden is a local-first macOS app. Before submitting Aiden On The Go, replace that product-limited wording and add the following sections while preserving the existing provider and website-hosting disclosures. +The current public policy says Aiden is a local-first macOS app. Before submitting Aiden On The Go or shipping the Linux desktop build, replace that product-limited wording and add the following sections while preserving the existing provider and website-hosting disclosures. ## Overview replacement -Aiden is designed as a local-first Mac app with an optional native iPhone and iPad companion called Aiden On The Go. We do not collect, sell, rent, or share personal information through the Aiden website or apps. Aiden On The Go connects directly to an Aiden Agent installation that you choose and control; Aiden does not provide a hosted relay or central synchronization service for that connection. +Aiden is designed as a local-first macOS and Linux desktop app with an optional native iPhone and iPad companion called Aiden On The Go. We do not collect, sell, rent, or share personal information through the Aiden website or apps. Aiden On The Go connects directly to an Aiden Agent installation that you choose and control; Aiden does not provide a hosted relay or central synchronization service for that connection. ## Local app data replacement -Aiden Agent stores chat history, workspace configuration, and app settings locally on your Mac. Provider API keys are stored on your Mac, such as in macOS Keychain, and are not copied into Aiden On The Go or sent to Aiden servers. +Aiden Agent stores chat history, workspace configuration, and app settings locally on your desktop. Provider API keys are stored in the operating system's credential store (such as macOS Keychain or a supported Linux Secret Service or KWallet backend) and are not copied into Aiden On The Go or sent to Aiden servers. Aiden On The Go stores its pairing credential in the iPhone or iPad Keychain. It may keep device-local settings and bounded caches for paired-installation names, workspaces, chats, navigation, and last-known run status. App Intents use a limited App Group cache containing stable identifiers and display names; they do not receive the pairing credential or contact the network. You can remove a paired installation from the mobile app, revoke a device from Aiden Agent, or remove the app and its local data using normal iOS or iPadOS controls. ## Mobile remote access -Remote Access is off by default in Aiden Agent. When you enable and pair Aiden On The Go, the mobile app connects directly to your Mac over your local network or a Tailscale connection you configure. Pairing uses a short-lived, one-use session and creates a revocable device credential. Aiden does not enable Tailscale Funnel or route this traffic through an Aiden-operated service. +Remote Access is off by default in Aiden Agent. When you enable and pair Aiden On The Go, the mobile app connects directly to your desktop over your local network or a Tailscale connection you configure. Pairing uses a short-lived, one-use session and creates a revocable device credential. Aiden does not enable Tailscale Funnel or route this traffic through an Aiden-operated service. -Chats, prompts, selected attachments, workspace operations, and approval decisions sent from Aiden On The Go go to the paired Mac. If a request uses an AI provider configured in Aiden Agent, the Mac may then send prompts, selected files, metadata, and responses to that provider or local model service under the provider's privacy policy, retention rules, and account settings. +Chats, prompts, selected attachments, workspace operations, and approval decisions sent from Aiden On The Go go to the paired desktop. If a request uses an AI provider configured in Aiden Agent, the desktop may then send prompts, selected files, metadata, and responses to that provider or local model service under the provider's privacy policy, retention rules, and account settings. ## iPhone and iPad permissions - **Local Network:** used only to discover or connect to an Aiden Agent installation on a network you choose. - **Camera:** used when you choose to scan a pairing QR code. Camera frames are processed for pairing and are not uploaded to Aiden. -- **Photos and Files:** content is accessed only after you select it. Selected content is sent to the paired Mac and may be processed by the AI provider you chose for the request. -- **Microphone and Speech Recognition:** requested only when you start dictation. In **On this device** mode, the app uses the platform speech API. In **Paired Mac** mode, it sends a bounded microphone recording through the authenticated, encrypted Aiden connection to the selected local Parakeet model on your Mac; neither endpoint stores the recording. The text composer remains usable if recognition is unavailable or permission is denied. +- **Photos and Files:** content is accessed only after you select it. Selected content is sent to the paired desktop and may be processed by the AI provider you chose for the request. +- **Microphone and Speech Recognition:** requested only when you start dictation. In **On this device** mode, the app uses the platform speech API. In **Paired desktop** mode, it sends a bounded microphone recording through the authenticated, encrypted Aiden connection to the selected local Parakeet model on your desktop; neither endpoint stores the recording. The text composer remains usable if recognition is unavailable or permission is denied. - **Notifications and Live Activities:** used for device-local status. Live Activities contain bounded last-known run state, use no Aiden cloud push relay, and hide assistant response excerpts by default. ## Bot image creation Bots always include an Aiden semantic avatar that works without Apple Intelligence. On supported devices running a compatible iOS or iPadOS version, you may choose **Create with Apple Intelligence** to open Apple's system Image Playground. Apple controls image generation and may use Private Cloud Compute under Apple's privacy terms. Personalization from people or the Photos library is disabled by Aiden, and Aiden supplies only the Bot name and purpose visible in the editor as starting concepts. -Aiden does not send Image Playground concepts, rejected candidates, or temporary file locations to Aiden's developer or to your paired Mac. Apple controls the system sheet and any Private Cloud Compute processing of the visible Bot name and purpose concepts. After you explicitly accept an image in Apple's sheet, Aiden copies it temporarily inside the app, removes metadata, center-crops and re-encodes it, and shows a preview. Only when you choose **Use this image** does Aiden send the normalized image directly to your paired Mac over the authenticated Remote Access connection. The Mac independently validates and stores its canonical copy. Temporary mobile candidates are deleted after use, cancellation, replacement, pairing changes, or editor dismissal. You can remove a generated Bot photo and return to the semantic avatar at any time. +Aiden does not send Image Playground concepts, rejected candidates, or temporary file locations to Aiden's developer or to your paired desktop. Apple controls the system sheet and any Private Cloud Compute processing of the visible Bot name and purpose concepts. After you explicitly accept an image in Apple's sheet, Aiden copies it temporarily inside the app, removes metadata, center-crops and re-encodes it, and shows a preview. Only when you choose **Use this image** does Aiden send the normalized image directly to your paired desktop over the authenticated Remote Access connection. The desktop independently validates and stores its canonical copy. Temporary mobile candidates are deleted after use, cancellation, replacement, pairing changes, or editor dismissal. You can remove a generated Bot photo and return to the semantic avatar at any time. Opening a web link or displaying externally hosted transcript media can make a normal network request to that third-party host. Aiden credentials are not forwarded to the host; the host may receive ordinary request information such as the device's network address under its own policy. @@ -45,6 +45,6 @@ Questions about privacy or support for Aiden Agent and Aiden On The Go can be se - Update the policy's “Last updated” date when this copy is published. - Preserve the existing disclosure that third-party AI providers process requests under their own policies. - Keep the direct statement that Aiden does not collect chats, prompts, selected files, provider keys, model responses, device identifiers, precise location, payment information, or analytics events unless the shipped product or operational services change. -- Describe Image Playground as Apple-controlled processing that may use Private Cloud Compute; do not promise universal on-device generation. Preserve the accepted-image-only direct-to-paired-Mac boundary. +- Describe Image Playground as Apple-controlled processing that may use Private Cloud Compute; do not promise universal on-device generation. Preserve the accepted-image-only direct-to-paired-desktop boundary. - Make the support email visibly reachable from `https://chatwithaiden.com/`, which is the App Store support URL. - Recheck this copy against the final distribution candidate and App Privacy answers before every submission. diff --git a/ios/app-store/metadata/version/0.1.0/en-US.json b/ios/app-store/metadata/version/0.1.0/en-US.json index b56efca7..173de7a1 100644 --- a/ios/app-store/metadata/version/0.1.0/en-US.json +++ b/ios/app-store/metadata/version/0.1.0/en-US.json @@ -1,5 +1,5 @@ { - "description": "Aiden On The Go is the native iPhone and iPad companion for Aiden Agent on your Mac. Pair directly with a Mac you control over your local network or Tailscale, then continue Aiden chats and manage workspaces from your mobile device.\n\nReview conversations, stream responses, handle approval requests, inspect workspace files, work with supported Git flows, and manage scheduled tasks. App Intents provide quick navigation, Live Activities show bounded run status, and optional voice dictation can use native recognition or a local model on your paired Mac. Read-aloud stays on device.\n\nYour Mac remains the execution authority. Remote Access is off by default, each mobile device uses a revocable credential, and provider credentials remain on the Mac.", + "description": "Aiden On The Go is the native iPhone and iPad companion for Aiden Agent on your macOS or Linux desktop. Pair directly with a desktop you control over your local network or Tailscale, then continue Aiden chats and manage workspaces from your mobile device.\n\nReview conversations, stream responses, handle approval requests, inspect workspace files, work with supported Git flows, and manage scheduled tasks. App Intents provide quick navigation, Live Activities show bounded run status, and optional voice dictation can use native recognition or a local model on your paired desktop. Read-aloud stays on device.\n\nYour desktop remains the execution authority. Remote Access is off by default, each mobile device uses a revocable credential, and provider credentials remain on the desktop.", "keywords": "AI assistant,agent,developer,Git,workspace,chat,automation,remote,Tailscale,Swift", "marketingUrl": "https://chatwithaiden.com/", "supportUrl": "https://chatwithaiden.com/" diff --git a/main/application-lifecycle-core.test.ts b/main/application-lifecycle-core.test.ts new file mode 100644 index 00000000..92092f9e --- /dev/null +++ b/main/application-lifecycle-core.test.ts @@ -0,0 +1,12 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { shouldQuitAfterAllWindowsClose } from "./application-lifecycle-core.js"; + +test("macOS retains its conventional last-window behavior", () => { + assert.equal(shouldQuitAfterAllWindowsClose("darwin", false), false); +}); + +test("Linux quits without background ownership and stays alive for Remote Access", () => { + assert.equal(shouldQuitAfterAllWindowsClose("linux", false), true); + assert.equal(shouldQuitAfterAllWindowsClose("linux", true), false); +}); diff --git a/main/application-lifecycle-core.ts b/main/application-lifecycle-core.ts new file mode 100644 index 00000000..87e11183 --- /dev/null +++ b/main/application-lifecycle-core.ts @@ -0,0 +1,6 @@ +export function shouldQuitAfterAllWindowsClose( + platform: NodeJS.Platform, + backgroundServiceRunning: boolean, +): boolean { + return platform !== "darwin" && !backgroundServiceRunning; +} diff --git a/main/desktop-cli-core.test.ts b/main/desktop-cli-core.test.ts new file mode 100644 index 00000000..13c87baa --- /dev/null +++ b/main/desktop-cli-core.test.ts @@ -0,0 +1,29 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { desktopVersionRequested } from "./desktop-cli-core.js"; + +test("packaged desktop recognizes only an explicit user --version argument", () => { + assert.equal(desktopVersionRequested(["/opt/Aiden Agent/aiden-agent"], false), false); + assert.equal( + desktopVersionRequested( + ["/opt/Aiden Agent/aiden-agent", "--no-sandbox", "--version"], + false, + ), + true, + ); +}); + +test("development desktop does not mistake the application path for an argument", () => { + assert.equal( + desktopVersionRequested(["/repo/node_modules/.bin/electron", "--version"], true), + false, + ); + assert.equal( + desktopVersionRequested( + ["/repo/node_modules/.bin/electron", "/repo", "--version"], + true, + ), + true, + ); +}); diff --git a/main/desktop-cli-core.ts b/main/desktop-cli-core.ts new file mode 100644 index 00000000..274e01e8 --- /dev/null +++ b/main/desktop-cli-core.ts @@ -0,0 +1,8 @@ +export function desktopVersionRequested( + argv: readonly string[], + defaultApp: boolean, +): boolean { + // Packaged Electron starts user arguments after argv[0]. Development + // Electron reserves argv[1] for the application path. + return argv.slice(defaultApp ? 2 : 1).includes("--version"); +} diff --git a/main/handlers/app.ts b/main/handlers/app.ts index 73d0d61f..41e9d29c 100644 --- a/main/handlers/app.ts +++ b/main/handlers/app.ts @@ -18,6 +18,7 @@ import { app, logger } from "../platform.js"; import { currentRuntimeProfile } from "../runtime-profile.js"; +import { hostPlatformCapabilities } from "../services/host-platform-capabilities.js"; import { subagentsEnabled } from "../services/subagents/feature-flag.js"; // App handlers - these are the methods your app provides to the frontend @@ -25,12 +26,20 @@ export const appHandlers = { // Example: Get app information getInfo: async () => { logger.info("app", "App info requested"); + const host = hostPlatformCapabilities(); return { name: app.getName(), version: app.getVersion(), environment: currentRuntimeProfile().id, capabilities: { + platform: host.platform, subagents: subagentsEnabled(), + bots: host.bots, + computerUse: host.computerUse, + dockIcon: host.dockIcon, + accessibilityPaste: host.accessibilityPaste, + nativeShare: host.nativeShare, + appleFoundationModels: host.appleFoundationModels, }, }; }, diff --git a/main/handlers/bots-platform-contract.test.ts b/main/handlers/bots-platform-contract.test.ts new file mode 100644 index 00000000..37c06c64 --- /dev/null +++ b/main/handlers/bots-platform-contract.test.ts @@ -0,0 +1,31 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import test from "node:test"; + +test("Bot IPC registration is narrowed by the main-owned host policy", () => { + const source = readFileSync(new URL("./index.ts", import.meta.url), "utf8"); + assert.match( + source, + /if \(hostPlatformCapabilities\(\)\.bots\) registerBotHandlers\(\);\s+registerBtwHandlers\(\)/u, + ); +}); + +test("ordinary chat paths cannot activate Bot services on unsupported hosts", () => { + const chatHandlers = readFileSync(new URL("./chats.ts", import.meta.url), "utf8"); + const llmClient = readFileSync( + new URL("../services/llm-client.ts", import.meta.url), + "utf8", + ); + assert.match( + chatHandlers, + /if \(source\.botId\) \{\s+if \(!hostPlatformCapabilities\(\)\.bots\)/u, + ); + assert.match( + chatHandlers, + /if \(chat\?\.botId\) \{\s+if \(!hostPlatformCapabilities\(\)\.bots\) \{\s+return chatApplicationService\.remove\(chatId\)/u, + ); + assert.match( + llmClient, + /if \(chat\.botId && !hostPlatformCapabilities\(\)\.bots\) \{\s+throw new Error\("Bot chats are not available on this platform\."\)/u, + ); +}); diff --git a/main/handlers/bots.contract.test.ts b/main/handlers/bots.contract.test.ts index 65018d97..200ced10 100644 --- a/main/handlers/bots.contract.test.ts +++ b/main/handlers/bots.contract.test.ts @@ -175,7 +175,7 @@ test("Remote production wires Bot notice and retained-chat policy authority", () assert.match(remote, /revokeNoticeAudience\(deviceId\)/u); assert.match( remote, - /const revoked = await revokeAidenRemoteRuntimeDevice[\s\S]*await botApplicationService\.revokeNoticeAudience\(deviceId\);\s*return revoked;/u, + /const revoked = await revokeAidenRemoteRuntimeDevice[\s\S]*if \(hostPlatformCapabilities\(\)\.bots\) \{\s*await botApplicationService\.revokeNoticeAudience\(deviceId\);\s*\}\s*return revoked;/u, ); }); diff --git a/main/handlers/chats.ts b/main/handlers/chats.ts index b411decd..b4a71aa3 100644 --- a/main/handlers/chats.ts +++ b/main/handlers/chats.ts @@ -52,6 +52,7 @@ import { import { chatForRenderer } from "../services/visible-chat-projection.js"; import { chatActivityRegistry } from "../services/chat-activity.js"; import { botApplicationService } from "../services/bot-application-service-main.js"; +import { hostPlatformCapabilities } from "../services/host-platform-capabilities.js"; import { piCompactionSessionStore } from "../services/pi-compaction-session-store.js"; import { isTodoSnapshotFailure, replayTodoState } from "../services/rpiv-todo/replay.js"; import { @@ -214,6 +215,9 @@ export function registerChatHistoryHandlers(): void { } const runCopy = async () => { if (source.botId) { + if (!hostPlatformCapabilities().bots) { + throw new Error("Bot chats are not available on this platform."); + } const assertCurrent = () => { if (owner.isDestroyed()) { throw new Error("The application changed before the Bot chat was copied."); @@ -465,6 +469,9 @@ export function registerChatHistoryHandlers(): void { const chatId = asString(id, "id"); const chat = await chatStore.get(chatId); if (chat?.botId) { + if (!hostPlatformCapabilities().bots) { + return chatApplicationService.remove(chatId); + } return botApplicationService.deleteChat({ botId: chat.botId, chatId }); } return chatApplicationService.remove(chatId); diff --git a/main/handlers/computer-use.ts b/main/handlers/computer-use.ts index e0b7a453..7cdb6aa9 100644 --- a/main/handlers/computer-use.ts +++ b/main/handlers/computer-use.ts @@ -1,6 +1,10 @@ import { ipcMain } from "../platform.js"; import { computerUseStatus } from "../services/computer-use/status.js"; import { computerUseSettings } from "../services/computer-use/settings.js"; +import { + computerUseSupported, + unsupportedComputerUseStatus, +} from "../services/computer-use/platform.js"; import { rendererDocumentOwner, type RendererDocumentOwner, @@ -32,14 +36,23 @@ async function ownedStatusRequestConnection status is unavailable.
) : snapshot.devices.length === 0 ? ( -No devices have been paired with this Mac.
+No devices have been paired with this desktop.
) : ( <>