Skip to content

feat: add adaptive appearance and Codex task status - #67

Closed
bbnotcode wants to merge 5 commits into
ericjypark:mainfrom
bbnotcode:codex/adaptive-appearance-and-task-status
Closed

bbnotcode wants to merge 5 commits into
ericjypark:mainfrom
bbnotcode:codex/adaptive-appearance-and-task-status

Conversation

@bbnotcode

@bbnotcode bbnotcode commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add System, Light, and Dark appearance options to the custom settings window
  • add larger page indicators, persistent keyboard hints, Command-click style cycling, and ⌘1/⌘2/⌘3 plus arrow-key navigation
  • reuse the hidden Claude column as an optional local Codex task-status view
  • distinguish five task states: running, waiting for approval, waiting for input, idle, and error
  • aggregate multiple Codex tasks by urgency and deep-link to the selected task
  • improve Sparkle signing reliability by clearing extended attributes before codesigning
  • document the feature and its design rationale in English and Simplified Chinese

Why / Design rationale

Not every user subscribes to both Claude and Codex. When someone uses only Codex and hides Claude, leaving half of the island empty wastes its most glanceable space.

This change turns that freed column into an optional, privacy-preserving Codex companion. It reads only local lifecycle events from ~/.codex/sessions/**/*.jsonl and reduces them to five states; it does not display prompts, commands, or output, and it does not modify Codex configuration. The compact status group mirrors the Codex quota group, while the expanded view fills the otherwise unused provider column. The original per-model breakdown remains available when the feature is disabled.

设计动机

并不是每位用户都会同时订阅 Claude 和 Codex。只使用 Codex 并隐藏 Claude 时,如果左半区一直留空,灵动岛中最适合随时查看的一块空间就没有得到利用。

因此,这次改动把空出的栏位设计成可选的 Codex 任务状态视图。它只读取 ~/.codex/sessions/**/*.jsonl 中的本地生命周期事件,并归纳为运行中、等待审批、等待用户输入、空闲和异常五种状态;不会展示提示词、命令或输出,也不会修改 Codex 配置。收起状态与右侧额度信息采用镜像布局,展开后填充原本空置的服务商栏位。关闭此功能后,仍会恢复原有的按模型 Token 用量视图。

Validation

  • swiftc -frontend -parse passed for all Swift sources
  • English and Simplified Chinese Localizable.strings passed plutil -lint
  • git diff --check passed
  • ./scripts/run-tests.sh could not run in this local environment because the installed Swift compiler is 6.3.3 while the Command Line Tools SDK Swift interfaces are 6.3.2

Summary by CodeRabbit

  • New Features
    • Added Appearance selection (System/Light/Dark) with persisted preference and dynamic window theming.
    • Introduced “Codex task status” in Usage/Providers, with compact/expanded display, icon-only vs icon+text, “Updated” timestamps, and one-click task opening.
    • Improved shortcut/keyboard guidance and screen navigation controls.
  • Bug Fixes
    • Improved visual consistency across the app with adaptive primary-based styling.
  • Documentation
    • Updated English and Simplified Chinese docs covering local Codex task status behavior and display options.
  • Tests / Chores
    • Refactored incremental log parsing with new automated test coverage and added a dedicated test run step.
    • Updated packaging codesigning preparation to avoid attribute issues.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds persisted appearance selection, adaptive settings styling, local Codex task-state parsing and display, navigation controls, localization and documentation updates, parser tests, and Sparkle signing cleanup.

Changes

Codex status and appearance

Layer / File(s) Summary
Appearance persistence and adaptive settings
Sources/Model/AppearanceStore.swift, Sources/Theme/Colors.swift, Sources/Views/Settings*, Sources/Views/SettingsView.swift
Adds system/light/dark appearance persistence, dynamic window color-scheme updates, appearance settings, and adaptive settings styling.
Codex task log parsing
Sources/Model/CodexTaskStatusLogParser.swift, Sources/Model/CodexTaskStatusStore.swift, Tests/CodexTaskStatusLogParserTests.swift, scripts/run-tests.sh
Parses recent Codex JSONL lifecycle events into task states, delegates store parsing to the new parser, and adds focused parser tests to the test script.
Codex status settings and presentation
Sources/App.swift, Sources/Views/CodexTaskStatusView.swift, Sources/Views/IslandRootView.swift, Sources/Views/PanelHeader.swift, Sources/Views/UsageView.swift, Resources/*/Localizable.strings
Starts status tracking and adds status controls, glyphs, labels, timestamps, overlays, provider titles, usage-panel switching, accessibility text, and localizations.
Navigation and shortcut controls
Sources/Views/PageIndicator.swift, Sources/Views/PanelFooter.swift, Sources/Window/IslandWindowController.swift
Updates page indicator buttons, shortcut hints, screen selection, arrow navigation, and centralized key handling.
Feature documentation
README.md, README.zh-CN.md
Documents appearance controls, local Codex status inference, displayed states, privacy boundaries, task opening, and compact or expanded layouts.

Packaging signing preparation

Layer / File(s) Summary
Sparkle signing cleanup
build.sh
Clears extended attributes from Sparkle XPC services and the framework before codesigning.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant SettingsView
  participant CodexTaskStatusStore
  participant CodexTaskStatusLogParser
  participant CodexTaskStatusView
  participant CodexApp
  User->>SettingsView: Enable Codex task status
  SettingsView->>CodexTaskStatusStore: Persist status preferences
  CodexTaskStatusStore->>CodexTaskStatusLogParser: Parse recent JSONL events
  CodexTaskStatusLogParser-->>CodexTaskStatusStore: Return task state
  CodexTaskStatusStore-->>CodexTaskStatusView: Publish task snapshot
  User->>CodexTaskStatusView: Select status card
  CodexTaskStatusView->>CodexTaskStatusStore: openThread()
  CodexTaskStatusStore->>CodexApp: Open Codex thread
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately highlights the main changes: adaptive appearance settings and the Codex task status feature.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@bbnotcode
bbnotcode marked this pull request as ready for review July 29, 2026 16:02

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 982ae9ec87

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +234 to +235
let maxBytes: UInt64 = 512 * 1024
try? handle.seek(toOffset: length > maxBytes ? length - maxBytes : 0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retain lifecycle state beyond the tail window

For a single long-running turn whose rollout grows beyond 512 KiB, this seek discards the initial task_started/user_message events; if the retained tail contains only token-count, reasoning, or response_item records, parseState starts from .idle and never changes it, so an active task is reported as idle. Preserve the last lifecycle state separately or include the relevant initial events when parsing a truncated file.

Useful? React with 👍 / 👎.

Comment on lines +100 to +101
timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { [weak self] _ in
Task { @MainActor in self?.refresh() }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid rescanning every rollout every two seconds

With the setting enabled by default, this timer runs even when Claude is visible and the task-status UI cannot be shown; every tick recursively enumerates the entire sessions tree and reparses as much as 24 × 512 KiB of unchanged JSONL data. Established Codex installations therefore incur continuous filesystem and JSON-processing work for a hidden feature, so polling should be gated by visibility/file changes or cache unchanged modification dates.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
Sources/Views/PanelFooter.swift (1)

74-84: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Command-key hint may not read clearly via VoiceOver.

Image(systemName: "command") typically has no built-in accessibility label, so combining it with the plain-text hint under .accessibilityElement(children: .combine) may drop the "Command" cue for VoiceOver users, reading as e.g. "click cycle view" without conveying the modifier key. Consider an explicit .accessibilityLabel on the combined element (e.g. "Command + (keys), (label)").

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Views/PanelFooter.swift` around lines 74 - 84, The combined
accessibility element in shortcutHint must explicitly convey the Command
modifier, since the command icon may be omitted by VoiceOver. Add an
accessibility label to the combined HStack that includes “Command,” the
localized keys text, and the localized label while preserving the existing
visual layout.
Sources/Window/IslandWindowController.swift (1)

160-165: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Hardcoded ["1","2","3"] is only safe while ScreenPref.Screen has exactly 3 cases.

ScreenPref.Screen.allCases[index] will crash if the enum's case count ever diverges from this literal array (e.g. a 4th screen added later without updating this line). Currently safe since there are exactly 3 cases, but bounding the index (or deriving the digit set from allCases.count) would make this refactor-proof.

🛡️ Illustrative guard
         if modifiers == .command,
            let character = event.charactersIgnoringModifiers,
-           let index = ["1", "2", "3"].firstIndex(of: character) {
+           let index = ["1", "2", "3"].firstIndex(of: character),
+           index < ScreenPref.Screen.allCases.count {
             model.showScreen(ScreenPref.Screen.allCases[index])
             return nil
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Window/IslandWindowController.swift` around lines 160 - 165, Update
the command-number handling in the event method around
ScreenPref.Screen.allCases so the parsed digit index is validated against
allCases.count before indexing. Preserve the existing behavior for valid screen
shortcuts and return without indexing when the digit does not correspond to an
available screen.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/Model/CodexTaskStatusStore.swift`:
- Around line 97-103: Reduce the continuous scan cost driven by start() and its
timer callback: replace the 2-second polling cadence with a substantially
longer, reasonable refresh interval and narrow recentRolloutFiles() so it avoids
recursively enumerating the entire sessions tree, or trigger refresh only on
filesystem changes using an appropriate watcher. Preserve the existing refresh
behavior while ensuring scan() and its tail-reads are not repeatedly executed
when no session files changed.
- Around line 105-126: Keep the existing com.openai.codex bundle identifier in
openCodexApp, but replace the silent failure when urlForApplication returns nil
or opening the app fails with a user-visible fallback or lightweight
notification using clear wording. Preserve the current app-opening behavior when
the application is installed.

In `@Sources/Views/IslandRootView.swift`:
- Around line 464-466: Update the elapsedUpdate label in IslandRootView to
refresh periodically while visible by driving its Date-dependent value from a
SwiftUI timeline or equivalent periodic view update. Preserve the existing
missing-date placeholder and Duration.compact formatting, and scope the refresh
to the visible peek rather than introducing a global timer.

In `@Sources/Views/Settings/StyleTile.swift`:
- Around line 21-23: Update the selected-state branch of the foregroundStyle
expression in StyleTile to use an adaptive semantic foreground color instead of
the fixed pale-blue Color value. Preserve the unselected styling and existing
selection visuals while ensuring selected text remains readable in Light mode.

---

Nitpick comments:
In `@Sources/Views/PanelFooter.swift`:
- Around line 74-84: The combined accessibility element in shortcutHint must
explicitly convey the Command modifier, since the command icon may be omitted by
VoiceOver. Add an accessibility label to the combined HStack that includes
“Command,” the localized keys text, and the localized label while preserving the
existing visual layout.

In `@Sources/Window/IslandWindowController.swift`:
- Around line 160-165: Update the command-number handling in the event method
around ScreenPref.Screen.allCases so the parsed digit index is validated against
allCases.count before indexing. Preserve the existing behavior for valid screen
shortcuts and return without indexing when the digit does not correspond to an
available screen.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 40d2f099-0819-4f91-88de-78c549cfafe8

📥 Commits

Reviewing files that changed from the base of the PR and between 3b78b69 and 982ae9e.

📒 Files selected for processing (26)
  • README.md
  • README.zh-CN.md
  • Resources/en.lproj/Localizable.strings
  • Resources/zh-Hans.lproj/Localizable.strings
  • Sources/App.swift
  • Sources/Model/AppearanceStore.swift
  • Sources/Model/CodexTaskStatusStore.swift
  • Sources/Theme/Colors.swift
  • Sources/Views/CodexTaskStatusView.swift
  • Sources/Views/IslandRootView.swift
  • Sources/Views/PageIndicator.swift
  • Sources/Views/PanelFooter.swift
  • Sources/Views/PanelHeader.swift
  • Sources/Views/Settings/BrandHeader.swift
  • Sources/Views/Settings/ChartStylePicker.swift
  • Sources/Views/Settings/CostStylePicker.swift
  • Sources/Views/Settings/SegmentedControl.swift
  • Sources/Views/Settings/SettingsFooter.swift
  • Sources/Views/Settings/SettingsRow.swift
  • Sources/Views/Settings/SettingsToggle.swift
  • Sources/Views/Settings/StyleTile.swift
  • Sources/Views/SettingsView.swift
  • Sources/Views/SettingsWindowController.swift
  • Sources/Views/UsageView.swift
  • Sources/Window/IslandWindowController.swift
  • build.sh

Comment thread Sources/Model/CodexTaskStatusStore.swift
Comment thread Sources/Model/CodexTaskStatusStore.swift
Comment thread Sources/Views/IslandRootView.swift Outdated
Comment thread Sources/Views/Settings/StyleTile.swift

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Sources/Model/CodexTaskStatusStore.swift (1)

269-272: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve error status after task_complete.

parseState(at:) resets all status to idle for task_complete, so a sequence like error then task_complete reports a failed turn as idle. Track the current turn’s terminal failure state and keep .error on completion unless a new run starts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Model/CodexTaskStatusStore.swift` around lines 269 - 272, Update
parseState(at:) so task_complete preserves an existing .error status instead of
unconditionally setting .idle. Track the current turn’s terminal failure state
across error, stream_error, and turn_aborted events, and clear it only when a
new run starts.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@Sources/Model/CodexTaskStatusStore.swift`:
- Around line 269-272: Update parseState(at:) so task_complete preserves an
existing .error status instead of unconditionally setting .idle. Track the
current turn’s terminal failure state across error, stream_error, and
turn_aborted events, and clear it only when a new run starts.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9776230c-0151-464f-915e-34628729e608

📥 Commits

Reviewing files that changed from the base of the PR and between 982ae9e and 4183810.

📒 Files selected for processing (5)
  • Resources/en.lproj/Localizable.strings
  • Resources/zh-Hans.lproj/Localizable.strings
  • Sources/Model/CodexTaskStatusStore.swift
  • Sources/Views/IslandRootView.swift
  • Sources/Views/Settings/StyleTile.swift
🚧 Files skipped from review as they are similar to previous changes (3)
  • Resources/en.lproj/Localizable.strings
  • Resources/zh-Hans.lproj/Localizable.strings
  • Sources/Views/IslandRootView.swift

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Sources/Model/CodexTaskStatusStore.swift (1)

253-281: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Preserve terminal errors across the tail boundary.

parseState starts currentTurnFailed as false, but tailData(at:) can begin in the middle of a JSONL record or after the preceding error/turn_aborted event. If task_complete is present in the suffix, the task is then reported as .idle, losing the terminal error.

Read from a complete line boundary and ensure the full current turn—or an equivalent persisted terminal-error marker—is available before applying task_complete. Add a regression test for a failure immediately before or outside the 512 KiB window.

Also applies to: 295-301

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Model/CodexTaskStatusStore.swift` around lines 253 - 281, Update
parseState and its tailData(at:) input handling so parsing begins at a complete
JSONL line boundary and retains the current turn’s terminal failure state when
the preceding error, turn_aborted, or stream_error event falls outside the 512
KiB suffix. Ensure task_complete reports .error rather than .idle when that
persisted failure marker applies, and add a regression test covering a failure
immediately before or outside the tail window.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@Sources/Model/CodexTaskStatusStore.swift`:
- Around line 253-281: Update parseState and its tailData(at:) input handling so
parsing begins at a complete JSONL line boundary and retains the current turn’s
terminal failure state when the preceding error, turn_aborted, or stream_error
event falls outside the 512 KiB suffix. Ensure task_complete reports .error
rather than .idle when that persisted failure marker applies, and add a
regression test covering a failure immediately before or outside the tail
window.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b8c95d96-423b-4014-a39a-80a6c95175f2

📥 Commits

Reviewing files that changed from the base of the PR and between 4183810 and 24c99fd.

📒 Files selected for processing (1)
  • Sources/Model/CodexTaskStatusStore.swift

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
Tests/CodexTaskStatusLogParserTests.swift (1)

34-54: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the normal status transitions too.

These cases only validate preserved errors. Add table-driven assertions for running, approval, user-input, idle completion, and a new task_started/user_message clearing a prior error; otherwise regressions in the primary displayed states pass unnoticed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Tests/CodexTaskStatusLogParserTests.swift` around lines 34 - 54, Extend the
tests around CodexTaskStatusLogParser.parse with table-driven cases covering
running, approval, user-input, idle completion, and task_started/user_message
clearing a prior error. Keep the existing preserved-error cases, and assert each
scenario’s expected parser status so normal transitions and error recovery are
validated.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/Model/CodexTaskStatusLogParser.swift`:
- Around line 20-21: Bound CodexTaskStatusLogParser I/O to maxBytes: update
completeLineStart and failureMarker so oversized JSONL records and
failure-marker lookups do not scan or reread unbounded session history. Preserve
failure state incrementally by file/offset, or limit backward scanning to the
current turn’s reset/failure marker, and ensure the fallback path is capped at
the configured tail size.

---

Nitpick comments:
In `@Tests/CodexTaskStatusLogParserTests.swift`:
- Around line 34-54: Extend the tests around CodexTaskStatusLogParser.parse with
table-driven cases covering running, approval, user-input, idle completion, and
task_started/user_message clearing a prior error. Keep the existing
preserved-error cases, and assert each scenario’s expected parser status so
normal transitions and error recovery are validated.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 69713819-8688-4969-aa60-44dbdeffdb4d

📥 Commits

Reviewing files that changed from the base of the PR and between 24c99fd and bc544fe.

📒 Files selected for processing (4)
  • Sources/Model/CodexTaskStatusLogParser.swift
  • Sources/Model/CodexTaskStatusStore.swift
  • Tests/CodexTaskStatusLogParserTests.swift
  • scripts/run-tests.sh

Comment thread Sources/Model/CodexTaskStatusLogParser.swift Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/Model/CodexTaskStatusLogParser.swift`:
- Around line 38-83: Update parse(at:) to detect when completeLines returns no
data and zero consumedBytes because no complete line boundary was available on a
fresh, non-continuing read. Return nil instead of parsing and returning the
hardcoded initial .idle state; preserve cached-state continuation behavior and
normal parsing when complete data is available.
- Around line 15-36: Add a thread-safe removal method to StateCache for deleting
an entry by URL, then invoke it from the store whenever a session file is no
longer tracked, including rotation and task removal from the aggregation set.
Preserve the existing locking pattern used by entry(for:) and set(_:for:) to
keep cache access synchronized.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5ba4d6c7-80c7-4198-a49a-fa8e588516cf

📥 Commits

Reviewing files that changed from the base of the PR and between bc544fe and 89d136b.

📒 Files selected for processing (2)
  • Sources/Model/CodexTaskStatusLogParser.swift
  • Tests/CodexTaskStatusLogParserTests.swift

Comment on lines +15 to +36
private struct CacheEntry {
let offset: UInt64
let state: CodexTaskLogState
let currentTurnFailed: Bool
}

private final class StateCache: @unchecked Sendable {
private let lock = NSLock()
private var entries: [URL: CacheEntry] = [:]

func entry(for url: URL) -> CacheEntry? {
lock.lock()
defer { lock.unlock() }
return entries[url]
}

func set(_ entry: CacheEntry, for url: URL) {
lock.lock()
defer { lock.unlock() }
entries[url] = entry
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

StateCache has no eviction path — unbounded growth over the app's lifetime.

StateCache only exposes entry(for:)/set(_:for:); there is no way to remove an entry. Since Codex creates a new session .jsonl file per task, this dictionary grows for as long as the app runs, with no bound.

♻️ Proposed fix: expose a removal path for stale sessions
     private final class StateCache: `@unchecked` Sendable {
         private let lock = NSLock()
         private var entries: [URL: CacheEntry] = [:]
 
         func entry(for url: URL) -> CacheEntry? {
             lock.lock()
             defer { lock.unlock() }
             return entries[url]
         }
 
         func set(_ entry: CacheEntry, for url: URL) {
             lock.lock()
             defer { lock.unlock() }
             entries[url] = entry
         }
+
+        func remove(for url: URL) {
+            lock.lock()
+            defer { lock.unlock() }
+            entries.removeValue(forKey: url)
+        }
     }

Have the store call this when a session file is no longer tracked (rotated away, task removed from the aggregation set).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private struct CacheEntry {
let offset: UInt64
let state: CodexTaskLogState
let currentTurnFailed: Bool
}
private final class StateCache: @unchecked Sendable {
private let lock = NSLock()
private var entries: [URL: CacheEntry] = [:]
func entry(for url: URL) -> CacheEntry? {
lock.lock()
defer { lock.unlock() }
return entries[url]
}
func set(_ entry: CacheEntry, for url: URL) {
lock.lock()
defer { lock.unlock() }
entries[url] = entry
}
}
private struct CacheEntry {
let offset: UInt64
let state: CodexTaskLogState
let currentTurnFailed: Bool
}
private final class StateCache: `@unchecked` Sendable {
private let lock = NSLock()
private var entries: [URL: CacheEntry] = [:]
func entry(for url: URL) -> CacheEntry? {
lock.lock()
defer { lock.unlock() }
return entries[url]
}
func set(_ entry: CacheEntry, for url: URL) {
lock.lock()
defer { lock.unlock() }
entries[url] = entry
}
func remove(for url: URL) {
lock.lock()
defer { lock.unlock() }
entries.removeValue(forKey: url)
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Model/CodexTaskStatusLogParser.swift` around lines 15 - 36, Add a
thread-safe removal method to StateCache for deleting an entry by URL, then
invoke it from the store whenever a session file is no longer tracked, including
rotation and task removal from the aggregation set. Preserve the existing
locking pattern used by entry(for:) and set(_:for:) to keep cache access
synchronized.

Comment on lines +38 to +83
static func parse(at url: URL, maxBytes: UInt64 = 512 * 1024) -> CodexTaskLogState? {
guard maxBytes > 0,
let handle = try? FileHandle(forReadingFrom: url)
else { return nil }
defer { try? handle.close() }

let length = (try? handle.seekToEnd()) ?? 0
let cached = cache.entry(for: url)
let canContinue = cached.map {
length >= $0.offset && length - $0.offset <= maxBytes
} ?? false
let readStart: UInt64
let initialState: CodexTaskLogState
let initialFailure: Bool
if canContinue, let cached {
readStart = cached.offset
initialState = cached.state
initialFailure = cached.currentTurnFailed
} else {
readStart = length > maxBytes ? length - maxBytes : 0
initialState = .idle
initialFailure = false
}

try? handle.seek(toOffset: readStart)
let readLimit = Int(min(maxBytes, UInt64(Int.max)))
guard let raw = try? handle.read(upToCount: readLimit) else { return nil }
let complete = completeLines(
in: raw,
droppingLeadingPartialLine: !canContinue && readStart > 0
)
let result = parse(
complete.data,
initialState: initialState,
currentTurnFailed: initialFailure
)
cache.set(
CacheEntry(
offset: readStart + UInt64(complete.consumedBytes),
state: result.state,
currentTurnFailed: result.currentTurnFailed
),
for: url
)
return result.state
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Oversized tail-window record can silently report a stale .idle state.

When the tail read (raw, bounded by maxBytes) is dominated by a single oversized JSONL record such that the window contains only that record's own terminating newline, completeLines hits the lastNewline >= lowerBound guard failure and returns (Data(), 0). parse(at:) then returns initialState unchanged, which on a fresh (non-continuing) read is hardcoded to .idle — even if the true state is .running, .waitingApproval, or .error. This self-heals once the file grows past the record, but until then the UI can show a misleading idle status. This is a residual gap in the oversized-record handling that the earlier review flagged (I/O is now bounded, but correctness for this specific case is not).

Consider returning nil (unknown/no-change) instead of defaulting to .idle when no complete line boundary could be found and there's no prior cached state to preserve, so callers keep the last known state rather than showing a fabricated idle.

🐛 Sketch of the fix
         let complete = completeLines(
             in: raw,
             droppingLeadingPartialLine: !canContinue && readStart > 0
         )
+        guard canContinue || complete.consumedBytes > 0 else {
+            // Tail window is entirely consumed by one oversized record with
+            // no other line boundary; report unknown rather than a
+            // fabricated `.idle` until the window advances past it.
+            return nil
+        }
         let result = parse(

Also applies to: 85-106

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Model/CodexTaskStatusLogParser.swift` around lines 38 - 83, Update
parse(at:) to detect when completeLines returns no data and zero consumedBytes
because no complete line boundary was available on a fresh, non-continuing read.
Return nil instead of parsing and returning the hardcoded initial .idle state;
preserve cached-state continuation behavior and normal parsing when complete
data is available.

@ericjypark ericjypark left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this contribution — there's real care in it. The appearance work is thorough (the .whiteColor.primary conversion is complete across every settings surface, verified), the parser ships with tests, you responded to four rounds of bot feedback, and the bilingual docs are appreciated. The full-source typecheck and the test suite both pass on my machine, so the toolchain issue on your side didn't leak into the code.

That said, I can't merge this as one PR. It bundles four independent changes, and one of them needs a design-level conversation. Could you split it into separate PRs?

  1. Appearance (System/Light/Dark)AppearanceStore, the Color.primary conversion, SettingsWindowController. This is close to mergeable as-is.
  2. Keyboard navigationIslandWindowController, PageIndicator, PanelFooter. One fix needed (below), plus one product question.
  3. build.sh xattr fix — 4 lines, mergeable immediately with a one-flag hardening (below).
  4. Codex task status — the big one; needs iteration before it's mergeable.

Splitting lets 1–3 land quickly instead of being held hostage by 4.


Blocking issues on the Codex task status feature

The event vocabulary doesn't match what current Codex writes to rollouts. I checked two months of real ~/.codex/sessions rollouts on a current CLI install (240+ turns): mcp_tool_call_end appears 667×, patch_apply_end 173×, but exec_command_begin, apply_patch_begin, mcp_tool_call_begin, exec_approval_request, apply_patch_approval_request, request_user_input, elicitation_request, and stream_error appear zero times — current rollouts record *_end lifecycle events, not *_begin, and no approval/input-request events at all. That means waitingApproval and waitingUserInput — the feature's two highest-priority states and its main pitch — can never fire, and the five-state design collapses to running/idle/error. Which Codex version emitted the events CodexTaskStatusLogParser listens for? If they're from an older format (or the app-server protocol rather than rollout files), the parser needs to be rebuilt around the vocabulary that rollouts actually contain, and the README claims adjusted to match.

A cancelled task pins the global status for up to 24h. turn_aborted (plain Esc — 6 occurrences in my logs) marks a file .error, and the aggregator ranks error (3) above running (2) with recency only as a tie-break (CodexTaskStatusStore.swift:49, :181). One cancelled session from this morning hides every currently-running session behind a red error glyph, and clicking deep-links to the dead thread. Completed-with-error files need to decay or rank below live running work.

Click behavior in compact/peek hijacks the island. The overlay is a Button, so with Claude hidden, clicking the left side of the island no longer expands the panel — it deep-links to the Codex app. For CLI-only users without com.openai.codex installed, every click produces a modal NSAlert (CodexTaskStatusStore.swift:135) — and the users whose rollouts this feature parses are exactly CLI users. Suggest: plain click keeps expanding the island everywhere; the deep link lives only on the expanded card.

The poller runs 24/7 for users who can never see it. start() is unconditional (App.swift:54), enabled defaults to true, and both providers default visible — but all three render surfaces require !claudeVisible && codexVisible. Default installs pay directory enumeration + up to 24 file stats every 15 s forever with zero render path, in an app that lives in the notch on battery. Please gate the timer on renderability (enabled && !claudeVisible && codexVisible, subscribing to both stores, with an immediate refresh when it flips true) — and default the feature off so existing hide-Claude users don't have their per-model breakdown replaced by an update.

False idle under growth and failure. If a rollout grows >512 KiB between polls (multi-MB screenshot/tool records are normal — see the comment in CodexLogReader.swift:63), the cache is abandoned and the tail parse seeds .idle, showing a running task as idle. Separately, every failure mode (unreadable dir, wrong CODEX_HOME, schema drift) also collapses to idle. An explicit unknown/unavailable state would stop failures from masquerading as "all quiet".

Smaller items, take or leave: StateCache never evicts (mirror LogParseCache.walk's visited-set prune) and has no file-identity check (rollouts can be rewritten — thread_rolled_back exists in real logs); a byte prefilter before JSONSerialization (the CodexLogReader.swift:70 marker pattern) would skip the bulk response_item/token_count lines cheaply; the two-day directory window misses sessions started 2+ days ago but still active; the state-machine mappings and truncation path have no test coverage, and the store's private static methods make it untestable — the CODEX_HOME override is a ready-made seam.

Quick fixes on the other PRs

  • Keyboard: the new exact-match modifier check breaks ⌘Q with Caps Lock on (IslandWindowController.swift:152) — and this app has no menu bar, so that's the only quit path. You already subtract .capsLock for the arrow keys; do the same before the ⌘ comparisons. (Same fix makes ⌘1/2/3 survive layouts that shift digits.) Product note: replacing the one-shot "⌘-click to cycle" hint with permanent footer hints changes a deliberate quiet-chrome design — let's discuss in the split PR. hasCycledStyle also becomes write-only after this change; either remove it or keep the one-shot behavior.
  • build.sh: xattr -c follows symlinks; use xattr -cs (or find -type f) so a vendored tree can never redirect the sweep outside the framework.
  • Localization: the "click" / "1 2 3" hint keys aren't in either .strings file, so zh users see raw English; "Icon" display mode currently only affects one label on the expanded card — peek and header always render text.

Happy to review the split PRs quickly — the appearance and build ones especially should be fast merges.

@bbnotcode

Copy link
Copy Markdown
Contributor Author

已按 review 建议将这个综合 PR 拆分为四个独立、可单独审阅的 PR:

任务状态 PR 特意保持为 Draft,便于继续讨论设计和数据语义。原 PR 现由以上四个 PR 取代,感谢详细 review。

Following the review, this combined PR has been split into four independently reviewable PRs:

The task-status PR intentionally remains a draft for further design and data-semantics discussion. This PR is superseded by the four PRs above. Thank you for the detailed review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants