Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
1f89bbf
fix: remove legacy codex hooks config (#175)
ZiYang-oyxy May 26, 2026
0929926
feat: 支持灵动岛宽度设置 (#171)
Makia9879 May 26, 2026
29157ed
fix(AskUserQuestion): always include questions key and use question t…
sudo-yf May 26, 2026
c6820d2
test: align AskUserQuestion tests with question-text answer keys
wxtsky May 26, 2026
be8bec4
fix(codex): respect user-deleted hook events during auto-repair (#182)
wxtsky May 26, 2026
6392b30
fix(remote): install Hermes hooks on SSH remote hosts (#176)
wxtsky May 26, 2026
e1faa46
fix(permissions): don't deny parallel tool calls sharing a tool_use_i…
wxtsky May 26, 2026
6c7d66c
Improve Buddy Bluetooth recovery and signing (#187)
Lakphy May 26, 2026
14c2c10
test: silence concurrent-capture warnings in JSONLTailerTests
wxtsky May 26, 2026
e8ceed8
release: v1.0.25
wxtsky May 26, 2026
6ad7a1c
feat: add Apple companion prototype
fengye404 May 26, 2026
e5667eb
fix: tighten watch companion layout
fengye404 May 26, 2026
d1d8d63
fix: page watch companion experience
fengye404 May 26, 2026
bded9d1
feat: add watch notifications and complications
fengye404 May 26, 2026
3efd9e1
fix: surface live activity failures
fengye404 May 26, 2026
1147946
feat: add Bluetooth companion background channel
fengye404 May 26, 2026
cc45c81
test: add background live activity smoke hook
fengye404 May 26, 2026
54214a8
test: add watch companion UI smoke coverage
fengye404 May 26, 2026
9f164e0
docs: add apple companion guide
fengye404 May 26, 2026
6044c0d
fix: refresh stale apple companion sync
fengye404 May 26, 2026
808f9b5
chore: prepare apple companion for app review
fengye404 May 26, 2026
7da6855
fix: initialize companion bluetooth for background restore
fengye404 May 26, 2026
9484037
Add Apple companion UI test coverage
fengye404 May 27, 2026
f34e949
docs: clarify apple companion communication
fengye404 May 27, 2026
562df64
fix: include required watch app icons
fengye404 May 27, 2026
209959d
feat: add pi coding-agent integration (#197)
cat0825 May 30, 2026
f42e264
fix(remote): per-user SSH socket isolation; harden tunnel and iTerm2 …
wxtsky May 30, 2026
2fad1b1
fix(remote): harden iTerm2 window select and remote uid probe (#202)
wxtsky May 30, 2026
48f9b93
release: v1.0.26
wxtsky May 30, 2026
c406771
fix(jump): match IDE workspace window by cwd for multi-window apps (#…
wxtsky May 30, 2026
ef7db33
feat(remote): install custom CLI hooks on SSH remote hosts (#192) (#204)
wxtsky May 30, 2026
c918539
release: v1.0.27
wxtsky May 30, 2026
3eeafe9
fix(remote): clean stale remote socket before -R forwarding (#206) (#…
wxtsky May 31, 2026
f878234
fix(warp): correct tab activation across windows and tabs (#205)
wxtsky May 31, 2026
d4f3bbe
feat: polish iPhone Buddy release flow
fengye404 Jun 4, 2026
5df688b
Merge remote-tracking branch 'upstream/main' into codex/apple-compani…
fengye404 Jun 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ docs/superpowers/
# IDE vscode/cursor
.vscode/
.cursor/
xcuserdata/
*.xcuserstate

# Tmp files
*.log
Expand All @@ -25,7 +27,45 @@ android-watch/local.properties
android-watch/**/build/

# Code agents config files
.adal/
.aider-desk/
.agents/
.augment/
.bob/
.claude/
.codeartsdoer/
.codebuddy/
.codemaker/
.codestudio/
.commandcode/
.continue/
.cortex/
.crush/
.devin/
.factory/
.forge/
.goose/
.hermes/
.iflow/
.junie/
.kilocode/
.kiro/
.kode/
.mcpjam/
.mux/
.neovate/
.openhands/
.pi/
.pochi/
.qoder/
.qwen/
.roo/
.rovodev/
.tabnine/
.trae/
.vibe/
.windsurf/
.zencoder/
skills/
skills-lock.json
CLAUDE.md
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,29 @@
# Changelog

## [v1.0.27] - 2026-05-30

### English
- Fix Cursor / Trae / Qoder / Factory click-to-jump raising the most-recently-used window instead of the one running the clicked session — now matches the workspace window by project folder (#199)
- Install custom CLI hooks on SSH remote hosts too (claude / nested hook formats) — previously only the built-in CLIs were configured remotely (#192)

### 中文
- 修复 Cursor / Trae / Qoder / Factory 点击灵动岛跳到"最近用过的窗口"而不是正在对话的那个——现在按项目目录匹配对应 workspace 窗口 (#199)
- SSH 远程主机也会安装自定义 CLI 的 hooks(claude / nested 格式)——此前远程只配置内置 CLI (#192)

## [v1.0.26] - 2026-05-30

### English
- Add pi / Oh My Pi (OMP) coding agent integration — auto-install the bundled extension into `~/.pi/agent/extensions` and `~/.omp/agent/extensions` (#197)
- Isolate the remote SSH socket per user (`/tmp/codeisland-<uid>.sock`) so multiple OS users on a shared host no longer collide or steal each other's events (#193)
- Fix the SSH tunnel being misreported as `ssh exited (0)` when ControlMaster multiplexing makes `ssh -N` hand off the forward and exit immediately — force a dedicated connection (#190)
- Fix iTerm2 click-to-jump landing on the wrong window when the target session is fullscreen or on another Space — select the owning window so macOS switches to its Space (#198)

### 中文
- 新增 pi / Oh My Pi (OMP) 编码 agent 集成——自动把扩展装到 `~/.pi/agent/extensions` 和 `~/.omp/agent/extensions` (#197)
- 远程 SSH socket 改为按用户隔离(`/tmp/codeisland-<uid>.sock`),多用户共享主机不再互相串话或抢占事件 (#193)
- 修复 SSH 隧道在 ControlMaster 多路复用下 `ssh -N` 立即退出、被误报为 `ssh exited (0)` 的问题——强制独占连接 (#190)
- 修复 iTerm2 全屏 / 跨 Space 时点击会话跳到错误窗口——命中后选中目标窗口以触发 Space 切换 (#198)

## [v1.0.23] - 2026-04-25

### English
Expand Down
14 changes: 10 additions & 4 deletions Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0.24</string>
<string>1.0.27</string>
<key>CFBundleVersion</key>
<string>1.0.24</string>
<string>1.0.27</string>
<key>LSUIElement</key>
<true/>
<key>NSAppTransportSecurity</key>
Expand All @@ -27,6 +27,8 @@
</dict>
<key>NSAppleEventsUsageDescription</key>
<string>CodeIsland needs to control terminal apps to jump to the correct window and tab when you click a session.</string>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>CodeIsland uses Bluetooth to mirror the island onto Buddy.</string>
<key>SUAutomaticallyUpdate</key>
<false/>
<key>SUEnableAutomaticChecks</key>
Expand All @@ -37,7 +39,11 @@
<string>oqLtx5s2hc8Xgsp4rEuTwnQ8UGRT4ma4tjlf+1i3YHA=</string>
<key>SUScheduledCheckInterval</key>
<integer>14400</integer>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>CodeIsland uses Bluetooth to mirror the island onto Buddy.</string>
<key>NSBonjourServices</key>
<array>
<string>_codeisland._tcp</string>
</array>
<key>NSLocalNetworkUsageDescription</key>
<string>CodeIsland advertises itself on the local network so iPhone can mirror the island state.</string>
</dict>
</plist>
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ Requires **macOS 14+** and **Swift 5.9+**.
git clone https://github.com/wxtsky/CodeIsland.git
cd CodeIsland

# Development (debug build + launch)
# Development (debug build + launch; Buddy Bluetooth needs the .app below)
swift build && ./.build/debug/CodeIsland

# Release (universal binary: Apple Silicon + Intel)
Expand Down
2 changes: 1 addition & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ brew install --cask codeisland
git clone https://github.com/wxtsky/CodeIsland.git
cd CodeIsland

# 开发模式(debug 构建 + 启动)
# 开发模式(debug 构建 + 启动;Buddy 蓝牙需要下面的 .app
swift build && ./.build/debug/CodeIsland

# 发布模式(通用二进制:Apple Silicon + Intel)
Expand Down
3 changes: 1 addition & 2 deletions Sources/CodeIsland/AntiGravityView.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import SwiftUI
import CodeIslandCore

/// AntiGravityBot — AntiGravity mascot, rainbow gradient swoosh character.
/// Multicolor gradient inspired by the AntiGravity "A" logo.
struct AntiGravityView: View {
let status: AgentStatus
let status: MascotAgentStatus
var size: CGFloat = 27
@State private var alive = false
@Environment(\.mascotSpeed) private var speed
Expand Down
19 changes: 19 additions & 0 deletions Sources/CodeIsland/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,19 @@ class AppDelegate: NSObject, NSApplicationDelegate {
guard let appState else { return }
appState.handleBuddyControlCommand(command)
}
AppleCompanionPublisher.shared.attach(appState)
AppleCompanionPublisher.shared.onFocusRequest = { [weak appState] mascot in
guard let appState else { return }
ESP32FocusCoordinator.handle(mascot: mascot, appState: appState)
}
AppleCompanionPublisher.shared.onControlCommand = { [weak appState] command in
guard let appState else { return }
appState.handleBuddyControlCommand(command)
}
AppleCompanionPublisher.shared.onQuestionAnswer = { [weak appState] answer in
guard let appState else { return }
appState.answerCompanionQuestion(answer)
}
let buddyEnabled = UserDefaults.standard.bool(forKey: SettingsKey.esp32BridgeEnabled)
let buddySyncInterval = UserDefaults.standard.double(forKey: SettingsKey.esp32HeartbeatSeconds)
let buddyBrightness = UserDefaults.standard.double(forKey: SettingsKey.buddyScreenBrightnessPercent)
Expand All @@ -73,6 +86,12 @@ class AppDelegate: NSObject, NSApplicationDelegate {
brightnessPercent: buddyBrightness > 0 ? buddyBrightness : SettingsDefaults.buddyScreenBrightnessPercent,
screenOrientation: buddyScreenOrientation
)
let appleCompanionEnabled = UserDefaults.standard.bool(forKey: SettingsKey.appleCompanionEnabled)
let appleCompanionHeartbeat = UserDefaults.standard.double(forKey: SettingsKey.appleCompanionHeartbeatSeconds)
AppleCompanionPublisher.shared.configure(
enabled: appleCompanionEnabled,
heartbeatSeconds: appleCompanionHeartbeat > 0 ? appleCompanionHeartbeat : SettingsDefaults.appleCompanionHeartbeatSeconds
)

// Hooks auto-recovery: periodic + app activation trigger
hookRecoveryTimer = Timer.scheduledTimer(withTimeInterval: 300, repeats: true) { [weak self] _ in
Expand Down
11 changes: 11 additions & 0 deletions Sources/CodeIsland/AppState+ToolUseCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,17 @@ extension AppState {
else { return false }

let existing = permissionQueue[existingIndex]
// #169: a shared tool_use_id alone is not enough to call this a replay.
// Claude Code can emit several *parallel* tool calls (e.g. reading 4
// files at once); if they carry the same id but different inputs they are
// distinct requests and each needs its own decision. Treat it as a replay
// — deny the old waiter, keep the new one in place — only when the tool
// inputs match. Otherwise let the new request enqueue on its own.
let existingInput = existing.event.toolInput ?? [:]
let newInput = request.event.toolInput ?? [:]
guard NSDictionary(dictionary: existingInput).isEqual(to: NSDictionary(dictionary: newInput)) else {
return false
}
log.notice("⚠️ permission deny reason=mergeDuplicatePermissionRequest session=\(existing.event.sessionId ?? "nil", privacy: .public) toolUseId=\(toolUseId, privacy: .public) tool=\(existing.event.toolName ?? "nil", privacy: .public)")
let denyBody = #"{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"deny"}}}"#
existing.continuation.resume(returning: Data(denyBody.utf8))
Expand Down
49 changes: 44 additions & 5 deletions Sources/CodeIsland/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,7 @@ final class AppState {
rotatingSessionId = cachedActiveIds.first
}
ESP32StatePublisher.shared.notifyDirty()
AppleCompanionPublisher.shared.notifyDirty()
}

/// Start monitoring the CLI process for a session.
Expand Down Expand Up @@ -875,6 +876,7 @@ final class AppState {
if activeSessionCount != summary.activeSessionCount { activeSessionCount = summary.activeSessionCount }
if totalSessionCount != summary.totalSessionCount { totalSessionCount = summary.totalSessionCount }
ESP32StatePublisher.shared.notifyDirty()
AppleCompanionPublisher.shared.notifyDirty()
}

private func refreshProviderTitle(for trackedSessionId: String, providerSessionId: String? = nil) {
Expand Down Expand Up @@ -1153,6 +1155,38 @@ final class AppState {
}
}

func answerCompanionQuestion(_ answer: String) {
guard !questionQueue.isEmpty else {
log.info("Ignored companion question answer because question queue is empty")
return
}

if questionQueue[0].isFromPermission,
var askState = questionQueue[0].askUserQuestionState {
guard let index = askState.items.firstIndex(where: { askState.answers[$0.answerKey] == nil }) else {
answerQuestionMulti(askState.items.map {
(question: $0.payload.question, answer: askState.answers[$0.answerKey] ?? "")
})
return
}

let item = askState.items[index]
askState.answers[item.answerKey] = answer
questionQueue[0].askUserQuestionState = askState

if askState.canConfirm {
answerQuestionMulti(askState.items.map {
(question: $0.payload.question, answer: askState.answers[$0.answerKey] ?? "")
})
} else {
refreshDerivedState()
}
return
}

answerQuestion(answer)
}

/// Find an existing session whose source matches and whose CLI PID equals
/// the supplied ppid. Used by HookServer to merge plugin-proxied events
/// (e.g. omo) into their main session when pluginSessionMode == "merge". (#123)
Expand Down Expand Up @@ -1303,8 +1337,10 @@ final class AppState {
descriptions: optionDescs,
header: header
)
let trimmedHeader = header?.trimmingCharacters(in: .whitespacesAndNewlines)
let baseKey = (trimmedHeader?.isEmpty == false ? trimmedHeader : nil) ?? "answer_\(index + 1)"
// Claude Code's mapToolResultToToolResultBlockParam looks up answers by
// question text: `answers[question.question]`. Using header as the key
// causes a mismatch and all answers arrive as empty strings.
let baseKey = questionText
var answerKey = baseKey
if usedAnswerKeys.contains(answerKey) {
var suffix = 2
Expand Down Expand Up @@ -1481,9 +1517,12 @@ final class AppState {
originalQuestions: [[String: Any]]?
) -> [String: Any] {
var updatedInput = event.toolInput ?? [:]
if let originalQuestions {
updatedInput["questions"] = originalQuestions
}
// `questions` must always be present in updatedInput. Claude Code's
// mapToolResultToToolResultBlockParam calls H.map() on it directly;
// if the key is absent H is undefined and the call crashes with
// "undefined is not an object (evaluating 'H.map')".
// Fall back to the raw toolInput value when the [[String:Any]] cast fails.
updatedInput["questions"] = originalQuestions ?? (event.toolInput?["questions"] ?? [] as [[String: Any]])
updatedInput["answers"] = answers
if let answer {
updatedInput["answer"] = answer
Expand Down
Loading