ping-island
erha19/ping-island/AGENTS.md
This file is a routing layer for coding agents working in this repo. Keep it short. Put long-lived detail in nearby code, focused docs, or tests. - Product overview: README.md - App entry: PingIsland/App/PingIslandApp.swift, PingIsland/App/AppDelegate.swift - Docked/detached presentation orchestration: PingIsland/App/IslandPresentationCoordinator.swift, PingIsland/App/WindowManager.swift - First-run surface-mode onboarding and mode-switch UI: PingIsland/App/AppDelegate.swift, PingIsland/UI/Window/SettingsWindowController.swift, PingIsland/UI/Views/SettingsWindowView.swift - Main state hub: PingIsland/Services/State/SessionStore.swift - Session association cache: PingIsland/Services/State/SessionAssociationStore.swift - Usage/quota snapshots for Claude status-line caches, Claude-family transcript token totals, and Codex rollout logs: PingIsland/Services/Usage/ - Claude-family transcript…
# AGENTS.md
This file is a routing layer for coding agents working in this repo. Keep it short. Put long-lived detail in nearby code, focused docs, or tests.
## Mission
- `PingIsland` is a macOS menu bar app that surfaces Dynamic Island-style status for Claude Code, Codex, Gemini CLI, Antigravity CLI, Hermes Agent, Qwen Code, Kimi CLI, Oh My Pi, and compatible hook-driven agent sessions.
- The main runtime path is:
- hook or app-server events
- monitoring and service layers
- `SessionStore`
- `SessionMonitor` and `NotchViewModel`
- SwiftUI notch UI
- There are two important codepaths:
- `PingIsland/`: the shipping Xcode app
- `Prototype/`: a SwiftPM prototype with focused tests and reference implementations
## Start Here
- Product overview: `README.md`
- App entry: `PingIsland/App/PingIslandApp.swift`, `PingIsland/App/AppDelegate.swift`
- Docked/detached presentation orchestration: `PingIsland/App/IslandPresentationCoordinator.swift`, `PingIsland/App/WindowManager.swift`
- First-run surface-mode onboarding and mode-switch UI: `PingIsland/App/AppDelegate.swift`, `PingIsland/UI/Window/SettingsWindowController.swift`, `PingIsland/UI/Views/SettingsWindowView.swift`
- Main state hub: `PingIsland/Services/State/SessionStore.swift`
- Session association cache: `PingIsland/Services/State/SessionAssociationStore.swift`
- Usage/quota snapshots for Claude status-line caches, Claude-family transcript token totals, and Codex rollout logs: `PingIsland/Services/Usage/`
- Claude-family transcript token totals are tailed incrementally by `ClaudeTranscriptUsageReader`, which skips reopening unchanged files, rebuilds only after replacement/truncation, defers half-written trailing records, and carries a streaming FNV-1a digest so `AgentUsageStore` deduplication stays stable. Keep cumulative totals for the whole transcript, and do not return to whole-file reparsing for ordinary appends.
- Local agent usage analytics keeps a backward-compatible per-day session token ledger for the statistics panel's latest-three-today total and current-calendar-week top session; preserve legacy `agent-usage.json` decoding when evolving it
- `AgentUsageTokenTotals` keeps the input tiers apart: `input` is uncached input only, with `cacheCreation`/`cacheRead` alongside it. Cache reads are re-reads of context already sent, so they stay out of `total`, out of `billableInput`, and are priced at their own rate; folding them back into `input` overstates usage by orders of magnitude on long sessions. Bump `AgentUsageDocument.currentSchemaVersion` whenever stored counters change meaning, and migrate by zeroing token counters while leaving session records, tool counts, and the activity heatmap intact.
- One assistant response spans several transcript lines (one per content block) that each repeat the same `message.usage`; `ClaudeTranscriptUsageAccumulator` counts a `message.id` once per consecutive run. Those repeats are always adjacent, so keep this O(1) rather than introducing a seen-set that would grow with the transcript.
- Native runtime rollout scaffold: `PingIsland/Services/Runtime/`, `PingIsland/Core/FeatureFlags.swift`
- Session bridge for UI: `PingIsland/Services/Session/SessionMonitor.swift`
- Claude-family transcript titles and last-message text: `PingIsland/Services/Session/ConversationParser.swift`
- Missing derived Claude transcripts must not fall back to unrelated OpenClaw history. OpenClaw rotation recovery requires an explicit path in that client's sessions directory. Reload store state after awaiting transcript reads so completed or archived sessions are not restored from stale snapshots.
- Claude Code 2.x stopped writing `summary` records and now persists the title shown in its own UI as a `custom-title` record, rewritten on every turn. Take the newest one, keep the legacy `summary` path as a fallback for older transcripts and other Claude-compatible clients, and only fall back to the first user message when neither exists.
- Notch state and layout: `PingIsland/Core/NotchViewModel.swift`, `PingIsland/UI/Views/NotchView.swift`
- App-wide low-power policy for background polling, event monitoring, UI animation tiers, and silent update gating: `PingIsland/Core/EnergyGovernor.swift`
- Session-aware keep-awake: `PingIsland/Core/SessionKeepAwake.swift`, `PingIsland/Core/KeepAwakePolicy.swift`, and [behavior/verification](docs/keep-awake.md). Use one assertion owner and one persisted `keepAwakeMode` shared by the notch shortcut and settings. Auto-mode power checks must continue while work or grace exists even after low battery releases the assertion; measure the 120-second grace from the last working session stopping, not its last published activity. Always mode bypasses the battery floor and must not run a grace timer.
- User idle protection for temporarily routing blocking approvals/questions back to terminals: `PingIsland/Core/UserIdleAutoProtection.swift`, `PingIsland/Core/Settings.swift`, `PingIsland/Services/Hooks/BridgeRuntimeConfigWriter.swift`
- Detached floating capsule: `PingIsland/UI/Window/DetachedIslandWindowController.swift`, `PingIsland/UI/Views/DetachedIslandPanelView.swift`, `PingIsland/UI/Views/IslandOpenedContentView.swift`
- Detached pet interactions now keep the pet anchored in place while hover/click previews expand sideways as message-bubble lists; trace both the panel layout and window-anchor math together when changing this flow
- Expanded content routing is shared with the docked notch through `IslandOpenedContentView` + `IslandExpandedRouteResolver`; keep hover/click/notification semantics aligned instead of reintroducing detached-only content priorities
- Global shortcuts and shortcut persistence: `PingIsland/Services/Shared/GlobalShortcutManager.swift`, `PingIsland/Utilities/GlobalShortcut.swift`, `PingIsland/Core/Settings.swift`, `PingIsland/UI/Views/SettingsWindowView.swift`
- Claude hook ingress: `Prototype/Sources/IslandBridge/`, `PingIsland/Services/Hooks/HookInstaller.swift`, `PingIsland/Services/Hooks/HookSocketServer.swift`
- `PingIslandBridge` is the unified Claude/Codex hook entrypoint and is responsible for terminal, tmux, SSH-remote, and IDE terminal context capture before envelopes hit Swift code
- `AskUserQuestion` must remain answerable under `bypassPermissions` and session auto-approval. A question response already allows the tool with `updatedInput`; never mark its tracked tool as waiting for approval, or the next hook resurrects a redundant approval card.
- Claude questions intercepted at `PreToolUse` need the same long hook timeout as `PermissionRequest` in both local and remote installs. Only recognized question tools own that answer channel; a normal tool's `questions` argument must never turn it into an Island question.
- State-only hook delivery is acknowledged by the app after envelope decoding and routing; keep bridge socket writes complete and do not report `deliveryOutcome=delivered` without a matching app acknowledgement
- Codex ingress: `PingIsland/Services/Codex/`, `PingIsland/UI/Views/CodexSessionView.swift`
- The desktop client is branded ChatGPT; retain the `codex-app` profile, Codex provider/CLI identifiers, `com.openai.codex` bundle ID, and `codex://` thread links for compatibility. App-server discovery prefers `ChatGPT.app`, keeps legacy `Codex.app` as a fallback, and may only launch its bundled `codex` binary after validating the canonical `com.openai.codex` app. Other IDEs can embed a binary with the same filename; their app identity must not be applied to every Codex thread. Rollout `originator` stays host/source metadata and must not replace the canonical client-profile display name.
- Qoder statistics variables never establish host identity. Desktop evidence beats bare IDE hints; TTY, terminal program/session, and tmux evidence preserve real Codex CLI routing. Routing repairs must replace nullable fields in both persisted associations and current memory instead of merging stale hosts back in. See [Codex identity and filtering](docs/codex-session-identity.md).
- Title and ambient suggestion helpers can use real project directories and execute tools. Recognize exact helper sources or dedicated opening prompts in `CodexAuxiliaryHookFilter` across hooks, app-server snapshots, rollout recovery, and restored UI rows. Never hide ordinary JSON replies or user-created `ambient_suggestion_task` threads by their output shape.
- Hook-less fallback parsing for Codex sessions lives in `PingIsland/Services/Codex/CodexRolloutParser.swift`
- The fallback parser tails append-only rollout JSONL incrementally, rebuilds after file replacement/truncation, skips pathological oversized records, and retains a bounded recent history. Keep the full content of each retained item, and do not return to whole-file reparsing for ordinary appends.
- App-server `thread/list` rows are canonicalized by thread ID before ingestion. Keep active/newer/readable rollout candidates preferred, keep rollout recovery cache keys path-scoped, and discard the old parser cache only when the canonical rollout path genuinely changes.
- Terminal and focus control: `PingIsland/Services/Tmux/`, `PingIsland/Services/Window/`, `PingIsland/Utilities/TerminalVisibilityDetector.swift`
- Terminal focus flows currently cover iTerm2, Ghostty, Terminal.app, tmux, and IDE-hosted terminals
- Sessions hosted inside the Claude desktop app are focused by tab, not by app: `PingIsland/Services/Session/ClaudeDesktopSessionIndex.swift` maps the hook `session_id` (the CLI session id) to the desktop-side `local_…` id recorded in `~/Library/Application Support/Claude/{claude-code-sessions,local-agent-mode-sessions}/<org>/<account>/local_*.json`, and `SessionLauncher` opens `claude://claude.ai/epitaxy/<local id>` (the app's own session route; `/code/<id>` reloads the page and lands on the last-focused tab since Claude 1.46). The `claude://code/continue?session=…` entry link is a server-gated surface and cannot be relied on
- Remote SSH forwarding and remote-host management: `PingIsland/Services/Remote/`
- Remote hosts can bootstrap a bridge on the SSH target, rewrite remote hooks, install managed plugin-directory integrations such as Hermes under the remote home directory, and attach a bidirectional forwarding channel back into PingIsland
- The remote bridge forwards recent Codex app-server thread activity from the SSH target's `~/.codex/state_*.sqlite` through the existing remote hook-event channel
- Provider/client routing: bridge envelopes are normalized in `PingIsland/Services/Hooks/HookSocketServer.swift`, stored on `SessionState`, and launched via `PingIsland/Services/Window/SessionLauncher.swift`
- IDE host names mirrored into `client_originator` and terminal bundle IDs describe the terminal surface, not the agent. Keep them out of runtime client matching; generic Claude hooks must retain a concrete `claude-code` profile so fresh events can correct stale client associations.
- Client profile registry: installable hook clients and runtime client branding / recognition are centralized in `PingIsland/Models/ClientProfile.swift`
- The Kimi desktop app is a separate target (`kimi-app-hooks`). It runs a vendored kimi-code kernel (`@moonshot-ai/agent-core`) out of `~/Library/Application Support/kimi-desktop/daimon-share/daimon/runtime/kimi-code/config.toml`, so chat and agent turns both speak the same hooks protocol as the CLI. That file is rewritten every time Kimi's daimon runner starts, which drops the managed block; `KimiAppHookGuard` is started/stopped by `SessionMonitor`, polls it and reinstalls whenever the block goes missing; failed writes must stay retryable even if the file timestamp is unchanged. Sessions from this target carry `--client-kind kimi-app` so they resolve to the `kimi-app` runtime profile and badge as "Kimi App".
- VS Code-compatible IDE focus extension install / URI launch: `PingIsland/Services/Window/IDEExtensionInstaller.swift`, `PingIsland/Services/Window/TerminalSessionFocuser.swift`
- Session list UI: `PingIsland/UI/Views/SessionListView.swift`
- Client mascot system: `PingIsland/UI/Components/MascotView.swift`, `PingIsland/UI/Views/MascotSettingsView.swift`
- App updates and release notes: `PingIsland/Services/Update/`, `PingIsland/UI/Views/ReleaseNotesWindowView.swift`, `PingIsland/UI/Window/ReleaseNotesWindowController.swift`
- Sparkle build configuration: `Config/App.xcconfig`, `Config/LocalSecrets.xcconfig`, `docs/sparkle-release.md`
- Mac App Store distribution lane: `PingIslandAppStore` target / scheme, `PingIsland/Info-AppStore.plist`, `PingIsland/Resources/PingIsland-AppStore.entitlements`, `Config/AppStore.xcconfig`, `scripts/build-app-store.sh`, and `docs/mac-app-store-submission.md`
## Repo Map
- `PingIsland/App`: app lifecycle, window setup, screen observation
- `PingIsland/Core`: notch geometry, shared state, app settings, selectors
- `PingIsland/Models`: domain models for sessions, events, tools, phases
- `PingIsland/Services`: ingestion, socket handling, state management, tmux, windows, updates
- `PingIsland/Services/Usage`: Claude status-line quota cache readers, Claude-family transcript token parsing, and Codex rollout quota readers for UI usage summaries
- `PingIsland/Services/Runtime`: isolated native Claude/Codex runtime work. This path should coexist with the current implementation behind feature flags until parity is proven.
- `PingIsland/Services/Remote`: remote endpoint persistence, SSH bootstrap / attach, and remote hook forwarding
- Remote bootstrap currently covers JSON hook configs, managed hook directories, and managed plugin directories (for example remote Hermes installs under `~/.hermes/plugins/ping_island`)
- `PingIsland/Services/Update`: Sparkle updater bridge, appcast/release-notes loading, update state publishing
- `PingIsland/Services/Window/IDEExtensionInstaller.swift`: installs the VS Code-compatible terminal-focus extension used by Cursor / VS Code / CodeBuddy / Qoder IDE / Qoder CN IDE style hosts (`QoderWork` is hook-only, not an IDE extension host)
- `PingIsland/UI`: SwiftUI views, reusable components, AppKit-backed window controllers
- `PingIsland/Resources`: hook assets, entitlements, bundled fonts
- `Prototype`: Swift package prototype and testbed
- `Prototype/Tests`: logic-level unit tests plus process/socket e2e coverage for `IslandBridge`, hook mapping, and install flows
- `scripts`: release, signing, and packaging automation
- `Config`: checked-in build configuration defaults plus optional local-only secrets overrides
## Change Routing
- If you change hook payload shape or hook event semantics, update these together:
- `Prototype/Sources/IslandBridge/`
- `PingIsland/Services/Hooks/HookSocketServer.swift`
- `PingIsland/Models/SessionEvent.swift`
- `PingIsland/Services/State/SessionStore.swift`
- the affected UI under `PingIsland/UI/`
- If you change provider/client detection or click-through behavior, trace through `HookSocketServer`, `SessionStore`, `SessionState`, `SessionLauncher`, and the session list / hover UI so labels and launch targets stay in sync.
- If you add a Claude-compatible hook client, start in `PingIsland/Models/ClientProfile.swift` and wire any truly client-specific behavior from there before adding new ad-hoc switches elsewhere.
- Gemini CLI hooks are managed through `~/.gemini/settings.json`; its `BeforeTool` / `AfterTool` matchers are regex-based, so use `.*` rather than Claude-style `*`.
- Antigravity CLI is managed as a generated native plugin under `~/.gemini/antigravity-cli/plugins/ping-island/`, with `plugin.json` plus namespaced `hooks.json`. Its camelCase hook payloads use `conversationId`, `workspacePaths`, and `toolCall`; keep those normalized at the bridge boundary. Ping Island's observational `PreToolUse` hook must return `ask` so Antigravity's native permission engine remains authoritative, including when the Island socket is unavailable.
- Hermes Agent CLI integration must use plugin hooks under `~/.hermes/plugins/ping_island/`; `~/.hermes/hooks/` is gateway-only and will not fire in the Hermes CLI, so keep Ping Island on `ctx.register_hook()`-based plugin registration instead of gateway hook directories.
- Qwen Code hooks are managed through `~/.qwen/settings.json`; follow the official Qwen Code hook event names (`PreToolUse`, `PostToolUseFailure`, `Notification`, `Stop`, etc.) and remember that `Notification` matcher values are exact notification types such as `permission_prompt`, `idle_prompt`, and `auth_success`.
- OpenClaw hooks are managed as a generated internal hook directory under `~/.openclaw/hooks/<hook-name>/` and require the paired enablement entry in `~/.openclaw/openclaw.json`; treat it as a directory-discovery integration, not a JSON hook list.
- Gemini `Notification` hooks are observability-only in the upstream protocol; do not treat them as actionable approval callbacks unless the bridge grows explicit Gemini response handling.
- Qoder-family hook installs currently cover Qoder IDE and Qoder CLI as separate profiles that share `~/.qoder/settings.json`, plus QoderWork under `~/.qoderwork/settings.json`. Keep Qoder IDE and Qoder CLI hook semantics independent even though they share a file; app launch should refresh only the Qoder CLI managed entries when `qodercli -v` is newer than 0.2.5 while preserving Qoder IDE hooks and unrelated JSON settings. New Qoder CLI uses Claude Code-compatible blocking hooks and response payloads; Qoder IDE and QoderWork remain notify-only and must not create Island-side blocking question or approval responses.
- Qoder CN IDE is a separate product identity: its desktop app uses bundle identifier `com.aliyun.lingma.ide`, URI scheme `qoder-cn`, and `.qoder-cn` data root, while its CLI executable is `~/.local/bin/qoderclicn`. Keep `qoder-cn` and `qoder-cn-cli` separate from the international Qoder profiles even though the CN desktop and CLI share `~/.qoder-cn/settings.json`; the desktop remains notify-only while the CLI uses blocking Claude-compatible responses.
- Qoder App is a separate runtime profile (`qoder-app`, `com.qoder.app`); Qoder CN uses the product name `Qoder CN`, with `qoder-cn-app` reserved for explicit `parent_business_info.product=app` evidence because its bundle overlaps the IDE. Real source processes and payload product beat shared hook arguments. App questions retain the blocking answer channel, automatically submit available default options, and keep the QoderWork-style question notice / open-client action; ordinary App approvals remain actionable. Shared desktop hooks need the same long question/approval timeout as CLI hooks. See [Qoder identity and question routing](docs/qoder-clients.md).
- CodeBuddy-family hook installs currently cover CodeBuddy IDE and CodeBuddy CLI as separate profiles that share `~/.codebuddy/settings.json`, plus WorkBuddy under `~/.workbuddy/settings.json`. Keep CodeBuddy IDE and CodeBuddy CLI hook semantics independent even though they share a file; CodeBuddy CLI uses its Claude-compatible hook response shape and must preserve CodeBuddy IDE hooks plus unrelated JSON settings.
- OpenCode is managed as a generated plugin file under `~/.config/opencode/plugins/ping-island.js`; treat it as a plugin-based integration, not a JSON hooks file.
- Kimi CLI hooks are managed primarily through `~/.kimi-code/config.toml`, with `~/.kimi/config.toml` retained as a legacy fallback; use `[[hooks]]` array-of-tables syntax. The installer preserves all non-Island TOML content (providers, models, loop_control, etc.) and only manipulates the `[[hooks]]` sections. Event names follow the Claude Code convention (`SessionStart`, `SessionEnd`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `PermissionRequest`, `Notification`, `Stop`).
- Pi Agent is managed as a generated TypeScript extension under `~/.pi/agent/extensions/ping_island/index.ts`; treat it as an official Pi extension integration that forwards events through the Claude-compatible bridge with `client-kind=pi`, not as JSON/RPC polling or process scanning. Pi has a dedicated `MascotKind.pi`, so trace mascot changes through `ClientProfile`, `SessionProvider`, `MascotView`, and mascot settings together.
- Oh My Pi (OMP) hooks are managed as a generated TypeScript hook file at `~/.omp/agent/hooks/pre/ping-island.ts`; forward events through the Claude-compatible bridge with `client-kind=omp`. OMP uses `HookAPI` from `@oh-my-pi/pi-coding-agent/extensibility/hooks` and emits Claude-style event names (`SessionStart`, `SessionEnd`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `PermissionRequest`, `Stop`).
- OMP's built-in question tool is named `ask`. The template intercepts `ask` tool calls island-first: it sends a blocking Island question (Claude-style `AskUserQuestion` envelope with `expectsResponse` and `tool_use_id`) and waits unbounded, matching OMP's native ask dialog default (`ask.timeout = 0` waits indefinitely). This keeps several concurrent pending questions across sessions answerable in Island in any order; the app routes each answer back by `tool_use_id`. The handler only falls through to OMP's native ask UI when the bridge cannot reach Island (fast failure) or Island closed the socket without an answer (superseded question, session end, app quit). Fallback and headless `ask` calls must return directly to OMP without sending a second generic blocking `PreToolUse` request whose answer would be discarded. Do not reintroduce a template-side answer window: a bounded window abandons the bridge process while the app still accepts the answer, so late answers are silently dropped. Do not leave a `ctx.ui` call in flight when the handler returns (cancelled dialogs can render after the tool result and never resolve). Keep the mapper's question-tool recognition scoped to `provider == .omp` for the generic `ask` name so other clients' `ask` tools are never misclassified.
- `QoderWork` should not be added to `ideExtensionProfiles` unless it actually ships VS Code-compatible extension support in the future.
- If you change how sessions are associated across relaunches or between hook/app-server ingress paths, inspect both `SessionStore` and `SessionAssociationStore` so cached client metadata stays compatible.
- If you change the new native runtime rollout path, keep it isolated from the legacy hook/app-server flow. Reuse shared `SessionState`-driven views, but keep runtime orchestration, persistence, and feature gating under `PingIsland/Services/Runtime/` and `PingIsland/Core/FeatureFlags.swift`.
- If you change session lifecycle or transitions, start in `SessionStore`. Avoid ad-hoc state mutation elsewhere.
- Workspace supersession and liveness cleanup must protect `needsPromptNotification`, including terminal-only questions with suppressed in-app controls; `needsManualAttention` only covers Island-answerable requests. Explicit SessionEnd still takes precedence. Hover previews keep connected waiting-for-input and recently completed sessions, while disconnected rows remain outside the active preview.
- Idle hooks must evaluate execution evidence against the activity time before the incoming hook refresh. All transcript pollers (including native runtime and Claude desktop) must deliver pending tool-result-only deltas even when no new chat messages are parsed, and suppress repeated delivery after those tools complete.
- Current rule: provider-originated end events should preserve the session in `.ended` so it stays visible in the list; only explicit user archive/removal should delete it from `SessionStore`.
- Primary list rule: sessions with no new activity for 30 minutes should auto-hide from the primary list until fresh hook/file/app-server activity updates `lastActivity`; sessions that need manual attention should stay visible.
- Same-workspace rule: several agent sessions may run in one directory at the same time. `PingIsland/Utilities/SameWorkspaceSessionSupersession.swift` owns the single rule for when a newer session replaces an older one, and both `SessionStore.endOrphanedSessions` and `SessionMonitor.filteredVisibleSessions` must go through it. A session's visibility may never depend on which sibling reported activity most recently; only sessions that cannot still be running (no live process, no live execution evidence, no pending intervention, not ended) may be replaced.
- Recent activity is a first-class liveness signal, not a convenience: the Claude desktop app reports no pid, and a session between two tool calls has no `.running` tail, so `recentActivityLivenessWindow` is the only thing keeping two live agents in one directory from deleting each other on every hook — each rebuilt from scratch by its next event, the list showing one row that swaps identity every few seconds. A leftover from a restart is indistinguishable from a live sibling at that instant, so it is waited out rather than guessed at; it makes way once it has been quiet past the window.
- Liveness signals must stay bounded, or a crashed agent latches its row into "working" forever. A transcript records that a tool started, never that it was abandoned, so `SessionState.hasLiveExecutionEvidence` expires after `liveExecutionEvidenceMaxAge`; a pid alone is not identity, so `SessionProcessLiveness.isAlive(_:lastSeenAlive:)` also checks the process start time against the session's last activity to catch pid reuse; and recent activity ages out on its own. Keep every bound when adding new liveness checks.
- Transcript reads are evidence about the agent, so they must not manufacture it. `ConversationParser.parseIncremental` returns only genuinely appended messages — an unchanged file yields none, and callers wanting the whole conversation read `IncrementalParseResult.allMessages`. Republishing the accumulated conversation as a delta made the sync a `Stop` hook schedules look like a fresh turn and dragged finished sessions back into `processing`. For the same reason `SessionStore.processFileUpdate` only treats a user message *newer than the session's last activity* as the user starting a turn: every full-conversation payload contains historical prompts.
- A session's transcript is addressed by its own id. Only OpenClaw rotates a transcript out from under the path its hook reported, so only an OpenClaw path may fall back to "newest `.jsonl` in that directory"; pointed at `~/.claude/projects/<workspace>/` that fallback made a session which had not written a transcript yet adopt a live sibling's file, and it then carried that session's title, messages, and token totals in a row of its own.
- If you change notch sizing, opening behavior, or visibility, inspect both `NotchViewModel` and `NotchView`.
- Smart suppression gates automatic attention expansion in both docked and detached modes through `AutoOpenSuppressionPolicy`; its user-active option is subordinate to the master setting. `SessionManualAttentionTracker` must consume suppressed approval/question edges (including delayed auto-approve requests) so later idle time does not replay them. Preserve explicit click/hover access, attention cues/sounds, and the separate completion/compaction popup settings.
- Approval attention deduplication must use the actual request key, including intervention IDs when no `activePermission` exists. Consecutive approvals in one session need independent notifications and independent auto-approval delays.
- Closed-notch input is armed from `EventMonitors.mouseRoutingLocation` before mouse-down to prevent clicks reaching menu-bar items underneath. Keep this lightweight routing active in interaction-only energy modes, while hover work stays on the gated `mouseLocation` stream; hidden/detached windows must remain pass-through.
- The screen-attached notch is theme-invariant: keep its surface and top separator black, retain its canonical PingIsland motion/content styling, and do not reintroduce experience-theme surface or grid tokens there. Detached panels remain theme-aware.
- If you change docked/detached Island transitions or drag-to-detach behavior, trace through `IslandPresentationCoordinator`, `WindowManager`, `NotchViewModel`, `NotchWindowController`, and `DetachedIslandWindowController` together so gesture gating, content resolution, and re-docking stay aligned.
- If you change the persisted surface mode or first-run onboarding, trace through `AppDelegate`, `WindowManager`, `IslandPresentationCoordinator`, `SettingsWindowController`, `SettingsWindowView`, and `Settings.swift` together so launch-time routing and in-app switching stay aligned.
- If you change global shortcuts, shortcut persistence, or shortcut hints, trace through `PingIsland/Services/Shared/GlobalShortcutManager.swift`, `PingIsland/Utilities/GlobalShortcut.swift`, `PingIsland/Core/Settings.swift`, `PingIsland/UI/Views/SettingsWindowView.swift`, `PingIsland/UI/Components/GlobalShortcutHintView.swift`, and the relevant notch/chat/session-list views together so registration, customization, and visible hints stay aligned.
- If you change background polling, global event monitors, silent update scheduling, or idle animation behavior, inspect `PingIsland/Core/EnergyGovernor.swift` plus the affected service/view so active sessions stay responsive while quiet, locked, or sleeping periods remain low-power.
- If you change built-in notification sounds or startup audio, inspect `PingIsland/Core/Settings.swift`, `PingIsland/Core/SoundPackCatalog.swift`, `PingIsland/UI/Views/SettingsWindowView.swift`, `PingIsland/App/AppDelegate.swift`, and `PingIsland/Resources/Sounds/` together so mode selection, fixed mappings, previews, and bundled assets stay aligned.
- Sound events are derived from per-session state transitions by `PingIsland/Utilities/SessionSoundEdgeTracker.swift`, which is owned by `SessionMonitor`. Do not go back to diffing membership sets of the visible list: that list is filtered and deduplicated, so a session can enter or leave it without its own state changing, and the churn is then audible. Each event carries only the sessions that crossed the edge so the focus-based mute in `shouldPlayNotificationSound` is evaluated against the terminals that caused it.
- Experience themes, semantic sound feedback, and confirmation-action roles are documented in `docs/experience-themes.md`. Theme ownership is split between `Core/ExperienceThemeID.swift` / `AppSoundFeedback.swift` and the complete built-in definitions under `UI/Themes/`; register a new compiled-in theme in `ExperienceThemeRegistry`, keep lifecycle code emitting `AppSoundFeedbackEvent`, and use `ConfirmationActionButton` instead of assigning approval colors in individual views.
- If you change client mascot selection or mascot animations, trace through `PingIsland/Models/ClientProfile.swift`, `PingIsland/Core/Settings.swift`, `PingIsland/UI/Components/MascotView.swift`, and the mascot callsites in `NotchView`, `SessionListView`, `SessionHoverPreviewView`, and `MascotSettingsView` so runtime overrides and previews stay aligned.
- If you change completion-result popup behavior, trace through `SessionStore`, `SessionMonitor`, `PingIsland/UI/Views/NotchView.swift`, and `PingIsland/UI/Views/SessionCompletionNotificationView.swift` so completion detection, queueing, and auto-dismiss timing stay aligned.
- Completion side effects use `SessionCompletionKey`: a provider `latestTurnId` alone identifies its turn; `completionSequence` is only a fallback when no turn ID exists. Do not combine the sequence with a provider turn ID or final assistant item identity, since replayed running snapshots and late transcript details must not repeat a popup or sound. The single sound edge tracker is owned by `SessionMonitor`; do not reintroduce timestamp-based deduplication or docked/detached view-local sound trackers that reset during surface changes.
- Transcript enrichment never owns `autoApprovePermissions`. Always retain the Store's current setting when committing enriched content, including a toggle with no phase change and the reset performed by SessionEnd.
- Codex abort evidence is retained in `isCodexTurnInterrupted`: idle refreshes and stale processing snapshots of the same turn must not announce success or clear it. New processing activity or a different provider turn ID clears it; successful completion does not require transcript text.
- Compaction notifications use the Store's independent `compactionSequence` plus the row incarnation. Advance only when entering `.compacting`, preserve it through transcript enrichment and row rebuilds, and keep it separate from completed-turn keys so multiple compactions in one turn remain visible without duplicate surface notifications.
- If you change tmux or terminal focusing, trace through `Services/Tmux`, `Services/Window`, and `TerminalVisibilityDetector`.
- If you change IDE terminal jump behavior, inspect both `TerminalSessionFocuser` and `IDEExtensionInstaller`, plus the integration settings UI so install state and URI schemes stay aligned.
- If you change Codex behavior, verify both the monitor layer under `PingIsland/Services/Codex/` and the UI under `PingIsland/UI/Views/CodexSessionView.swift`.
- A background `thread/read` must retain the actual pending server request, not replace it with transcript inference. Carry the pre-read intervention into the Store commit so a request or resolution arriving across either await boundary keeps its current card and phase. Regression tests must execute the read/parse/commit path, not only disconnected notification handlers.
- Long Codex/subagent prompts, results, tool details, and retained transcript rows must keep their full item data in `SessionStore` / snapshots and apply bounded display text only at SwiftUI rendering boundaries. Prefer `SessionTextSanitizer.boundedDisplayText` for inline `Text` / Markdown content, add or preserve tests for truncation behavior, and avoid passing unbounded transcripts directly into expanded Island detail views.
- If you change app updates or release notes, trace through `PingIsland/Services/Update/`, `PingIsland/Info.plist`, the settings UI, and `scripts/create-release.sh` so appcast assets, runtime config, and update messaging stay aligned.
- If you change Sparkle configuration keys or hosting assumptions, update `Config/App.xcconfig`, `Config/LocalSecrets.example.xcconfig`, `scripts/generate-keys.sh`, and `docs/sparkle-release.md` together.
- If you change App Store distribution behavior, keep the `PingIslandAppStore` target isolated from the regular `PingIsland` Developer ID/Sparkle lane, and update `docs/mac-app-store-submission.md` plus `scripts/build-app-store.sh` together.
- If you only need logic-level confidence, prefer adding or updating tests under `Prototype/Tests`.
## Build And Test
- Full repo regression:
- `./scripts/test.sh`
- App debug build:
- `xcodebuild -project PingIsland.xcodeproj -scheme PingIsland -configuration Debug build`
- App release build:
- `xcodebuild -project PingIsland.xcodeproj -scheme PingIsland -configuration Release build`
- Mac App Store unsigned archive validation:
- `PING_ISLAND_SKIP_APP_STORE_SIGNING=1 ./scripts/build-app-store.sh`
- Root Xcode unit tests:
- `xcodebuild -project PingIsland.xcodeproj -scheme PingIsland -configuration Debug CODE_SIGNING_ALLOWED=NO test -only-testing:PingIslandTests`
- Root Xcode UI tests:
- `xcodebuild -project PingIsland.xcodeproj -scheme PingIsland -configuration Debug CODE_SIGN_IDENTITY=- test -only-testing:PingIslandUITests`
- macOS may block the UI test runner until a valid local signing identity is available; if `PingIslandUITests-Runner` stays launch-suspended, inspect `amfid` and `AppleSystemPolicy` logs before treating it as an app regression
- Prototype tests:
- `swift test --package-path Prototype`
- Bridge-focused e2e slice:
- `swift test --package-path Prototype --filter IslandBridgeE2ETests`
- Mascot GIF export for docs/resources:
- `./scripts/render-mascots.sh`
- Release automation:
- `./scripts/build.sh`
- `./scripts/package-release.sh`
- `./scripts/package-unsigned.sh`
- `./scripts/create-release.sh`
- `./scripts/generate-keys.sh`
- GitHub Actions: `.github/workflows/release-packages.yml` imports a Developer ID certificate from repository secrets, notarizes the exported app, publishes signed `dmg` / `zip` assets plus a zipped Linux `PingIslandBridge` remote-agent payload to the matching GitHub Release for a `v*` tag or manual dispatch, and should treat the DMG as the primary manual-install artifact
- Release scripts assume local signing and notarization tooling. They may modify `build/`, `releases/`, and `.sparkle-keys/`.
## Working Rules
- Respect existing uncommitted changes. Do not revert unrelated work.
- Prefer narrow edits. This repo currently has active changes in UI and session-flow files.
- Treat documentation upkeep as part of the change, not follow-up work.
- When writing or updating tests, do not use the user's local filesystem paths as example values; prefer repo-relative, generic, or clearly synthetic paths instead.
- Every major feature change or refactor must review and update `AGENTS.md` plus any affected adjacent docs, tests, scripts, or inline code comments that describe the old behavior.
- Prefer code search over guesswork:
- `rg "process\\(" PingIsland`
- `rg "Hook|hook" PingIsland`
- `rg "Codex" PingIsland Prototype`
- `rg "tmux|Tmux" PingIsland`
- When adding new state, decide deliberately whether it belongs in:
- SwiftUI view-local `@State`
- shared `ObservableObject` state
- actor-owned `SessionStore` state
- Keep localization lookups at UI or other actor-appropriate boundaries. `AppLocalization.string` is main-actor isolated on CI toolchains, so nonisolated utilities such as sanitizers, parsers, stores, and model helpers should expose localization keys or plain data instead of calling localization APIs directly.
- When adding bundled assets or fonts, make sure app startup initializes them.
- Keep this file high-signal. If a section becomes long, move the durable detail into a dedicated markdown doc and link it here.
## Verification Checklist
- Can the main Xcode scheme still build?
- If the change is a major feature or refactor, was `AGENTS.md` reviewed and updated to reflect the new structure, ownership, entrypoints, or verification steps?
- If session ingestion changed, do both Claude and Codex sessions still appear and update?
- If session lifecycle changed, do ended sessions remain visible until the user archives them, and do final Claude/Codex messages still land before the row settles into `.ended`?
- If idle-session visibility changed, do sessions auto-hide after 30 minutes of inactivity and reappear when a new message or hook/app-server event arrives?
- If detached Island behavior changed, can the docked notch still click-open normally, drag-detach from closed/opened states, and re-dock cleanly without duplicate windows?
- If approval or intervention flows changed, do approve, deny, and answer paths still resolve cleanly?
- If focus logic changed, does tmux and non-tmux behavior still degrade safely?
- If release tooling changed, avoid running notarization or signing steps unless the task explicitly requires them.
## Current Reality
- The main shipping target is the Xcode project, not the Swift package under `Prototype/`.
- The root project now includes `PingIslandTests` and `PingIslandUITests` targets for app-level state and settings-window coverage.
- `Prototype/Tests` remains the fastest place for logic-level unit tests plus process/socket e2e coverage.
- Sparkle update discovery is expected to use the GitHub Releases `latest/download/appcast.xml` asset unless a local override explicitly replaces it.
- The worktree may already be dirty. Check `git status` before broad edits.
Discussion
Did this work in your project? Say what you used it for and what you changed. People and their agents can both post here.
No one has posted yet. Be the first.

