fix: prevent Sparkle crash in DEBUG mode when running without bundle ID#133
Merged
Merged
Conversation
nguyenvanduocit
pushed a commit
to nguyenvanduocit/CodeIsland
that referenced
this pull request
Apr 27, 2026
No new actionable upstream changes since v1.0.23 (Apr 25). PR wxtsky#133 (Sparkle DEBUG fix), wxtsky#135 (Turkish L10n), wxtsky#136 (wrong-repo) and Issue wxtsky#134 (cursor-agent/qodercli) all skipped. Notes GitHub Issues disabled in our repo; tracking continues via kanban only. https://claude.ai/code/session_01GExyntV3NY82SUvfvjEjJW
Uncle-Peke
added a commit
to Uncle-Peke/CodeIsland
that referenced
this pull request
May 4, 2026
* chore: polish Turkish translation for stylistic consistency (wxtsky#135) Align 4 post-PR entries with the established style (Title Case for setting labels, ALL CAPS for action buttons, correct semantics): - auto_collapse_after_session_jump: lowercase → Title Case - dismiss: ERTELE (postpone) → YOKSAY (ignore/dismiss) - buddy_reconnect: sentence case → Title Case - buddy_enable_bluetooth: sentence case → Title Case Key parity unchanged (259/259 keys covered). Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: prevent Sparkle crash in DEBUG mode when running without bundle ID (wxtsky#133) Co-authored-by: JZ <jz@JZdeMacBook-Pro.local> * fix: avoid stale update abort errors (wxtsky#138) * feat: improve Buddy watch approval previews and alerts (wxtsky#144) Show the full Bash approval command alongside the human-readable description so pending approvals stay understandable on macOS and on the watch. Preserve pending TraeCLI approvals until the user responds, add watch-side approve/deny and skip controls over BLE, and trigger vibration plus richer attention notifications when approval is required. * test: align AppStateQuestionFlow assertions with wxtsky#144 toolDescription change wxtsky#144 changed `HookEvent.toolDescription` for Bash to combine description and command (so ESP32/watch previews show the full command). Update the existing two assertions to match the new format. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: add ProcessRunner with timeout to harden detectClaudeVersion (wxtsky#139) Replace the unbounded waitUntilExit() in detectClaudeVersion with a shared ProcessRunner that enforces a hard deadline (SIGTERM, then SIGKILL after 1s grace) and drains stdout off the wait path so a full pipe buffer cannot wedge the child. detectClaudeVersion now uses a 5s timeout — a stuck `claude --version` used to freeze app launch silently. Refs wxtsky#139. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: move launch-time hook installation off the main thread (wxtsky#139) ConfigInstaller.install() does subprocess version detection plus disk I/O across multiple CLI configs. Running it inline from applicationDidFinishLaunching meant a slow CLI binary or network home dir froze the menu bar / panel until install finished — sometimes silently, because macOS doesn't pop a "Not Responding" dialog for status items. Detach it onto a userInitiated Task so app UI comes up immediately while hooks install in the background. HookServer is still started synchronously before the detach so the socket is listening by the time settings.json is rewritten (preserves the ordering noted in the existing comment). Mark AppDelegate.log nonisolated so the detached task can use it without violating Swift 6 actor isolation (Logger is Sendable). Refs wxtsky#139. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: run periodic hook verifyAndRepair off the main thread (wxtsky#139) checkAndRepairHooks fires from a 300s timer AND every NSWorkspace didActivateApplicationNotification (i.e. each time the user app-switches). verifyAndRepair walks every enabled CLI and rewrites settings.json — on a network home dir or a slow disk that's enough to stutter the panel. Keep the 60s debounce on the main actor (cheap), but do the actual repair on a background task. Repair results only feed a log line so no UI thread hand-off is needed. Refs wxtsky#139. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: cap subprocess waits in TerminalActivator/VisibilityDetector (wxtsky#139) Both files reimplemented the same Process+Pipe+waitUntilExit pattern with no timeout. Activate() and the visibility checks fire from the main thread (panel buttons, shortcut handler, NotchPanelView body, etc.), so a stuck osascript / tmux / iTerm CLI was free to freeze the UI for as long as the child took to wake up. Replace both runProcess implementations with ProcessRunner.run: - TerminalActivator: 10s cap (matches current osascript-tab-switch worst-case behaviour; activateCmux still dispatches off-main on its own, the timeout is just a backstop). - TerminalVisibilityDetector: 5s cap (these are speculative "is the tab visible?" probes called frequently from view bodies — fail fast). Removes ~30 lines of duplicated boilerplate. No behaviour change on the happy path; on a hang, callers see nil instead of a frozen UI. Refs wxtsky#139. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: dispatch Ghostty activation off the main thread (wxtsky#139) activateGhostty was the last activator that still resolved its subprocess inputs synchronously on the main thread. It calls `tmux display-message` to fetch a session/window key (used to match the right Ghostty terminal in the AppleScript template) and then hands the script off to runOsaScript. Every other activateXxx already runs on a background queue (activateITerm/Warp/WezTerm/Kitty/Tmux/IDE window via runAppleScript or an explicit DispatchQueue.global; cmux has its own dispatch). Move the same pattern here: keep the AppKit gating (NSWorkspace runningApplications + unhide) on the main thread where it has to live, then dispatch the tmux probe, AppleScript construction, and osascript invocation onto a userInitiated queue. With this change no TerminalActivator code path can hold the main thread on a stuck subprocess, which closes the loop on issue wxtsky#139. Refs wxtsky#139. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: register Codex PermissionRequest hook so approvals trigger UI + sound (wxtsky#145) Codex CLI supports a PermissionRequest hook event (per developers.openai.com/codex/hooks) that fires when Codex needs user approval — shell escalation, managed-network access, etc. CodeIsland's Codex CLIConfig only registered SessionStart/End, UserPromptSubmit, PreToolUse, PostToolUse, and Stop, so this event was never installed into Codex's hooks.json. Result: when Codex hit an approval gate, the panel stayed in "running" status forever, no approval card opened, and SoundManager wasn't triggered (handlePermissionRequest is only called from the PermissionRequest event path). Add the event with the same shape as Claude's: 24h timeout, blocking (async=false) so Codex waits for the user's allow/deny decision. Existing users need to hit Settings → Reinstall Hooks once to rewrite ~/.codex/hooks.json with the new entry; new installs pick it up automatically. Refs wxtsky#145. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: setting to disable auto-expand on agent completion (wxtsky#146) Adds Settings → Behavior → "Auto-Expand Panel on Agent Completion" (default On — preserves current behavior). When off, CodeIsland stays in its compact state when agents/subagents finish; status indicators, the queue, and manual interactions are unaffected. Implementation: - New SettingsKey/SettingsDefaults entry registered with default true so existing users keep the current behavior across upgrades. - AppState.enqueueCompletion early-returns when the toggle is off. This is the single chokepoint — both Stop events from the reducer and any other paths into completion display funnel through here, so one guard covers all entry points. - BehaviorToggleRow added next to the existing auto-collapse toggle, reusing the .smartSuppress animation icon (semantically the closest "suppress auto-expand" preview we already have). - Translations added for en / zh / ja / ko / tr. Refs wxtsky#146. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: prefer opencode.jsonc over opencode.json when registering plugin (wxtsky#132) OpenCode's troubleshooting guide recommends opencode.jsonc (with-comments JSON) as the canonical config filename. Before this change, CodeIsland only ever read/wrote ~/.config/opencode/opencode.json, so users on opencode.jsonc would see CodeIsland resurrect a sibling opencode.json on every install / repair pass. Pick the target by precedence at install time: 1. opencode.jsonc — write here when it already exists (OpenCode-recommended) 2. opencode.json — fallback for existing setups, also the default for fresh installs 3. config.json — legacy, still cleaned up after migration uninstall and isOpencodePluginInstalled now scan all three. The existing mergeOpencodePluginRef path already runs through stripJSONComments, so .jsonc input parses correctly without changes. Refs wxtsky#132. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: surface subagent count + agent type tooltip on session card (wxtsky#141) Subagents already rendered as 8px MiniAgentIcons under the mascot but were visually identical and unlabelled — users couldn't tell at a glance which sessions had spawned background work, or what each subagent was actually doing. Two additions, no layout changes: 1. Each MiniAgentIcon now carries a .help(...) tooltip that surfaces the agent type and current tool (e.g. "Explore — Read src/main.swift", or just "general-purpose" when no tool is active). 2. A new SessionTag "+N Sub" (purple) appears in the session header tag row alongside @Remote / INT / YOLO when session.subagents is non-empty — gives the at-a-glance count the issue asked for without touching the column-1 mascot/icon block. Refs wxtsky#141. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: Settings → Plugin Sub-Sessions: separate / merge / hide (wxtsky#123) When a plugin running inside another agent (e.g. omo inside OpenCode) fires its own hook events without an explicit --source, bridge already infers the real source via process ancestry (wxtsky#95). The plugin's events end up with the right source but a different session_id from the main session, so the user sees two same-source pets — confusing for users who think of the plugin as part of the main session. Add a 3-way Settings toggle to control how those events render: - separate (default, current behavior): plugin events keep their own session_id, render as their own card. - merge: rewrite session_id to the matching main session (same source, same _ppid). Plugin events fold into the parent agent's card. - hide: drop plugin events entirely; PermissionRequest gets an auto-allow so plugin tool execution isn't blocked by a UI prompt the user asked to suppress. Implementation: - Bridge: stamp `_via_plugin = true` when sourceTag is nil but inferSource succeeded (this is exactly the "plugin proxy" path). - HookServer.processRequest: pre-filter `_via_plugin` events per the pluginSessionMode setting before HookEvent construction. Merge looks up AppState.findSessionId(forSource:ppid:) using SessionSnapshot.cliPid (which already mirrors bridge's _ppid). Hide returns the right payload for blocking events. - Settings + SettingsView: new pluginSessionMode key (default "separate" preserves existing behavior), Picker added to the Sessions section. - L10n: en / zh / ja / ko / tr translations for title, desc, and the three options. Existing users will need to Reinstall Hooks once so the new bridge binary stamps `_via_plugin`. Without the new bridge, all paths behave exactly as before (separate). Refs wxtsky#123. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: hook event ring buffer + diagnostics export (wxtsky#103) Issue wxtsky#103 (qwen problem 1: prompts not surfacing) and wxtsky#117 (Hermes hooks not flowing) both stalled on the same thing — without runtime visibility into which events HookServer accepted with what source / session_id, all triage was guessing. Add a 100-entry in-memory ring that captures the post-merge view of every dispatched hook event, exported to state/hook-events.json on diagnostics export. Per event we keep just the fields needed to reconstruct routing decisions: timestamp, source, sessionId (truncated), eventName, toolName, and viaPlugin (wxtsky#123). Payloads stay out of the export so bug reports remain a sane size. Mechanism: - AppState gains a nested DiagnosticHookEvent struct, an ObservationIgnored ring buffer, and a recordHookEvent(...) entry. Capped at 100 — older entries trimmed FIFO. - HookServer.processRequest records right after HookEvent construction (so merged session_ids are reflected) and before the routeKind switch. - DiagnosticsExporter adds a state/hook-events.json step alongside the existing sessions.json snapshot. Refs wxtsky#103, wxtsky#117. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: harden UserPromptSubmit prompt extraction + richer hook diagnostics (wxtsky#103) Two changes both aimed at the qwen "prompt not showing" symptom: 1. Reducer prompt extraction - Switch UserPromptSubmit's prompt extraction to firstStringFromEvent so it walks the same key list across both top-level and nested `payload` / `data` containers that the rest of the reducer already supports. - Add `userPrompt` / `text` to the candidate keys. - The helper trims and skips empty/whitespace-only values, so a hook that fires with `"prompt": ""` no longer inserts a blank chat row that pushes the rest of the UI around. 2. Hook ring buffer (wxtsky#103 follow-up) - DiagnosticHookEvent now also captures `payloadKeys` (top-level field names with bridge-injected `_*` filtered out) and `promptPreview` (first 80 chars of any extracted prompt). - HookServer computes both right after HookEvent construction and passes them into recordHookEvent. - DiagnosticsExporter writes them into state/hook-events.json. Together these answer the question that's blocked qwen / hermes / antigravity triage: when a user reports "my prompt isn't showing", exporting diagnostics now tells us at a glance whether (a) the hook didn't fire at all (no entry), (b) it fired without a prompt field (payloadKeys lists everything but `prompt` and promptPreview is null), or (c) it fired with a prompt but the UI still dropped it (promptPreview shows the text and we know to look at the SwiftUI path). Refs wxtsky#103, wxtsky#117. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: distinguish cursor-agent / qodercli from desktop IDE source (wxtsky#134) cursor-agent and qodercli share their hooks file (~/.cursor/hooks.json, ~/.qoder/settings.json) with the matching desktop IDE, so the bridge hook command is identical for both. Until now everything fired through that file landed as `--source cursor` / `--source qoder`, with the session displayed as "Cursor" / "Qoder" — confusing for terminal users who never opened the IDE. Add `cursor-cli` and `qoder-cli` as first-class sources: - SessionSnapshot.supportedSources / normalizedSupportedSource gain the two new keys plus aliases ("cursor-agent", "cursoragent", "cursorcli", "qodercli"). - sourceLabel renders them as "Cursor CLI" / "Qoder CLI". - ESP32Protocol.MascotID.init reuses the existing .cursor / .qoder mascot slots — Buddy firmware only has 16 mascot slots and the visual identity is the same. (No firmware change required.) - CLIProcessResolver.sourceMatchesExecutablePath learns to recognize cursor-agent (`/.local/share/cursor-agent/...` or `/cursor-agent/index.js`) and qodercli (`/qodercli` or `/@qoder-ai/qodercli`). - New `cliVariantOverride(declaredSource:ancestry:)` promotes a `--source cursor` tag to "cursor-cli" when the ancestry has a cursor-agent binary (same for qoder → qoder-cli). The bridge calls it after sourceTag/inferSource so explicit tags also get promoted. - inferSource scans `-cli` variants before plain ones so a path ending in `cursor-agent` doesn't get mis-attributed to `cursor`. Terminal-jump routing uses session.termBundleId / termApp which already points at the user's terminal app, so no TerminalActivator change is needed — clicking a Cursor CLI session jumps to the host terminal, not to Cursor.app. Existing users need to Reinstall Hooks once so the rebuilt bridge ships with cliVariantOverride. Refs wxtsky#134. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: review-driven hardening of session changes Three issues surfaced by independent code review against the just-landed batch (ProcessRunner / HookServer plugin filter / subagent tooltip): 1. ProcessRunner data race (Swift 6 strict-concurrency) `var data` mutated inside a Sendable closure and read on the calling thread compiled cleanly under Swift 5 mode but Swift 6 will flag the captured-var write. Wrap the slot in a private `@unchecked Sendable` reference cell so the synchronization (drained.signal happens-before drained.wait) is explicit. 2. HookServer plugin filter ran JSONSerialization on every hook event The pre-filter for `_via_plugin` was parsing every incoming payload on the main actor — wasteful for the common case where the marker isn't there. Probe the raw bytes for the literal `_via_plugin` string first; only fall through to JSON parsing on a hit. 3. Subagent tooltip dropped `toolDescription` The previous commit's message advertised "Explore — Read src/main.swift" but the actual code only rendered the bare tool name. Fold `sub.toolDescription` into the detail when present and replace the force-unwrap with a flatMap guard while we're in there. Refs wxtsky#103, wxtsky#123, wxtsky#139, wxtsky#141. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: clean up remaining review nits from session Five smaller follow-ups from the same review pass — all flagged minor or nit, but the ask was to clear the list, not pick: 1. cachedClaudeVersion race (NSLock) `detectClaudeVersion` is now reachable from two `Task.detached` call sites (wxtsky#139 install + verifyAndRepair). The static cache field was unguarded. Wrap read+write in an NSLock — writes are idempotent so functional impact was nil, but Swift 6 strict concurrency would flag it and a future refactor could turn the benign race into a real one. 2. firstStringFromDict no longer trims caller-visible value The reducer helper used to return `value.trimmingCharacters(...)` which silently stripped intentional leading / trailing whitespace from code-snippet prompts. Switch the trim to be empty-check-only and return the original value so chat history preserves the user's exact text. 3. Diagnostics export description (en / zh / ja / ko / tr) `state/hook-events.json` includes a 80-char prompt preview. The button description didn't say so. Update all five locales to spell out what the zip contains so users aren't surprised by including prompt text in a bug report. 4. findSessionId 5-min activity window Plugin merge previously matched any session sharing source + cliPid, including sessions whose CLI long since exited and whose PID may have been recycled by macOS for an unrelated process. Require lastActivity within 5 minutes — live sessions update on every event so the window is generous. 5. activateGhostty avoids double dispatch `activateGhostty` body already runs inside `DispatchQueue.global` (wxtsky#139). It then called `runOsaScript` which dispatched a second time. Add a `runOsaScriptSync` variant for callers already on a background queue and use it from activateGhostty. Refs wxtsky#103, wxtsky#123, wxtsky#139. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: empty default autoApproveTools Previous default silently auto-approved 9 internal agent tools (TaskCreate / TaskUpdate / TaskGet / TaskList / TaskOutput / TaskStop / TodoRead / TodoWrite / EnterPlanMode), which hid those calls from the approval panel — users who never opened Settings → Auto-Approve Tools didn't know that hiding was happening. Switch the default to the empty string. Every tool call now flows through the regular approval path; users who want the old behavior can opt-in per tool from the Auto-Approve Tools list. Existing users who'd already toggled the list explicitly are unaffected — UserDefaults register only fills in absent keys. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * perf: extract subagent tooltip builder out of ViewBuilder User reported the hover-triggered island expand animation felt janky after the recent batch landed. The most plausible regression was the subagent ForEach in wxtsky#141 — the review fix in be9231a swelled the inline tooltip computation to five nested `let`s plus an IIFE plus `flatMap` plus `.map`. That kind of expression in a SwiftUI ViewBuilder triggers slow-path type inference and forces the body to re-evaluate the whole closure tree on every hover state flip / animation tick. Move the logic to a file-private `subagentTooltipText(_:)` plain Swift function. The ForEach body is now just two lines: `MiniAgentIcon(...).help(subagentTooltipText(sub))`. Output is identical to the previous version. Refs wxtsky#141. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * release: v1.0.24 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: appcast.xml entry for v1.0.24 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * build: codesign DMG container before notarization build-dmg.sh was creating an unsigned DMG container — the inner .app got Developer ID signed and the dmg got stapled with a notary ticket, but the dmg envelope itself had no signature. \`spctl --assess\` reports "no usable signature" in that state, and Sparkle's update flow can abort with "An error occurred while running the updater" when the helper hand-off picks up the unsigned container. Sign the DMG with the same Developer ID identity (timestamped) before submitting to notarytool. The signature applies before notarization, so the Apple ticket attaches to the signed container. This is a latent issue — both v1.0.23 and v1.0.24 dmgs ship unsigned containers; the symptom was intermittent because most code paths only verify the inner .app + the staple ticket. v1.0.25+ won't have it. Workaround for users hitting the error on v1.0.24: download the dmg manually from the GitHub release page. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: stop blanket-draining pending permissions on activity events (wxtsky#147) handleEvent's wasWaiting branch used to drain the entire permission queue for a session whenever an activity event arrived in waitingApproval/waitingQuestion. The "user replied in terminal" heuristic misfires for parallel MCP / plugin tool calls: e.g. the official Notion plugin fetching 2 pages in parallel — the first PostToolUse arrives while the second tool's PermissionRequest is still pending, the blanket drain denies it, and the user sees "briefly shown then auto-denied". Switch to surgical drain only — resolveToolUseIfCompleted already removes queue entries by tool_use_id when PostToolUse / PostToolUseFailure / PermissionDenied for the same id arrives. Other pending requests are unrelated parallel work and must keep waiting. Question queue still gets drained (questions are rare and don't carry tool_use_id reliably), and session status only resets to idle/processing when nothing is left pending for the session. Also threads a `reason:` argument through drainPermissions / drainQuestions and adds log.notice traces at every auto-deny site (resolveToolUseIfCompleted, mergeDuplicatePermissionRequest, drainPermissions/Questions internal). Surfaced as "⚠️ permission deny reason=… session=… toolUseId=… tool=…" in com.codeisland subsystem so future "card flashed and disappeared" reports can be diagnosed from Console.app without a custom build. Tests: add 2 wxtsky#147 regressions (testStopEventDoesNotDenyPendingPermission, testParallelPostToolUseDoesNotDenyUnrelatedPendingPermission). 241/241 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: collapse Cursor sub-agent processes onto one session card (wxtsky#148) Cursor IDE spawns N parallel sub-agent subprocesses per logical session (file search, code analysis, etc.), and each sub-agent runs its own hook subprocess with a different immediate ppid. Bridge's session_id fallback used `cursor-ppid-<getppid()>`, fanning each sub-agent into a separate session card — users saw 10+ "Cursor — thinking" cards for what was logically one session, drowning out the actual session they were interacting with. Add CLIProcessResolver.resolvedSessionPID — same shape as resolvedTrackedPID but picks the *root-most* same-source binary in the ancestry instead of the nearest. Bridge's session_id fallback now uses it, so all sub-agents spawned by the same root cursor-agent collapse onto a single session_id. To call resolvedSessionPID at fallback time we need ancestry already computed, so the ancestry/effectiveSource block was moved up in main.swift to run before the session_id fallback. Same code, earlier position — `_ppid` / `_via_plugin` / `_source` semantics unchanged. resolvedTrackedPID still uses `first(where:)` (nearest binary) because its job — telling AppState which PID to monitor for liveness — wants the closest CLI process, not the root one. Tests: new CLIProcessResolverTests covering parallel sub-agents collapsing to the same root PID, root-most vs nearest distinction between the two resolvers, and fallbacks (no match, empty ancestry, nil source, non-positive immediate ppid). 247/247 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: honor user default mascot whenever no session is actively working (wxtsky#149) wxtsky#102 fixed the empty-session case (no sessions → use default mascot), but `refreshDerivedState` and `CompactLeftWing.displaySource` still echoed `deriveSessionSummary`'s `mostRecentIdleSource` whenever any session existed — even all-idle ones. Result: a user who picked Codex as the default mascot still saw Claude every time their last session went idle, because the most recently active session's source kept "sticking" to the notch. Switch the trigger from `totalSessionCount == 0` to `summary.status == .idle`, covering both empty-state (wxtsky#102) and all-idle (wxtsky#149) under one rule. Active work (running / processing / waitingApproval / waitingQuestion) still wins — we don't want to mute the source of something actively happening just because the user has a preferred idle mascot. NotchPanelView's `CompactLeftWing.displaySource` mirrors the same logic so the compact wing renders the user default in idle even when the picked-out displaySession happens to be an idle one. Tests: 4 new regressions in AppStatePrimarySourceTests (idle-honors-default, active-overrides-default, empty-honors-default, mixed-active-and-idle-uses-active). 251/251 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Arda Kılıçdağı <ardakilicdagi@gmail.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: jerry0804 <jerryzhao0804@gmail.com> Co-authored-by: JZ <jz@JZdeMacBook-Pro.local> Co-authored-by: Drswith <49299002+Drswith@users.noreply.github.com> Co-authored-by: K0ala <liushaoxiong10@outlook.com> Co-authored-by: wxtsky <1970550145@qq.com>
Uncle-Peke
added a commit
to Uncle-Peke/CodeIsland
that referenced
this pull request
May 4, 2026
* chore: polish Turkish translation for stylistic consistency (wxtsky#135) Align 4 post-PR entries with the established style (Title Case for setting labels, ALL CAPS for action buttons, correct semantics): - auto_collapse_after_session_jump: lowercase → Title Case - dismiss: ERTELE (postpone) → YOKSAY (ignore/dismiss) - buddy_reconnect: sentence case → Title Case - buddy_enable_bluetooth: sentence case → Title Case Key parity unchanged (259/259 keys covered). Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: prevent Sparkle crash in DEBUG mode when running without bundle ID (wxtsky#133) Co-authored-by: JZ <jz@JZdeMacBook-Pro.local> * fix: avoid stale update abort errors (wxtsky#138) * feat: improve Buddy watch approval previews and alerts (wxtsky#144) Show the full Bash approval command alongside the human-readable description so pending approvals stay understandable on macOS and on the watch. Preserve pending TraeCLI approvals until the user responds, add watch-side approve/deny and skip controls over BLE, and trigger vibration plus richer attention notifications when approval is required. * test: align AppStateQuestionFlow assertions with wxtsky#144 toolDescription change wxtsky#144 changed `HookEvent.toolDescription` for Bash to combine description and command (so ESP32/watch previews show the full command). Update the existing two assertions to match the new format. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: add ProcessRunner with timeout to harden detectClaudeVersion (wxtsky#139) Replace the unbounded waitUntilExit() in detectClaudeVersion with a shared ProcessRunner that enforces a hard deadline (SIGTERM, then SIGKILL after 1s grace) and drains stdout off the wait path so a full pipe buffer cannot wedge the child. detectClaudeVersion now uses a 5s timeout — a stuck `claude --version` used to freeze app launch silently. Refs wxtsky#139. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: move launch-time hook installation off the main thread (wxtsky#139) ConfigInstaller.install() does subprocess version detection plus disk I/O across multiple CLI configs. Running it inline from applicationDidFinishLaunching meant a slow CLI binary or network home dir froze the menu bar / panel until install finished — sometimes silently, because macOS doesn't pop a "Not Responding" dialog for status items. Detach it onto a userInitiated Task so app UI comes up immediately while hooks install in the background. HookServer is still started synchronously before the detach so the socket is listening by the time settings.json is rewritten (preserves the ordering noted in the existing comment). Mark AppDelegate.log nonisolated so the detached task can use it without violating Swift 6 actor isolation (Logger is Sendable). Refs wxtsky#139. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: run periodic hook verifyAndRepair off the main thread (wxtsky#139) checkAndRepairHooks fires from a 300s timer AND every NSWorkspace didActivateApplicationNotification (i.e. each time the user app-switches). verifyAndRepair walks every enabled CLI and rewrites settings.json — on a network home dir or a slow disk that's enough to stutter the panel. Keep the 60s debounce on the main actor (cheap), but do the actual repair on a background task. Repair results only feed a log line so no UI thread hand-off is needed. Refs wxtsky#139. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: cap subprocess waits in TerminalActivator/VisibilityDetector (wxtsky#139) Both files reimplemented the same Process+Pipe+waitUntilExit pattern with no timeout. Activate() and the visibility checks fire from the main thread (panel buttons, shortcut handler, NotchPanelView body, etc.), so a stuck osascript / tmux / iTerm CLI was free to freeze the UI for as long as the child took to wake up. Replace both runProcess implementations with ProcessRunner.run: - TerminalActivator: 10s cap (matches current osascript-tab-switch worst-case behaviour; activateCmux still dispatches off-main on its own, the timeout is just a backstop). - TerminalVisibilityDetector: 5s cap (these are speculative "is the tab visible?" probes called frequently from view bodies — fail fast). Removes ~30 lines of duplicated boilerplate. No behaviour change on the happy path; on a hang, callers see nil instead of a frozen UI. Refs wxtsky#139. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: dispatch Ghostty activation off the main thread (wxtsky#139) activateGhostty was the last activator that still resolved its subprocess inputs synchronously on the main thread. It calls `tmux display-message` to fetch a session/window key (used to match the right Ghostty terminal in the AppleScript template) and then hands the script off to runOsaScript. Every other activateXxx already runs on a background queue (activateITerm/Warp/WezTerm/Kitty/Tmux/IDE window via runAppleScript or an explicit DispatchQueue.global; cmux has its own dispatch). Move the same pattern here: keep the AppKit gating (NSWorkspace runningApplications + unhide) on the main thread where it has to live, then dispatch the tmux probe, AppleScript construction, and osascript invocation onto a userInitiated queue. With this change no TerminalActivator code path can hold the main thread on a stuck subprocess, which closes the loop on issue wxtsky#139. Refs wxtsky#139. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: register Codex PermissionRequest hook so approvals trigger UI + sound (wxtsky#145) Codex CLI supports a PermissionRequest hook event (per developers.openai.com/codex/hooks) that fires when Codex needs user approval — shell escalation, managed-network access, etc. CodeIsland's Codex CLIConfig only registered SessionStart/End, UserPromptSubmit, PreToolUse, PostToolUse, and Stop, so this event was never installed into Codex's hooks.json. Result: when Codex hit an approval gate, the panel stayed in "running" status forever, no approval card opened, and SoundManager wasn't triggered (handlePermissionRequest is only called from the PermissionRequest event path). Add the event with the same shape as Claude's: 24h timeout, blocking (async=false) so Codex waits for the user's allow/deny decision. Existing users need to hit Settings → Reinstall Hooks once to rewrite ~/.codex/hooks.json with the new entry; new installs pick it up automatically. Refs wxtsky#145. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: setting to disable auto-expand on agent completion (wxtsky#146) Adds Settings → Behavior → "Auto-Expand Panel on Agent Completion" (default On — preserves current behavior). When off, CodeIsland stays in its compact state when agents/subagents finish; status indicators, the queue, and manual interactions are unaffected. Implementation: - New SettingsKey/SettingsDefaults entry registered with default true so existing users keep the current behavior across upgrades. - AppState.enqueueCompletion early-returns when the toggle is off. This is the single chokepoint — both Stop events from the reducer and any other paths into completion display funnel through here, so one guard covers all entry points. - BehaviorToggleRow added next to the existing auto-collapse toggle, reusing the .smartSuppress animation icon (semantically the closest "suppress auto-expand" preview we already have). - Translations added for en / zh / ja / ko / tr. Refs wxtsky#146. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: prefer opencode.jsonc over opencode.json when registering plugin (wxtsky#132) OpenCode's troubleshooting guide recommends opencode.jsonc (with-comments JSON) as the canonical config filename. Before this change, CodeIsland only ever read/wrote ~/.config/opencode/opencode.json, so users on opencode.jsonc would see CodeIsland resurrect a sibling opencode.json on every install / repair pass. Pick the target by precedence at install time: 1. opencode.jsonc — write here when it already exists (OpenCode-recommended) 2. opencode.json — fallback for existing setups, also the default for fresh installs 3. config.json — legacy, still cleaned up after migration uninstall and isOpencodePluginInstalled now scan all three. The existing mergeOpencodePluginRef path already runs through stripJSONComments, so .jsonc input parses correctly without changes. Refs wxtsky#132. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: surface subagent count + agent type tooltip on session card (wxtsky#141) Subagents already rendered as 8px MiniAgentIcons under the mascot but were visually identical and unlabelled — users couldn't tell at a glance which sessions had spawned background work, or what each subagent was actually doing. Two additions, no layout changes: 1. Each MiniAgentIcon now carries a .help(...) tooltip that surfaces the agent type and current tool (e.g. "Explore — Read src/main.swift", or just "general-purpose" when no tool is active). 2. A new SessionTag "+N Sub" (purple) appears in the session header tag row alongside @Remote / INT / YOLO when session.subagents is non-empty — gives the at-a-glance count the issue asked for without touching the column-1 mascot/icon block. Refs wxtsky#141. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: Settings → Plugin Sub-Sessions: separate / merge / hide (wxtsky#123) When a plugin running inside another agent (e.g. omo inside OpenCode) fires its own hook events without an explicit --source, bridge already infers the real source via process ancestry (wxtsky#95). The plugin's events end up with the right source but a different session_id from the main session, so the user sees two same-source pets — confusing for users who think of the plugin as part of the main session. Add a 3-way Settings toggle to control how those events render: - separate (default, current behavior): plugin events keep their own session_id, render as their own card. - merge: rewrite session_id to the matching main session (same source, same _ppid). Plugin events fold into the parent agent's card. - hide: drop plugin events entirely; PermissionRequest gets an auto-allow so plugin tool execution isn't blocked by a UI prompt the user asked to suppress. Implementation: - Bridge: stamp `_via_plugin = true` when sourceTag is nil but inferSource succeeded (this is exactly the "plugin proxy" path). - HookServer.processRequest: pre-filter `_via_plugin` events per the pluginSessionMode setting before HookEvent construction. Merge looks up AppState.findSessionId(forSource:ppid:) using SessionSnapshot.cliPid (which already mirrors bridge's _ppid). Hide returns the right payload for blocking events. - Settings + SettingsView: new pluginSessionMode key (default "separate" preserves existing behavior), Picker added to the Sessions section. - L10n: en / zh / ja / ko / tr translations for title, desc, and the three options. Existing users will need to Reinstall Hooks once so the new bridge binary stamps `_via_plugin`. Without the new bridge, all paths behave exactly as before (separate). Refs wxtsky#123. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: hook event ring buffer + diagnostics export (wxtsky#103) Issue wxtsky#103 (qwen problem 1: prompts not surfacing) and wxtsky#117 (Hermes hooks not flowing) both stalled on the same thing — without runtime visibility into which events HookServer accepted with what source / session_id, all triage was guessing. Add a 100-entry in-memory ring that captures the post-merge view of every dispatched hook event, exported to state/hook-events.json on diagnostics export. Per event we keep just the fields needed to reconstruct routing decisions: timestamp, source, sessionId (truncated), eventName, toolName, and viaPlugin (wxtsky#123). Payloads stay out of the export so bug reports remain a sane size. Mechanism: - AppState gains a nested DiagnosticHookEvent struct, an ObservationIgnored ring buffer, and a recordHookEvent(...) entry. Capped at 100 — older entries trimmed FIFO. - HookServer.processRequest records right after HookEvent construction (so merged session_ids are reflected) and before the routeKind switch. - DiagnosticsExporter adds a state/hook-events.json step alongside the existing sessions.json snapshot. Refs wxtsky#103, wxtsky#117. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: harden UserPromptSubmit prompt extraction + richer hook diagnostics (wxtsky#103) Two changes both aimed at the qwen "prompt not showing" symptom: 1. Reducer prompt extraction - Switch UserPromptSubmit's prompt extraction to firstStringFromEvent so it walks the same key list across both top-level and nested `payload` / `data` containers that the rest of the reducer already supports. - Add `userPrompt` / `text` to the candidate keys. - The helper trims and skips empty/whitespace-only values, so a hook that fires with `"prompt": ""` no longer inserts a blank chat row that pushes the rest of the UI around. 2. Hook ring buffer (wxtsky#103 follow-up) - DiagnosticHookEvent now also captures `payloadKeys` (top-level field names with bridge-injected `_*` filtered out) and `promptPreview` (first 80 chars of any extracted prompt). - HookServer computes both right after HookEvent construction and passes them into recordHookEvent. - DiagnosticsExporter writes them into state/hook-events.json. Together these answer the question that's blocked qwen / hermes / antigravity triage: when a user reports "my prompt isn't showing", exporting diagnostics now tells us at a glance whether (a) the hook didn't fire at all (no entry), (b) it fired without a prompt field (payloadKeys lists everything but `prompt` and promptPreview is null), or (c) it fired with a prompt but the UI still dropped it (promptPreview shows the text and we know to look at the SwiftUI path). Refs wxtsky#103, wxtsky#117. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: distinguish cursor-agent / qodercli from desktop IDE source (wxtsky#134) cursor-agent and qodercli share their hooks file (~/.cursor/hooks.json, ~/.qoder/settings.json) with the matching desktop IDE, so the bridge hook command is identical for both. Until now everything fired through that file landed as `--source cursor` / `--source qoder`, with the session displayed as "Cursor" / "Qoder" — confusing for terminal users who never opened the IDE. Add `cursor-cli` and `qoder-cli` as first-class sources: - SessionSnapshot.supportedSources / normalizedSupportedSource gain the two new keys plus aliases ("cursor-agent", "cursoragent", "cursorcli", "qodercli"). - sourceLabel renders them as "Cursor CLI" / "Qoder CLI". - ESP32Protocol.MascotID.init reuses the existing .cursor / .qoder mascot slots — Buddy firmware only has 16 mascot slots and the visual identity is the same. (No firmware change required.) - CLIProcessResolver.sourceMatchesExecutablePath learns to recognize cursor-agent (`/.local/share/cursor-agent/...` or `/cursor-agent/index.js`) and qodercli (`/qodercli` or `/@qoder-ai/qodercli`). - New `cliVariantOverride(declaredSource:ancestry:)` promotes a `--source cursor` tag to "cursor-cli" when the ancestry has a cursor-agent binary (same for qoder → qoder-cli). The bridge calls it after sourceTag/inferSource so explicit tags also get promoted. - inferSource scans `-cli` variants before plain ones so a path ending in `cursor-agent` doesn't get mis-attributed to `cursor`. Terminal-jump routing uses session.termBundleId / termApp which already points at the user's terminal app, so no TerminalActivator change is needed — clicking a Cursor CLI session jumps to the host terminal, not to Cursor.app. Existing users need to Reinstall Hooks once so the rebuilt bridge ships with cliVariantOverride. Refs wxtsky#134. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: review-driven hardening of session changes Three issues surfaced by independent code review against the just-landed batch (ProcessRunner / HookServer plugin filter / subagent tooltip): 1. ProcessRunner data race (Swift 6 strict-concurrency) `var data` mutated inside a Sendable closure and read on the calling thread compiled cleanly under Swift 5 mode but Swift 6 will flag the captured-var write. Wrap the slot in a private `@unchecked Sendable` reference cell so the synchronization (drained.signal happens-before drained.wait) is explicit. 2. HookServer plugin filter ran JSONSerialization on every hook event The pre-filter for `_via_plugin` was parsing every incoming payload on the main actor — wasteful for the common case where the marker isn't there. Probe the raw bytes for the literal `_via_plugin` string first; only fall through to JSON parsing on a hit. 3. Subagent tooltip dropped `toolDescription` The previous commit's message advertised "Explore — Read src/main.swift" but the actual code only rendered the bare tool name. Fold `sub.toolDescription` into the detail when present and replace the force-unwrap with a flatMap guard while we're in there. Refs wxtsky#103, wxtsky#123, wxtsky#139, wxtsky#141. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: clean up remaining review nits from session Five smaller follow-ups from the same review pass — all flagged minor or nit, but the ask was to clear the list, not pick: 1. cachedClaudeVersion race (NSLock) `detectClaudeVersion` is now reachable from two `Task.detached` call sites (wxtsky#139 install + verifyAndRepair). The static cache field was unguarded. Wrap read+write in an NSLock — writes are idempotent so functional impact was nil, but Swift 6 strict concurrency would flag it and a future refactor could turn the benign race into a real one. 2. firstStringFromDict no longer trims caller-visible value The reducer helper used to return `value.trimmingCharacters(...)` which silently stripped intentional leading / trailing whitespace from code-snippet prompts. Switch the trim to be empty-check-only and return the original value so chat history preserves the user's exact text. 3. Diagnostics export description (en / zh / ja / ko / tr) `state/hook-events.json` includes a 80-char prompt preview. The button description didn't say so. Update all five locales to spell out what the zip contains so users aren't surprised by including prompt text in a bug report. 4. findSessionId 5-min activity window Plugin merge previously matched any session sharing source + cliPid, including sessions whose CLI long since exited and whose PID may have been recycled by macOS for an unrelated process. Require lastActivity within 5 minutes — live sessions update on every event so the window is generous. 5. activateGhostty avoids double dispatch `activateGhostty` body already runs inside `DispatchQueue.global` (wxtsky#139). It then called `runOsaScript` which dispatched a second time. Add a `runOsaScriptSync` variant for callers already on a background queue and use it from activateGhostty. Refs wxtsky#103, wxtsky#123, wxtsky#139. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: empty default autoApproveTools Previous default silently auto-approved 9 internal agent tools (TaskCreate / TaskUpdate / TaskGet / TaskList / TaskOutput / TaskStop / TodoRead / TodoWrite / EnterPlanMode), which hid those calls from the approval panel — users who never opened Settings → Auto-Approve Tools didn't know that hiding was happening. Switch the default to the empty string. Every tool call now flows through the regular approval path; users who want the old behavior can opt-in per tool from the Auto-Approve Tools list. Existing users who'd already toggled the list explicitly are unaffected — UserDefaults register only fills in absent keys. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * perf: extract subagent tooltip builder out of ViewBuilder User reported the hover-triggered island expand animation felt janky after the recent batch landed. The most plausible regression was the subagent ForEach in wxtsky#141 — the review fix in be9231a swelled the inline tooltip computation to five nested `let`s plus an IIFE plus `flatMap` plus `.map`. That kind of expression in a SwiftUI ViewBuilder triggers slow-path type inference and forces the body to re-evaluate the whole closure tree on every hover state flip / animation tick. Move the logic to a file-private `subagentTooltipText(_:)` plain Swift function. The ForEach body is now just two lines: `MiniAgentIcon(...).help(subagentTooltipText(sub))`. Output is identical to the previous version. Refs wxtsky#141. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * release: v1.0.24 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: appcast.xml entry for v1.0.24 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * build: codesign DMG container before notarization build-dmg.sh was creating an unsigned DMG container — the inner .app got Developer ID signed and the dmg got stapled with a notary ticket, but the dmg envelope itself had no signature. \`spctl --assess\` reports "no usable signature" in that state, and Sparkle's update flow can abort with "An error occurred while running the updater" when the helper hand-off picks up the unsigned container. Sign the DMG with the same Developer ID identity (timestamped) before submitting to notarytool. The signature applies before notarization, so the Apple ticket attaches to the signed container. This is a latent issue — both v1.0.23 and v1.0.24 dmgs ship unsigned containers; the symptom was intermittent because most code paths only verify the inner .app + the staple ticket. v1.0.25+ won't have it. Workaround for users hitting the error on v1.0.24: download the dmg manually from the GitHub release page. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: stop blanket-draining pending permissions on activity events (wxtsky#147) handleEvent's wasWaiting branch used to drain the entire permission queue for a session whenever an activity event arrived in waitingApproval/waitingQuestion. The "user replied in terminal" heuristic misfires for parallel MCP / plugin tool calls: e.g. the official Notion plugin fetching 2 pages in parallel — the first PostToolUse arrives while the second tool's PermissionRequest is still pending, the blanket drain denies it, and the user sees "briefly shown then auto-denied". Switch to surgical drain only — resolveToolUseIfCompleted already removes queue entries by tool_use_id when PostToolUse / PostToolUseFailure / PermissionDenied for the same id arrives. Other pending requests are unrelated parallel work and must keep waiting. Question queue still gets drained (questions are rare and don't carry tool_use_id reliably), and session status only resets to idle/processing when nothing is left pending for the session. Also threads a `reason:` argument through drainPermissions / drainQuestions and adds log.notice traces at every auto-deny site (resolveToolUseIfCompleted, mergeDuplicatePermissionRequest, drainPermissions/Questions internal). Surfaced as "⚠️ permission deny reason=… session=… toolUseId=… tool=…" in com.codeisland subsystem so future "card flashed and disappeared" reports can be diagnosed from Console.app without a custom build. Tests: add 2 wxtsky#147 regressions (testStopEventDoesNotDenyPendingPermission, testParallelPostToolUseDoesNotDenyUnrelatedPendingPermission). 241/241 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: collapse Cursor sub-agent processes onto one session card (wxtsky#148) Cursor IDE spawns N parallel sub-agent subprocesses per logical session (file search, code analysis, etc.), and each sub-agent runs its own hook subprocess with a different immediate ppid. Bridge's session_id fallback used `cursor-ppid-<getppid()>`, fanning each sub-agent into a separate session card — users saw 10+ "Cursor — thinking" cards for what was logically one session, drowning out the actual session they were interacting with. Add CLIProcessResolver.resolvedSessionPID — same shape as resolvedTrackedPID but picks the *root-most* same-source binary in the ancestry instead of the nearest. Bridge's session_id fallback now uses it, so all sub-agents spawned by the same root cursor-agent collapse onto a single session_id. To call resolvedSessionPID at fallback time we need ancestry already computed, so the ancestry/effectiveSource block was moved up in main.swift to run before the session_id fallback. Same code, earlier position — `_ppid` / `_via_plugin` / `_source` semantics unchanged. resolvedTrackedPID still uses `first(where:)` (nearest binary) because its job — telling AppState which PID to monitor for liveness — wants the closest CLI process, not the root one. Tests: new CLIProcessResolverTests covering parallel sub-agents collapsing to the same root PID, root-most vs nearest distinction between the two resolvers, and fallbacks (no match, empty ancestry, nil source, non-positive immediate ppid). 247/247 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: honor user default mascot whenever no session is actively working (wxtsky#149) wxtsky#102 fixed the empty-session case (no sessions → use default mascot), but `refreshDerivedState` and `CompactLeftWing.displaySource` still echoed `deriveSessionSummary`'s `mostRecentIdleSource` whenever any session existed — even all-idle ones. Result: a user who picked Codex as the default mascot still saw Claude every time their last session went idle, because the most recently active session's source kept "sticking" to the notch. Switch the trigger from `totalSessionCount == 0` to `summary.status == .idle`, covering both empty-state (wxtsky#102) and all-idle (wxtsky#149) under one rule. Active work (running / processing / waitingApproval / waitingQuestion) still wins — we don't want to mute the source of something actively happening just because the user has a preferred idle mascot. NotchPanelView's `CompactLeftWing.displaySource` mirrors the same logic so the compact wing renders the user default in idle even when the picked-out displaySession happens to be an idle one. Tests: 4 new regressions in AppStatePrimarySourceTests (idle-honors-default, active-overrides-default, empty-honors-default, mixed-active-and-idle-uses-active). 251/251 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Arda Kılıçdağı <ardakilicdagi@gmail.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: jerry0804 <jerryzhao0804@gmail.com> Co-authored-by: JZ <jz@JZdeMacBook-Pro.local> Co-authored-by: Drswith <49299002+Drswith@users.noreply.github.com> Co-authored-by: K0ala <liushaoxiong10@outlook.com> Co-authored-by: wxtsky <1970550145@qq.com>
Uncle-Peke
added a commit
to Uncle-Peke/CodeIsland
that referenced
this pull request
May 15, 2026
* chore: polish Turkish translation for stylistic consistency (wxtsky#135) Align 4 post-PR entries with the established style (Title Case for setting labels, ALL CAPS for action buttons, correct semantics): - auto_collapse_after_session_jump: lowercase → Title Case - dismiss: ERTELE (postpone) → YOKSAY (ignore/dismiss) - buddy_reconnect: sentence case → Title Case - buddy_enable_bluetooth: sentence case → Title Case Key parity unchanged (259/259 keys covered). Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: prevent Sparkle crash in DEBUG mode when running without bundle ID (wxtsky#133) Co-authored-by: JZ <jz@JZdeMacBook-Pro.local> * fix: avoid stale update abort errors (wxtsky#138) * feat: improve Buddy watch approval previews and alerts (wxtsky#144) Show the full Bash approval command alongside the human-readable description so pending approvals stay understandable on macOS and on the watch. Preserve pending TraeCLI approvals until the user responds, add watch-side approve/deny and skip controls over BLE, and trigger vibration plus richer attention notifications when approval is required. * test: align AppStateQuestionFlow assertions with wxtsky#144 toolDescription change wxtsky#144 changed `HookEvent.toolDescription` for Bash to combine description and command (so ESP32/watch previews show the full command). Update the existing two assertions to match the new format. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: add ProcessRunner with timeout to harden detectClaudeVersion (wxtsky#139) Replace the unbounded waitUntilExit() in detectClaudeVersion with a shared ProcessRunner that enforces a hard deadline (SIGTERM, then SIGKILL after 1s grace) and drains stdout off the wait path so a full pipe buffer cannot wedge the child. detectClaudeVersion now uses a 5s timeout — a stuck `claude --version` used to freeze app launch silently. Refs wxtsky#139. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: move launch-time hook installation off the main thread (wxtsky#139) ConfigInstaller.install() does subprocess version detection plus disk I/O across multiple CLI configs. Running it inline from applicationDidFinishLaunching meant a slow CLI binary or network home dir froze the menu bar / panel until install finished — sometimes silently, because macOS doesn't pop a "Not Responding" dialog for status items. Detach it onto a userInitiated Task so app UI comes up immediately while hooks install in the background. HookServer is still started synchronously before the detach so the socket is listening by the time settings.json is rewritten (preserves the ordering noted in the existing comment). Mark AppDelegate.log nonisolated so the detached task can use it without violating Swift 6 actor isolation (Logger is Sendable). Refs wxtsky#139. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: run periodic hook verifyAndRepair off the main thread (wxtsky#139) checkAndRepairHooks fires from a 300s timer AND every NSWorkspace didActivateApplicationNotification (i.e. each time the user app-switches). verifyAndRepair walks every enabled CLI and rewrites settings.json — on a network home dir or a slow disk that's enough to stutter the panel. Keep the 60s debounce on the main actor (cheap), but do the actual repair on a background task. Repair results only feed a log line so no UI thread hand-off is needed. Refs wxtsky#139. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: cap subprocess waits in TerminalActivator/VisibilityDetector (wxtsky#139) Both files reimplemented the same Process+Pipe+waitUntilExit pattern with no timeout. Activate() and the visibility checks fire from the main thread (panel buttons, shortcut handler, NotchPanelView body, etc.), so a stuck osascript / tmux / iTerm CLI was free to freeze the UI for as long as the child took to wake up. Replace both runProcess implementations with ProcessRunner.run: - TerminalActivator: 10s cap (matches current osascript-tab-switch worst-case behaviour; activateCmux still dispatches off-main on its own, the timeout is just a backstop). - TerminalVisibilityDetector: 5s cap (these are speculative "is the tab visible?" probes called frequently from view bodies — fail fast). Removes ~30 lines of duplicated boilerplate. No behaviour change on the happy path; on a hang, callers see nil instead of a frozen UI. Refs wxtsky#139. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: dispatch Ghostty activation off the main thread (wxtsky#139) activateGhostty was the last activator that still resolved its subprocess inputs synchronously on the main thread. It calls `tmux display-message` to fetch a session/window key (used to match the right Ghostty terminal in the AppleScript template) and then hands the script off to runOsaScript. Every other activateXxx already runs on a background queue (activateITerm/Warp/WezTerm/Kitty/Tmux/IDE window via runAppleScript or an explicit DispatchQueue.global; cmux has its own dispatch). Move the same pattern here: keep the AppKit gating (NSWorkspace runningApplications + unhide) on the main thread where it has to live, then dispatch the tmux probe, AppleScript construction, and osascript invocation onto a userInitiated queue. With this change no TerminalActivator code path can hold the main thread on a stuck subprocess, which closes the loop on issue wxtsky#139. Refs wxtsky#139. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: register Codex PermissionRequest hook so approvals trigger UI + sound (wxtsky#145) Codex CLI supports a PermissionRequest hook event (per developers.openai.com/codex/hooks) that fires when Codex needs user approval — shell escalation, managed-network access, etc. CodeIsland's Codex CLIConfig only registered SessionStart/End, UserPromptSubmit, PreToolUse, PostToolUse, and Stop, so this event was never installed into Codex's hooks.json. Result: when Codex hit an approval gate, the panel stayed in "running" status forever, no approval card opened, and SoundManager wasn't triggered (handlePermissionRequest is only called from the PermissionRequest event path). Add the event with the same shape as Claude's: 24h timeout, blocking (async=false) so Codex waits for the user's allow/deny decision. Existing users need to hit Settings → Reinstall Hooks once to rewrite ~/.codex/hooks.json with the new entry; new installs pick it up automatically. Refs wxtsky#145. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: setting to disable auto-expand on agent completion (wxtsky#146) Adds Settings → Behavior → "Auto-Expand Panel on Agent Completion" (default On — preserves current behavior). When off, CodeIsland stays in its compact state when agents/subagents finish; status indicators, the queue, and manual interactions are unaffected. Implementation: - New SettingsKey/SettingsDefaults entry registered with default true so existing users keep the current behavior across upgrades. - AppState.enqueueCompletion early-returns when the toggle is off. This is the single chokepoint — both Stop events from the reducer and any other paths into completion display funnel through here, so one guard covers all entry points. - BehaviorToggleRow added next to the existing auto-collapse toggle, reusing the .smartSuppress animation icon (semantically the closest "suppress auto-expand" preview we already have). - Translations added for en / zh / ja / ko / tr. Refs wxtsky#146. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: prefer opencode.jsonc over opencode.json when registering plugin (wxtsky#132) OpenCode's troubleshooting guide recommends opencode.jsonc (with-comments JSON) as the canonical config filename. Before this change, CodeIsland only ever read/wrote ~/.config/opencode/opencode.json, so users on opencode.jsonc would see CodeIsland resurrect a sibling opencode.json on every install / repair pass. Pick the target by precedence at install time: 1. opencode.jsonc — write here when it already exists (OpenCode-recommended) 2. opencode.json — fallback for existing setups, also the default for fresh installs 3. config.json — legacy, still cleaned up after migration uninstall and isOpencodePluginInstalled now scan all three. The existing mergeOpencodePluginRef path already runs through stripJSONComments, so .jsonc input parses correctly without changes. Refs wxtsky#132. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: surface subagent count + agent type tooltip on session card (wxtsky#141) Subagents already rendered as 8px MiniAgentIcons under the mascot but were visually identical and unlabelled — users couldn't tell at a glance which sessions had spawned background work, or what each subagent was actually doing. Two additions, no layout changes: 1. Each MiniAgentIcon now carries a .help(...) tooltip that surfaces the agent type and current tool (e.g. "Explore — Read src/main.swift", or just "general-purpose" when no tool is active). 2. A new SessionTag "+N Sub" (purple) appears in the session header tag row alongside @Remote / INT / YOLO when session.subagents is non-empty — gives the at-a-glance count the issue asked for without touching the column-1 mascot/icon block. Refs wxtsky#141. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: Settings → Plugin Sub-Sessions: separate / merge / hide (wxtsky#123) When a plugin running inside another agent (e.g. omo inside OpenCode) fires its own hook events without an explicit --source, bridge already infers the real source via process ancestry (wxtsky#95). The plugin's events end up with the right source but a different session_id from the main session, so the user sees two same-source pets — confusing for users who think of the plugin as part of the main session. Add a 3-way Settings toggle to control how those events render: - separate (default, current behavior): plugin events keep their own session_id, render as their own card. - merge: rewrite session_id to the matching main session (same source, same _ppid). Plugin events fold into the parent agent's card. - hide: drop plugin events entirely; PermissionRequest gets an auto-allow so plugin tool execution isn't blocked by a UI prompt the user asked to suppress. Implementation: - Bridge: stamp `_via_plugin = true` when sourceTag is nil but inferSource succeeded (this is exactly the "plugin proxy" path). - HookServer.processRequest: pre-filter `_via_plugin` events per the pluginSessionMode setting before HookEvent construction. Merge looks up AppState.findSessionId(forSource:ppid:) using SessionSnapshot.cliPid (which already mirrors bridge's _ppid). Hide returns the right payload for blocking events. - Settings + SettingsView: new pluginSessionMode key (default "separate" preserves existing behavior), Picker added to the Sessions section. - L10n: en / zh / ja / ko / tr translations for title, desc, and the three options. Existing users will need to Reinstall Hooks once so the new bridge binary stamps `_via_plugin`. Without the new bridge, all paths behave exactly as before (separate). Refs wxtsky#123. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: hook event ring buffer + diagnostics export (wxtsky#103) Issue wxtsky#103 (qwen problem 1: prompts not surfacing) and wxtsky#117 (Hermes hooks not flowing) both stalled on the same thing — without runtime visibility into which events HookServer accepted with what source / session_id, all triage was guessing. Add a 100-entry in-memory ring that captures the post-merge view of every dispatched hook event, exported to state/hook-events.json on diagnostics export. Per event we keep just the fields needed to reconstruct routing decisions: timestamp, source, sessionId (truncated), eventName, toolName, and viaPlugin (wxtsky#123). Payloads stay out of the export so bug reports remain a sane size. Mechanism: - AppState gains a nested DiagnosticHookEvent struct, an ObservationIgnored ring buffer, and a recordHookEvent(...) entry. Capped at 100 — older entries trimmed FIFO. - HookServer.processRequest records right after HookEvent construction (so merged session_ids are reflected) and before the routeKind switch. - DiagnosticsExporter adds a state/hook-events.json step alongside the existing sessions.json snapshot. Refs wxtsky#103, wxtsky#117. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: harden UserPromptSubmit prompt extraction + richer hook diagnostics (wxtsky#103) Two changes both aimed at the qwen "prompt not showing" symptom: 1. Reducer prompt extraction - Switch UserPromptSubmit's prompt extraction to firstStringFromEvent so it walks the same key list across both top-level and nested `payload` / `data` containers that the rest of the reducer already supports. - Add `userPrompt` / `text` to the candidate keys. - The helper trims and skips empty/whitespace-only values, so a hook that fires with `"prompt": ""` no longer inserts a blank chat row that pushes the rest of the UI around. 2. Hook ring buffer (wxtsky#103 follow-up) - DiagnosticHookEvent now also captures `payloadKeys` (top-level field names with bridge-injected `_*` filtered out) and `promptPreview` (first 80 chars of any extracted prompt). - HookServer computes both right after HookEvent construction and passes them into recordHookEvent. - DiagnosticsExporter writes them into state/hook-events.json. Together these answer the question that's blocked qwen / hermes / antigravity triage: when a user reports "my prompt isn't showing", exporting diagnostics now tells us at a glance whether (a) the hook didn't fire at all (no entry), (b) it fired without a prompt field (payloadKeys lists everything but `prompt` and promptPreview is null), or (c) it fired with a prompt but the UI still dropped it (promptPreview shows the text and we know to look at the SwiftUI path). Refs wxtsky#103, wxtsky#117. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: distinguish cursor-agent / qodercli from desktop IDE source (wxtsky#134) cursor-agent and qodercli share their hooks file (~/.cursor/hooks.json, ~/.qoder/settings.json) with the matching desktop IDE, so the bridge hook command is identical for both. Until now everything fired through that file landed as `--source cursor` / `--source qoder`, with the session displayed as "Cursor" / "Qoder" — confusing for terminal users who never opened the IDE. Add `cursor-cli` and `qoder-cli` as first-class sources: - SessionSnapshot.supportedSources / normalizedSupportedSource gain the two new keys plus aliases ("cursor-agent", "cursoragent", "cursorcli", "qodercli"). - sourceLabel renders them as "Cursor CLI" / "Qoder CLI". - ESP32Protocol.MascotID.init reuses the existing .cursor / .qoder mascot slots — Buddy firmware only has 16 mascot slots and the visual identity is the same. (No firmware change required.) - CLIProcessResolver.sourceMatchesExecutablePath learns to recognize cursor-agent (`/.local/share/cursor-agent/...` or `/cursor-agent/index.js`) and qodercli (`/qodercli` or `/@qoder-ai/qodercli`). - New `cliVariantOverride(declaredSource:ancestry:)` promotes a `--source cursor` tag to "cursor-cli" when the ancestry has a cursor-agent binary (same for qoder → qoder-cli). The bridge calls it after sourceTag/inferSource so explicit tags also get promoted. - inferSource scans `-cli` variants before plain ones so a path ending in `cursor-agent` doesn't get mis-attributed to `cursor`. Terminal-jump routing uses session.termBundleId / termApp which already points at the user's terminal app, so no TerminalActivator change is needed — clicking a Cursor CLI session jumps to the host terminal, not to Cursor.app. Existing users need to Reinstall Hooks once so the rebuilt bridge ships with cliVariantOverride. Refs wxtsky#134. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: review-driven hardening of session changes Three issues surfaced by independent code review against the just-landed batch (ProcessRunner / HookServer plugin filter / subagent tooltip): 1. ProcessRunner data race (Swift 6 strict-concurrency) `var data` mutated inside a Sendable closure and read on the calling thread compiled cleanly under Swift 5 mode but Swift 6 will flag the captured-var write. Wrap the slot in a private `@unchecked Sendable` reference cell so the synchronization (drained.signal happens-before drained.wait) is explicit. 2. HookServer plugin filter ran JSONSerialization on every hook event The pre-filter for `_via_plugin` was parsing every incoming payload on the main actor — wasteful for the common case where the marker isn't there. Probe the raw bytes for the literal `_via_plugin` string first; only fall through to JSON parsing on a hit. 3. Subagent tooltip dropped `toolDescription` The previous commit's message advertised "Explore — Read src/main.swift" but the actual code only rendered the bare tool name. Fold `sub.toolDescription` into the detail when present and replace the force-unwrap with a flatMap guard while we're in there. Refs wxtsky#103, wxtsky#123, wxtsky#139, wxtsky#141. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: clean up remaining review nits from session Five smaller follow-ups from the same review pass — all flagged minor or nit, but the ask was to clear the list, not pick: 1. cachedClaudeVersion race (NSLock) `detectClaudeVersion` is now reachable from two `Task.detached` call sites (wxtsky#139 install + verifyAndRepair). The static cache field was unguarded. Wrap read+write in an NSLock — writes are idempotent so functional impact was nil, but Swift 6 strict concurrency would flag it and a future refactor could turn the benign race into a real one. 2. firstStringFromDict no longer trims caller-visible value The reducer helper used to return `value.trimmingCharacters(...)` which silently stripped intentional leading / trailing whitespace from code-snippet prompts. Switch the trim to be empty-check-only and return the original value so chat history preserves the user's exact text. 3. Diagnostics export description (en / zh / ja / ko / tr) `state/hook-events.json` includes a 80-char prompt preview. The button description didn't say so. Update all five locales to spell out what the zip contains so users aren't surprised by including prompt text in a bug report. 4. findSessionId 5-min activity window Plugin merge previously matched any session sharing source + cliPid, including sessions whose CLI long since exited and whose PID may have been recycled by macOS for an unrelated process. Require lastActivity within 5 minutes — live sessions update on every event so the window is generous. 5. activateGhostty avoids double dispatch `activateGhostty` body already runs inside `DispatchQueue.global` (wxtsky#139). It then called `runOsaScript` which dispatched a second time. Add a `runOsaScriptSync` variant for callers already on a background queue and use it from activateGhostty. Refs wxtsky#103, wxtsky#123, wxtsky#139. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: empty default autoApproveTools Previous default silently auto-approved 9 internal agent tools (TaskCreate / TaskUpdate / TaskGet / TaskList / TaskOutput / TaskStop / TodoRead / TodoWrite / EnterPlanMode), which hid those calls from the approval panel — users who never opened Settings → Auto-Approve Tools didn't know that hiding was happening. Switch the default to the empty string. Every tool call now flows through the regular approval path; users who want the old behavior can opt-in per tool from the Auto-Approve Tools list. Existing users who'd already toggled the list explicitly are unaffected — UserDefaults register only fills in absent keys. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * perf: extract subagent tooltip builder out of ViewBuilder User reported the hover-triggered island expand animation felt janky after the recent batch landed. The most plausible regression was the subagent ForEach in wxtsky#141 — the review fix in be9231a swelled the inline tooltip computation to five nested `let`s plus an IIFE plus `flatMap` plus `.map`. That kind of expression in a SwiftUI ViewBuilder triggers slow-path type inference and forces the body to re-evaluate the whole closure tree on every hover state flip / animation tick. Move the logic to a file-private `subagentTooltipText(_:)` plain Swift function. The ForEach body is now just two lines: `MiniAgentIcon(...).help(subagentTooltipText(sub))`. Output is identical to the previous version. Refs wxtsky#141. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * release: v1.0.24 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: appcast.xml entry for v1.0.24 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * build: codesign DMG container before notarization build-dmg.sh was creating an unsigned DMG container — the inner .app got Developer ID signed and the dmg got stapled with a notary ticket, but the dmg envelope itself had no signature. \`spctl --assess\` reports "no usable signature" in that state, and Sparkle's update flow can abort with "An error occurred while running the updater" when the helper hand-off picks up the unsigned container. Sign the DMG with the same Developer ID identity (timestamped) before submitting to notarytool. The signature applies before notarization, so the Apple ticket attaches to the signed container. This is a latent issue — both v1.0.23 and v1.0.24 dmgs ship unsigned containers; the symptom was intermittent because most code paths only verify the inner .app + the staple ticket. v1.0.25+ won't have it. Workaround for users hitting the error on v1.0.24: download the dmg manually from the GitHub release page. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: stop blanket-draining pending permissions on activity events (wxtsky#147) handleEvent's wasWaiting branch used to drain the entire permission queue for a session whenever an activity event arrived in waitingApproval/waitingQuestion. The "user replied in terminal" heuristic misfires for parallel MCP / plugin tool calls: e.g. the official Notion plugin fetching 2 pages in parallel — the first PostToolUse arrives while the second tool's PermissionRequest is still pending, the blanket drain denies it, and the user sees "briefly shown then auto-denied". Switch to surgical drain only — resolveToolUseIfCompleted already removes queue entries by tool_use_id when PostToolUse / PostToolUseFailure / PermissionDenied for the same id arrives. Other pending requests are unrelated parallel work and must keep waiting. Question queue still gets drained (questions are rare and don't carry tool_use_id reliably), and session status only resets to idle/processing when nothing is left pending for the session. Also threads a `reason:` argument through drainPermissions / drainQuestions and adds log.notice traces at every auto-deny site (resolveToolUseIfCompleted, mergeDuplicatePermissionRequest, drainPermissions/Questions internal). Surfaced as "⚠️ permission deny reason=… session=… toolUseId=… tool=…" in com.codeisland subsystem so future "card flashed and disappeared" reports can be diagnosed from Console.app without a custom build. Tests: add 2 wxtsky#147 regressions (testStopEventDoesNotDenyPendingPermission, testParallelPostToolUseDoesNotDenyUnrelatedPendingPermission). 241/241 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: collapse Cursor sub-agent processes onto one session card (wxtsky#148) Cursor IDE spawns N parallel sub-agent subprocesses per logical session (file search, code analysis, etc.), and each sub-agent runs its own hook subprocess with a different immediate ppid. Bridge's session_id fallback used `cursor-ppid-<getppid()>`, fanning each sub-agent into a separate session card — users saw 10+ "Cursor — thinking" cards for what was logically one session, drowning out the actual session they were interacting with. Add CLIProcessResolver.resolvedSessionPID — same shape as resolvedTrackedPID but picks the *root-most* same-source binary in the ancestry instead of the nearest. Bridge's session_id fallback now uses it, so all sub-agents spawned by the same root cursor-agent collapse onto a single session_id. To call resolvedSessionPID at fallback time we need ancestry already computed, so the ancestry/effectiveSource block was moved up in main.swift to run before the session_id fallback. Same code, earlier position — `_ppid` / `_via_plugin` / `_source` semantics unchanged. resolvedTrackedPID still uses `first(where:)` (nearest binary) because its job — telling AppState which PID to monitor for liveness — wants the closest CLI process, not the root one. Tests: new CLIProcessResolverTests covering parallel sub-agents collapsing to the same root PID, root-most vs nearest distinction between the two resolvers, and fallbacks (no match, empty ancestry, nil source, non-positive immediate ppid). 247/247 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: honor user default mascot whenever no session is actively working (wxtsky#149) wxtsky#102 fixed the empty-session case (no sessions → use default mascot), but `refreshDerivedState` and `CompactLeftWing.displaySource` still echoed `deriveSessionSummary`'s `mostRecentIdleSource` whenever any session existed — even all-idle ones. Result: a user who picked Codex as the default mascot still saw Claude every time their last session went idle, because the most recently active session's source kept "sticking" to the notch. Switch the trigger from `totalSessionCount == 0` to `summary.status == .idle`, covering both empty-state (wxtsky#102) and all-idle (wxtsky#149) under one rule. Active work (running / processing / waitingApproval / waitingQuestion) still wins — we don't want to mute the source of something actively happening just because the user has a preferred idle mascot. NotchPanelView's `CompactLeftWing.displaySource` mirrors the same logic so the compact wing renders the user default in idle even when the picked-out displaySession happens to be an idle one. Tests: 4 new regressions in AppStatePrimarySourceTests (idle-honors-default, active-overrides-default, empty-honors-default, mixed-active-and-idle-uses-active). 251/251 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Arda Kılıçdağı <ardakilicdagi@gmail.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: jerry0804 <jerryzhao0804@gmail.com> Co-authored-by: JZ <jz@JZdeMacBook-Pro.local> Co-authored-by: Drswith <49299002+Drswith@users.noreply.github.com> Co-authored-by: K0ala <liushaoxiong10@outlook.com> Co-authored-by: wxtsky <1970550145@qq.com>
Uncle-Peke
added a commit
to Uncle-Peke/CodeIsland
that referenced
this pull request
May 15, 2026
* chore: polish Turkish translation for stylistic consistency (wxtsky#135) Align 4 post-PR entries with the established style (Title Case for setting labels, ALL CAPS for action buttons, correct semantics): - auto_collapse_after_session_jump: lowercase → Title Case - dismiss: ERTELE (postpone) → YOKSAY (ignore/dismiss) - buddy_reconnect: sentence case → Title Case - buddy_enable_bluetooth: sentence case → Title Case Key parity unchanged (259/259 keys covered). Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: prevent Sparkle crash in DEBUG mode when running without bundle ID (wxtsky#133) Co-authored-by: JZ <jz@JZdeMacBook-Pro.local> * fix: avoid stale update abort errors (wxtsky#138) * feat: improve Buddy watch approval previews and alerts (wxtsky#144) Show the full Bash approval command alongside the human-readable description so pending approvals stay understandable on macOS and on the watch. Preserve pending TraeCLI approvals until the user responds, add watch-side approve/deny and skip controls over BLE, and trigger vibration plus richer attention notifications when approval is required. * test: align AppStateQuestionFlow assertions with wxtsky#144 toolDescription change wxtsky#144 changed `HookEvent.toolDescription` for Bash to combine description and command (so ESP32/watch previews show the full command). Update the existing two assertions to match the new format. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: add ProcessRunner with timeout to harden detectClaudeVersion (wxtsky#139) Replace the unbounded waitUntilExit() in detectClaudeVersion with a shared ProcessRunner that enforces a hard deadline (SIGTERM, then SIGKILL after 1s grace) and drains stdout off the wait path so a full pipe buffer cannot wedge the child. detectClaudeVersion now uses a 5s timeout — a stuck `claude --version` used to freeze app launch silently. Refs wxtsky#139. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: move launch-time hook installation off the main thread (wxtsky#139) ConfigInstaller.install() does subprocess version detection plus disk I/O across multiple CLI configs. Running it inline from applicationDidFinishLaunching meant a slow CLI binary or network home dir froze the menu bar / panel until install finished — sometimes silently, because macOS doesn't pop a "Not Responding" dialog for status items. Detach it onto a userInitiated Task so app UI comes up immediately while hooks install in the background. HookServer is still started synchronously before the detach so the socket is listening by the time settings.json is rewritten (preserves the ordering noted in the existing comment). Mark AppDelegate.log nonisolated so the detached task can use it without violating Swift 6 actor isolation (Logger is Sendable). Refs wxtsky#139. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: run periodic hook verifyAndRepair off the main thread (wxtsky#139) checkAndRepairHooks fires from a 300s timer AND every NSWorkspace didActivateApplicationNotification (i.e. each time the user app-switches). verifyAndRepair walks every enabled CLI and rewrites settings.json — on a network home dir or a slow disk that's enough to stutter the panel. Keep the 60s debounce on the main actor (cheap), but do the actual repair on a background task. Repair results only feed a log line so no UI thread hand-off is needed. Refs wxtsky#139. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: cap subprocess waits in TerminalActivator/VisibilityDetector (wxtsky#139) Both files reimplemented the same Process+Pipe+waitUntilExit pattern with no timeout. Activate() and the visibility checks fire from the main thread (panel buttons, shortcut handler, NotchPanelView body, etc.), so a stuck osascript / tmux / iTerm CLI was free to freeze the UI for as long as the child took to wake up. Replace both runProcess implementations with ProcessRunner.run: - TerminalActivator: 10s cap (matches current osascript-tab-switch worst-case behaviour; activateCmux still dispatches off-main on its own, the timeout is just a backstop). - TerminalVisibilityDetector: 5s cap (these are speculative "is the tab visible?" probes called frequently from view bodies — fail fast). Removes ~30 lines of duplicated boilerplate. No behaviour change on the happy path; on a hang, callers see nil instead of a frozen UI. Refs wxtsky#139. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: dispatch Ghostty activation off the main thread (wxtsky#139) activateGhostty was the last activator that still resolved its subprocess inputs synchronously on the main thread. It calls `tmux display-message` to fetch a session/window key (used to match the right Ghostty terminal in the AppleScript template) and then hands the script off to runOsaScript. Every other activateXxx already runs on a background queue (activateITerm/Warp/WezTerm/Kitty/Tmux/IDE window via runAppleScript or an explicit DispatchQueue.global; cmux has its own dispatch). Move the same pattern here: keep the AppKit gating (NSWorkspace runningApplications + unhide) on the main thread where it has to live, then dispatch the tmux probe, AppleScript construction, and osascript invocation onto a userInitiated queue. With this change no TerminalActivator code path can hold the main thread on a stuck subprocess, which closes the loop on issue wxtsky#139. Refs wxtsky#139. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: register Codex PermissionRequest hook so approvals trigger UI + sound (wxtsky#145) Codex CLI supports a PermissionRequest hook event (per developers.openai.com/codex/hooks) that fires when Codex needs user approval — shell escalation, managed-network access, etc. CodeIsland's Codex CLIConfig only registered SessionStart/End, UserPromptSubmit, PreToolUse, PostToolUse, and Stop, so this event was never installed into Codex's hooks.json. Result: when Codex hit an approval gate, the panel stayed in "running" status forever, no approval card opened, and SoundManager wasn't triggered (handlePermissionRequest is only called from the PermissionRequest event path). Add the event with the same shape as Claude's: 24h timeout, blocking (async=false) so Codex waits for the user's allow/deny decision. Existing users need to hit Settings → Reinstall Hooks once to rewrite ~/.codex/hooks.json with the new entry; new installs pick it up automatically. Refs wxtsky#145. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: setting to disable auto-expand on agent completion (wxtsky#146) Adds Settings → Behavior → "Auto-Expand Panel on Agent Completion" (default On — preserves current behavior). When off, CodeIsland stays in its compact state when agents/subagents finish; status indicators, the queue, and manual interactions are unaffected. Implementation: - New SettingsKey/SettingsDefaults entry registered with default true so existing users keep the current behavior across upgrades. - AppState.enqueueCompletion early-returns when the toggle is off. This is the single chokepoint — both Stop events from the reducer and any other paths into completion display funnel through here, so one guard covers all entry points. - BehaviorToggleRow added next to the existing auto-collapse toggle, reusing the .smartSuppress animation icon (semantically the closest "suppress auto-expand" preview we already have). - Translations added for en / zh / ja / ko / tr. Refs wxtsky#146. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: prefer opencode.jsonc over opencode.json when registering plugin (wxtsky#132) OpenCode's troubleshooting guide recommends opencode.jsonc (with-comments JSON) as the canonical config filename. Before this change, CodeIsland only ever read/wrote ~/.config/opencode/opencode.json, so users on opencode.jsonc would see CodeIsland resurrect a sibling opencode.json on every install / repair pass. Pick the target by precedence at install time: 1. opencode.jsonc — write here when it already exists (OpenCode-recommended) 2. opencode.json — fallback for existing setups, also the default for fresh installs 3. config.json — legacy, still cleaned up after migration uninstall and isOpencodePluginInstalled now scan all three. The existing mergeOpencodePluginRef path already runs through stripJSONComments, so .jsonc input parses correctly without changes. Refs wxtsky#132. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: surface subagent count + agent type tooltip on session card (wxtsky#141) Subagents already rendered as 8px MiniAgentIcons under the mascot but were visually identical and unlabelled — users couldn't tell at a glance which sessions had spawned background work, or what each subagent was actually doing. Two additions, no layout changes: 1. Each MiniAgentIcon now carries a .help(...) tooltip that surfaces the agent type and current tool (e.g. "Explore — Read src/main.swift", or just "general-purpose" when no tool is active). 2. A new SessionTag "+N Sub" (purple) appears in the session header tag row alongside @Remote / INT / YOLO when session.subagents is non-empty — gives the at-a-glance count the issue asked for without touching the column-1 mascot/icon block. Refs wxtsky#141. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: Settings → Plugin Sub-Sessions: separate / merge / hide (wxtsky#123) When a plugin running inside another agent (e.g. omo inside OpenCode) fires its own hook events without an explicit --source, bridge already infers the real source via process ancestry (wxtsky#95). The plugin's events end up with the right source but a different session_id from the main session, so the user sees two same-source pets — confusing for users who think of the plugin as part of the main session. Add a 3-way Settings toggle to control how those events render: - separate (default, current behavior): plugin events keep their own session_id, render as their own card. - merge: rewrite session_id to the matching main session (same source, same _ppid). Plugin events fold into the parent agent's card. - hide: drop plugin events entirely; PermissionRequest gets an auto-allow so plugin tool execution isn't blocked by a UI prompt the user asked to suppress. Implementation: - Bridge: stamp `_via_plugin = true` when sourceTag is nil but inferSource succeeded (this is exactly the "plugin proxy" path). - HookServer.processRequest: pre-filter `_via_plugin` events per the pluginSessionMode setting before HookEvent construction. Merge looks up AppState.findSessionId(forSource:ppid:) using SessionSnapshot.cliPid (which already mirrors bridge's _ppid). Hide returns the right payload for blocking events. - Settings + SettingsView: new pluginSessionMode key (default "separate" preserves existing behavior), Picker added to the Sessions section. - L10n: en / zh / ja / ko / tr translations for title, desc, and the three options. Existing users will need to Reinstall Hooks once so the new bridge binary stamps `_via_plugin`. Without the new bridge, all paths behave exactly as before (separate). Refs wxtsky#123. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: hook event ring buffer + diagnostics export (wxtsky#103) Issue wxtsky#103 (qwen problem 1: prompts not surfacing) and wxtsky#117 (Hermes hooks not flowing) both stalled on the same thing — without runtime visibility into which events HookServer accepted with what source / session_id, all triage was guessing. Add a 100-entry in-memory ring that captures the post-merge view of every dispatched hook event, exported to state/hook-events.json on diagnostics export. Per event we keep just the fields needed to reconstruct routing decisions: timestamp, source, sessionId (truncated), eventName, toolName, and viaPlugin (wxtsky#123). Payloads stay out of the export so bug reports remain a sane size. Mechanism: - AppState gains a nested DiagnosticHookEvent struct, an ObservationIgnored ring buffer, and a recordHookEvent(...) entry. Capped at 100 — older entries trimmed FIFO. - HookServer.processRequest records right after HookEvent construction (so merged session_ids are reflected) and before the routeKind switch. - DiagnosticsExporter adds a state/hook-events.json step alongside the existing sessions.json snapshot. Refs wxtsky#103, wxtsky#117. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: harden UserPromptSubmit prompt extraction + richer hook diagnostics (wxtsky#103) Two changes both aimed at the qwen "prompt not showing" symptom: 1. Reducer prompt extraction - Switch UserPromptSubmit's prompt extraction to firstStringFromEvent so it walks the same key list across both top-level and nested `payload` / `data` containers that the rest of the reducer already supports. - Add `userPrompt` / `text` to the candidate keys. - The helper trims and skips empty/whitespace-only values, so a hook that fires with `"prompt": ""` no longer inserts a blank chat row that pushes the rest of the UI around. 2. Hook ring buffer (wxtsky#103 follow-up) - DiagnosticHookEvent now also captures `payloadKeys` (top-level field names with bridge-injected `_*` filtered out) and `promptPreview` (first 80 chars of any extracted prompt). - HookServer computes both right after HookEvent construction and passes them into recordHookEvent. - DiagnosticsExporter writes them into state/hook-events.json. Together these answer the question that's blocked qwen / hermes / antigravity triage: when a user reports "my prompt isn't showing", exporting diagnostics now tells us at a glance whether (a) the hook didn't fire at all (no entry), (b) it fired without a prompt field (payloadKeys lists everything but `prompt` and promptPreview is null), or (c) it fired with a prompt but the UI still dropped it (promptPreview shows the text and we know to look at the SwiftUI path). Refs wxtsky#103, wxtsky#117. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: distinguish cursor-agent / qodercli from desktop IDE source (wxtsky#134) cursor-agent and qodercli share their hooks file (~/.cursor/hooks.json, ~/.qoder/settings.json) with the matching desktop IDE, so the bridge hook command is identical for both. Until now everything fired through that file landed as `--source cursor` / `--source qoder`, with the session displayed as "Cursor" / "Qoder" — confusing for terminal users who never opened the IDE. Add `cursor-cli` and `qoder-cli` as first-class sources: - SessionSnapshot.supportedSources / normalizedSupportedSource gain the two new keys plus aliases ("cursor-agent", "cursoragent", "cursorcli", "qodercli"). - sourceLabel renders them as "Cursor CLI" / "Qoder CLI". - ESP32Protocol.MascotID.init reuses the existing .cursor / .qoder mascot slots — Buddy firmware only has 16 mascot slots and the visual identity is the same. (No firmware change required.) - CLIProcessResolver.sourceMatchesExecutablePath learns to recognize cursor-agent (`/.local/share/cursor-agent/...` or `/cursor-agent/index.js`) and qodercli (`/qodercli` or `/@qoder-ai/qodercli`). - New `cliVariantOverride(declaredSource:ancestry:)` promotes a `--source cursor` tag to "cursor-cli" when the ancestry has a cursor-agent binary (same for qoder → qoder-cli). The bridge calls it after sourceTag/inferSource so explicit tags also get promoted. - inferSource scans `-cli` variants before plain ones so a path ending in `cursor-agent` doesn't get mis-attributed to `cursor`. Terminal-jump routing uses session.termBundleId / termApp which already points at the user's terminal app, so no TerminalActivator change is needed — clicking a Cursor CLI session jumps to the host terminal, not to Cursor.app. Existing users need to Reinstall Hooks once so the rebuilt bridge ships with cliVariantOverride. Refs wxtsky#134. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: review-driven hardening of session changes Three issues surfaced by independent code review against the just-landed batch (ProcessRunner / HookServer plugin filter / subagent tooltip): 1. ProcessRunner data race (Swift 6 strict-concurrency) `var data` mutated inside a Sendable closure and read on the calling thread compiled cleanly under Swift 5 mode but Swift 6 will flag the captured-var write. Wrap the slot in a private `@unchecked Sendable` reference cell so the synchronization (drained.signal happens-before drained.wait) is explicit. 2. HookServer plugin filter ran JSONSerialization on every hook event The pre-filter for `_via_plugin` was parsing every incoming payload on the main actor — wasteful for the common case where the marker isn't there. Probe the raw bytes for the literal `_via_plugin` string first; only fall through to JSON parsing on a hit. 3. Subagent tooltip dropped `toolDescription` The previous commit's message advertised "Explore — Read src/main.swift" but the actual code only rendered the bare tool name. Fold `sub.toolDescription` into the detail when present and replace the force-unwrap with a flatMap guard while we're in there. Refs wxtsky#103, wxtsky#123, wxtsky#139, wxtsky#141. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: clean up remaining review nits from session Five smaller follow-ups from the same review pass — all flagged minor or nit, but the ask was to clear the list, not pick: 1. cachedClaudeVersion race (NSLock) `detectClaudeVersion` is now reachable from two `Task.detached` call sites (wxtsky#139 install + verifyAndRepair). The static cache field was unguarded. Wrap read+write in an NSLock — writes are idempotent so functional impact was nil, but Swift 6 strict concurrency would flag it and a future refactor could turn the benign race into a real one. 2. firstStringFromDict no longer trims caller-visible value The reducer helper used to return `value.trimmingCharacters(...)` which silently stripped intentional leading / trailing whitespace from code-snippet prompts. Switch the trim to be empty-check-only and return the original value so chat history preserves the user's exact text. 3. Diagnostics export description (en / zh / ja / ko / tr) `state/hook-events.json` includes a 80-char prompt preview. The button description didn't say so. Update all five locales to spell out what the zip contains so users aren't surprised by including prompt text in a bug report. 4. findSessionId 5-min activity window Plugin merge previously matched any session sharing source + cliPid, including sessions whose CLI long since exited and whose PID may have been recycled by macOS for an unrelated process. Require lastActivity within 5 minutes — live sessions update on every event so the window is generous. 5. activateGhostty avoids double dispatch `activateGhostty` body already runs inside `DispatchQueue.global` (wxtsky#139). It then called `runOsaScript` which dispatched a second time. Add a `runOsaScriptSync` variant for callers already on a background queue and use it from activateGhostty. Refs wxtsky#103, wxtsky#123, wxtsky#139. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: empty default autoApproveTools Previous default silently auto-approved 9 internal agent tools (TaskCreate / TaskUpdate / TaskGet / TaskList / TaskOutput / TaskStop / TodoRead / TodoWrite / EnterPlanMode), which hid those calls from the approval panel — users who never opened Settings → Auto-Approve Tools didn't know that hiding was happening. Switch the default to the empty string. Every tool call now flows through the regular approval path; users who want the old behavior can opt-in per tool from the Auto-Approve Tools list. Existing users who'd already toggled the list explicitly are unaffected — UserDefaults register only fills in absent keys. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * perf: extract subagent tooltip builder out of ViewBuilder User reported the hover-triggered island expand animation felt janky after the recent batch landed. The most plausible regression was the subagent ForEach in wxtsky#141 — the review fix in be9231a swelled the inline tooltip computation to five nested `let`s plus an IIFE plus `flatMap` plus `.map`. That kind of expression in a SwiftUI ViewBuilder triggers slow-path type inference and forces the body to re-evaluate the whole closure tree on every hover state flip / animation tick. Move the logic to a file-private `subagentTooltipText(_:)` plain Swift function. The ForEach body is now just two lines: `MiniAgentIcon(...).help(subagentTooltipText(sub))`. Output is identical to the previous version. Refs wxtsky#141. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * release: v1.0.24 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: appcast.xml entry for v1.0.24 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * build: codesign DMG container before notarization build-dmg.sh was creating an unsigned DMG container — the inner .app got Developer ID signed and the dmg got stapled with a notary ticket, but the dmg envelope itself had no signature. \`spctl --assess\` reports "no usable signature" in that state, and Sparkle's update flow can abort with "An error occurred while running the updater" when the helper hand-off picks up the unsigned container. Sign the DMG with the same Developer ID identity (timestamped) before submitting to notarytool. The signature applies before notarization, so the Apple ticket attaches to the signed container. This is a latent issue — both v1.0.23 and v1.0.24 dmgs ship unsigned containers; the symptom was intermittent because most code paths only verify the inner .app + the staple ticket. v1.0.25+ won't have it. Workaround for users hitting the error on v1.0.24: download the dmg manually from the GitHub release page. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: stop blanket-draining pending permissions on activity events (wxtsky#147) handleEvent's wasWaiting branch used to drain the entire permission queue for a session whenever an activity event arrived in waitingApproval/waitingQuestion. The "user replied in terminal" heuristic misfires for parallel MCP / plugin tool calls: e.g. the official Notion plugin fetching 2 pages in parallel — the first PostToolUse arrives while the second tool's PermissionRequest is still pending, the blanket drain denies it, and the user sees "briefly shown then auto-denied". Switch to surgical drain only — resolveToolUseIfCompleted already removes queue entries by tool_use_id when PostToolUse / PostToolUseFailure / PermissionDenied for the same id arrives. Other pending requests are unrelated parallel work and must keep waiting. Question queue still gets drained (questions are rare and don't carry tool_use_id reliably), and session status only resets to idle/processing when nothing is left pending for the session. Also threads a `reason:` argument through drainPermissions / drainQuestions and adds log.notice traces at every auto-deny site (resolveToolUseIfCompleted, mergeDuplicatePermissionRequest, drainPermissions/Questions internal). Surfaced as "⚠️ permission deny reason=… session=… toolUseId=… tool=…" in com.codeisland subsystem so future "card flashed and disappeared" reports can be diagnosed from Console.app without a custom build. Tests: add 2 wxtsky#147 regressions (testStopEventDoesNotDenyPendingPermission, testParallelPostToolUseDoesNotDenyUnrelatedPendingPermission). 241/241 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: collapse Cursor sub-agent processes onto one session card (wxtsky#148) Cursor IDE spawns N parallel sub-agent subprocesses per logical session (file search, code analysis, etc.), and each sub-agent runs its own hook subprocess with a different immediate ppid. Bridge's session_id fallback used `cursor-ppid-<getppid()>`, fanning each sub-agent into a separate session card — users saw 10+ "Cursor — thinking" cards for what was logically one session, drowning out the actual session they were interacting with. Add CLIProcessResolver.resolvedSessionPID — same shape as resolvedTrackedPID but picks the *root-most* same-source binary in the ancestry instead of the nearest. Bridge's session_id fallback now uses it, so all sub-agents spawned by the same root cursor-agent collapse onto a single session_id. To call resolvedSessionPID at fallback time we need ancestry already computed, so the ancestry/effectiveSource block was moved up in main.swift to run before the session_id fallback. Same code, earlier position — `_ppid` / `_via_plugin` / `_source` semantics unchanged. resolvedTrackedPID still uses `first(where:)` (nearest binary) because its job — telling AppState which PID to monitor for liveness — wants the closest CLI process, not the root one. Tests: new CLIProcessResolverTests covering parallel sub-agents collapsing to the same root PID, root-most vs nearest distinction between the two resolvers, and fallbacks (no match, empty ancestry, nil source, non-positive immediate ppid). 247/247 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: honor user default mascot whenever no session is actively working (wxtsky#149) wxtsky#102 fixed the empty-session case (no sessions → use default mascot), but `refreshDerivedState` and `CompactLeftWing.displaySource` still echoed `deriveSessionSummary`'s `mostRecentIdleSource` whenever any session existed — even all-idle ones. Result: a user who picked Codex as the default mascot still saw Claude every time their last session went idle, because the most recently active session's source kept "sticking" to the notch. Switch the trigger from `totalSessionCount == 0` to `summary.status == .idle`, covering both empty-state (wxtsky#102) and all-idle (wxtsky#149) under one rule. Active work (running / processing / waitingApproval / waitingQuestion) still wins — we don't want to mute the source of something actively happening just because the user has a preferred idle mascot. NotchPanelView's `CompactLeftWing.displaySource` mirrors the same logic so the compact wing renders the user default in idle even when the picked-out displaySession happens to be an idle one. Tests: 4 new regressions in AppStatePrimarySourceTests (idle-honors-default, active-overrides-default, empty-honors-default, mixed-active-and-idle-uses-active). 251/251 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Arda Kılıçdağı <ardakilicdagi@gmail.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: jerry0804 <jerryzhao0804@gmail.com> Co-authored-by: JZ <jz@JZdeMacBook-Pro.local> Co-authored-by: Drswith <49299002+Drswith@users.noreply.github.com> Co-authored-by: K0ala <liushaoxiong10@outlook.com> Co-authored-by: wxtsky <1970550145@qq.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fix: prevent Sparkle crash in DEBUG mode when running without bundle ID
PR 描述 (Description)
问题背景 (Problem)
当直接通过 Xcode 运行项目或在终端执行调试二进制文件时,由于此时程序不是以完整的 .app
包形式运行,系统无法识别 CFBundleIdentifier。这会导致内置的 Sparkle
更新框架在启动时抛出致命错误(Fatal Error),表现为应用启动后卡死或直接闪退。
解决方案 (Solution)
在 UpdateChecker.swift 的启动逻辑中增加了环境检测:
影响 (Impact)