Skip to content

feat(companion): add iPhone Buddy app and watch sync#1

Closed
fengye404 wants to merge 36 commits into
mainfrom
codex/apple-companion-prototype
Closed

feat(companion): add iPhone Buddy app and watch sync#1
fengye404 wants to merge 36 commits into
mainfrom
codex/apple-companion-prototype

Conversation

@fengye404
Copy link
Copy Markdown
Owner

@fengye404 fengye404 commented May 26, 2026

Summary

Adds the Code Island Buddy iPhone and Apple Watch experience for mirroring Code Island Mac agent sessions. The branch includes the Mac publishing bridge, iPhone app, Live Activity / Dynamic Island / StandBy surfaces, Apple Watch app and widget, and App Store release documentation for the first public iOS submission.

Changes

Added

  • iPhone Buddy app with local Mac discovery, demo mode, Live Activity, Dynamic Island, Lock Screen, and StandBy views.
  • Apple Watch app and watchOS widget surfaces for session status, messages, actions, and activity.
  • Mac-side Buddy publishing over local network and Bluetooth summaries for background refresh.
  • Shared companion payload models, including question details and multi-session previews.
  • App Store metadata, review notes, privacy policy draft, device testing guide, screenshots, and smoke scripts.

Changed

  • Renamed user-facing Apple companion copy to iPhone Buddy / Buddy across Mac settings and release docs.
  • Extended iPhone, Live Activity, and Watch rendering to show multiple prioritized Code Island sessions.
  • Prepared iOS project metadata for the public 1.0.0 build 4 release.

Fixed

  • Preserved companion state freshness across background restore and Bluetooth reconnect paths.
  • Added required watch app icons and tightened watch/live activity layout smoke coverage.

Motivation

Code Island now has a companion surface beyond the Mac: users can keep an eye on AI agent status, questions, tool activity, and recent session context from iPhone, Dynamic Island, StandBy, and Apple Watch. This also gives App Store reviewers a documented path to verify the iOS app with or without a paired Mac.

Testing

  • Unit tests added/updated
  • UI smoke coverage added/updated
  • Manual/device release notes documented

Commands run locally:

  • git diff --check
  • swift test
  • swift test --filter AppleCompanionPayloadTests
  • swift test --filter 'AppleCompanionPayloadTests|AppStatePrimarySourceTests|AppStateQuestionFlowTests'
  • scripts/check-companion-ui-regressions.sh
  • scripts/smoke-companion.sh
  • scripts/smoke-companion-ui.sh

Release Notes

This PR does not include a signed Mac DMG artifact. The final Mac DMG should be produced by the maintainer after merge using the repository's Developer ID certificate, Apple notarization profile, Sparkle update signing key, and GitHub Release permissions.

Checklist

  • Code follows existing project patterns
  • Self-review completed
  • Tests pass locally
  • Documentation updated

ZiYang-oyxy and others added 30 commits May 26, 2026 09:54
…ext as answer key (wxtsky#191)

Two bugs in the AskUserQuestion round-trip with Claude Code:

1. askUserQuestionUpdatedInput omitted the 'questions' key when
   originalQuestions was nil (cast failure). Claude Code's
   mapToolResultToToolResultBlockParam calls H.map() on questions directly,
   so a missing key causes 'undefined is not an object (evaluating H.map)'.
   Fix: always write questions, falling back to the raw toolInput value.

2. answerKey was derived from the question's header, but Claude Code looks up
   answers by question text (answers[question.question]). The key mismatch
   caused every answer to come back as an empty string.
   Fix: use questionText as the base key instead of header.

Co-authored-by: shiyi20060618-cmd <202430841089@mail.scut.edu.cn>
wxtsky#191 switched AskUserQuestion answer keys from header to question text
(matching Claude Code's `answers[question.question]` lookup) but left the
existing AppStateQuestionFlowTests asserting old header-based keys, turning
main red. Update the four affected assertions, rewrite the dedup/fallback
cases to cover the new behavior (duplicate question text gets a suffixed
key; header no longer participates in the key), and drop the now-unused
`trimmedHeader` binding wxtsky#191 left behind so the build is warning-free again.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ky#182)

verifyAndRepair treated any CLI whose hooks file was missing an event as a
corrupt config and rewrote the full event list, clobbering a user who
intentionally kept only a subset of (e.g. Codex) hooks. Add
shouldPreservePartialHooks so repair only fires when nothing of ours remains
or a stale `async` entry needs cleanup; a partial-but-intact config is left
untouched. State queries and the explicit install/enable path are unchanged,
so first-time install and manual re-enable still write every event.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The remote install script wired up claude/codex/codebuddy/traecli/opencode
but omitted Hermes, so SSH-monitored Hermes sessions never sent events.
Hermes is a Claude Code fork (same settings.json + hooks layout), so mirror
install_claude into install_hermes and add it to the install list. Covered by
a new script-content assertion; the existing python-compile test now exercises
its syntax as well.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
wxtsky#169)

mergeDuplicatePermissionRequest collapsed any queued request with a matching
tool_use_id, denying the old waiter. But Claude Code can emit several parallel
tool calls (e.g. "Read 4 files") that share an id while operating on different
inputs — those are distinct requests, and merging them denied all but the last,
surfacing as "denied by PermissionRequest hook" on tools the user never
rejected. Only merge as a replay when the tool inputs also match; otherwise let
the new request enqueue and get its own decision.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: 添加 Buddy 硬件桌宠的 README 文档

Co-authored-by: Copilot <copilot@github.com>

* feat: 添加 Buddy 桌宠使用前提说明

* Add question alert functionality to various mascot modules

- Introduced `*_Question` functions in multiple mascot header files to enable a question alert mode.
- Updated `drawBang` function to render a question mark when `_questionMode` is active.
- Modified `clawdSleep` to include a bored yawn animation based on global bored state.
- Added global variables for bored state and eye offset to enhance mascot behavior.
- Implemented tool icon classification and drawing functions for better visual representation of tools.

* feat: enhance Buddy pairing process and add legacy support

- Updated ESP32BridgeManager to handle legacy pairing fallback and timeout scenarios.
- Improved error messages and status handling for pairing states.
- Added new payload for clearing tool history.
- Enhanced localization strings for better user guidance.
- Updated README with clearer pairing instructions and hints for legacy firmware mode.
- Added tests for new functionality in AppState and ESP32Protocol.

* feat: implement write frame queue management for Buddy BLE communication

* Improve Buddy Bluetooth recovery and signing

* Fix Buddy recovery edge cases

---------

Co-authored-by: Copilot <copilot@github.com>
Both integration tests capture a mutable var inside the tailer's serial onDelta
callback; Swift 6 mode flags it as a data race even though the test only reads
after wait()/sleep, so the access is ordered. Mark the two vars
nonisolated(unsafe) to restore a warning-free build (surfaced by wxtsky#187's
full recompile of CodeIslandCore; the warnings pre-dated it).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…jump (wxtsky#201)

- wxtsky#193: the remote SSH socket was hardcoded to /tmp/codeisland.sock, so multiple
  OS users on a shared host collided. With StreamLocalBindUnlink a later user's
  reverse forward would unlink/rebind another user's socket, silently stealing
  their events. Probe the remote uid at connect time and route a per-user
  /tmp/codeisland-<uid>.sock through the SSH -R forward, the remote hook command
  (CODEISLAND_SOCKET_PATH), and the OpenCode plugin. Remote hook script also
  defaults to a uid-scoped path. Falls back to the legacy shared path if the
  probe fails.
- wxtsky#190: force ControlMaster=no / ControlPath=none on the tunnel so `ssh -N`
  doesn't hand the forward to a pre-existing multiplexing master and exit 0
  immediately, which the forwarder misreads as a failed connection ("ssh exited (0)").
- wxtsky#198: the iTerm2 jump now selects the matched *window* (not just tab+session)
  so a fullscreen / cross-Space session is raised and its Space switched to.

Tests: per-user socket injection into the remote hook script and OpenCode plugin,
plus the legacy fallback path.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…#202)

Follow-up hardening on wxtsky#201:
- Guard each iTerm2 `select <window>` with its own `try` so an unexpected failure
  (e.g. a window mid fullscreen-transition) can't abort the surrounding script and
  skip the tab/session select — that would have made the jump worse than before.
- Probe the remote uid with a bare `id -u` instead of a `$(...)` pipeline so it
  runs identically under non-POSIX login shells (fish/csh) instead of silently
  falling back to the shared socket path. Also drops the redundant cleanup
  round-trip (StreamLocalBindUnlink already clears stale sockets on bind), so a
  failing host no longer waits on a second SSH.
- Remove the now-unused shellSingleQuoted helper.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- pi / Oh My Pi (OMP) coding agent integration (wxtsky#197)
- per-user remote SSH socket isolation (wxtsky#193)
- SSH tunnel ControlMaster=no fix for ssh exited(0) (wxtsky#190)
- iTerm2 fullscreen/cross-Space jump fix (wxtsky#198)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…xtsky#199) (wxtsky#203)

Cursor / Trae / Qoder / Factory etc. can hold several workspace windows at
once. The native-app-bundle jump path only did app-level activation, so
clicking a session card raised whichever window was most recently used
rather than the one actually running the agent (wxtsky#199). Route these through
activateIDEWindow, which AXRaises the window whose title contains the
session's project folder and falls back to plain app activation when there's
no cwd or no matching window — so single-window apps don't regress.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
wxtsky and others added 5 commits May 30, 2026 10:48
… (wxtsky#204)

Remote install only handled the five built-in CLIs (claude/codex/codebuddy/
traecli/opencode) plus hermes; user-defined custom CLIs were never pushed to
the remote, so a custom CLI running on the remote host got no hooks.

Serialize the custom CLI configs into the remote install script and write
their hooks alongside the built-ins via a new install_custom(). Limited to the
claude/nested JSON-hook formats: their stdin carries hook_event_name, so the
remote hook (codeisland-remote-hook.py) handles them with no --event flag.
Cursor-style "flat" (needs --event) and the non-JSON formats (traecli/copilot/
kimi/kiroAgent/cline) are skipped remotely; they still work locally.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- IDE multi-window jump: match Cursor/Trae/Qoder/Factory workspace window by cwd (wxtsky#199)
- install custom CLI hooks on SSH remote hosts (claude/nested formats) (wxtsky#192)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…) (wxtsky#207)

macOS system SSH (LibreSSL) ignores StreamLocalBindUnlink=yes for -R
remote socket forwarding, so a stale listen socket is left behind when a
tunnel drops (sleep, network blip, disconnect). Reconnect then fails with
"remote port forwarding failed", surfacing as ssh exited(255). Remove the
stale socket with `rm -f` over a short-lived SSH before opening the tunnel.

The cleanup runs off the main thread, so a blocking ConnectTimeout never
freezes the menu-bar UI when the host is unreachable; the tunnel starts
only after cleanup returns, so the -R bind never races the leftover socket.

Closes wxtsky#206

Co-Authored-By: Stark-Lou <8287623+Stark-Lou@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drive Warp tab focus more reliably:
- Replace blanket app activation with raiseAppWithoutQuickTerminal so we do
  not pop the Quick Terminal, and post the Cmd+digit keystroke to Warp's pid
  only once it is actually frontmost (retrying briefly otherwise).
- Map tab index to Warp's shortcut semantics: Cmd+1..8 select tabs 1..8 and
  Cmd+9 selects the last tab.
- Read Warp's SQLite without nolock=1 so WAL writes are honored (fresh tab
  state), match cwd case-insensitively on case-insensitive volumes, and
  expose tabCountInWindow / isActiveTab(cwd) for tab-level visibility.

Verified against a live Warp database: tabIndexInWindow, tabCountInWindow,
isActiveTab and case-insensitive matching all agree with the real layout.

Co-Authored-By: rhinoc <9894617+rhinoc@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@fengye404 fengye404 added the enhancement New feature or request label Jun 4, 2026
@fengye404 fengye404 changed the title Add Apple companion prototype feat(companion): add iPhone Buddy app and watch sync Jun 4, 2026
@fengye404 fengye404 added the documentation Improvements or additions to documentation label Jun 4, 2026
…on-prototype

# Conflicts:
#	Info.plist
#	Sources/CodeIsland/ESP32StatePublisher.swift
#	Tests/CodeIslandCoreTests/JSONLTailerTests.swift
#	Tests/CodeIslandTests/AppStatePrimarySourceTests.swift
@fengye404
Copy link
Copy Markdown
Owner Author

Closing this fork-local PR because the branch is now submitted to the upstream repository: wxtsky#218

@fengye404 fengye404 closed this Jun 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants