Skip to content

fix(codex): write status_line when config.toml has no bare [tui] header - #116

Merged
aqua5230 merged 3 commits into
mainfrom
fix/codex-status-line-tui-subtable
Aug 27, 2026
Merged

fix(codex): write status_line when config.toml has no bare [tui] header#116
aqua5230 merged 3 commits into
mainfrom
fix/codex-status-line-tui-subtable

Conversation

@aqua5230

Copy link
Copy Markdown
Owner

The bug

usage setup printed ✓ Codex status_line 已配置 while leaving ~/.codex/config.toml byte-identical, and is_codex_setup() kept returning False afterwards. Nothing in the output said anything had gone wrong.

_setup_codex() chooses its branch from isinstance(parsed.get("tui"), dict). tomllib reports that as true for a config that only ever declares tui through a subtable:

[tui.model_availability_nux]
"gpt-5.6-sol" = 4

_insert_table_line() then searched the raw text for a [tui] header, found none, and returned the content untouched. _setup_codex() wrote the unchanged content and printed success anyway.

Not platform-specific — _setup_codex() through _insert_table_line() has no sys.platform branching and codex_home() resolves the same on both. It needs this particular config shape, which is why a machine whose config carries a bare [tui], or no tui at all, never sees it. [tui.model_availability_nux] is written by Codex itself, so the shape arrives on its own.

The fix

_insert_table_line() now handles both shapes:

  • a [tui.*] subtable → insert a [tui] table ahead of the first one
  • top-level dotted keys → append tui.status_line = [...] after the last top-level tui.* key and before the first table header, so the value cannot land in the wrong table

Appending a [tui] header in the dotted-key case would raise Cannot declare ('tui',) twice and leave an unparseable config — worse than the no-op it replaces, which is why that case gets dotted-key treatment rather than a new table.

Every mutating path now runs its candidate through tomllib.loads and falls back to the original text if it would not parse, so an unanticipated shape degrades to a no-op instead of corrupting the file. _setup_codex() compares against the original content and reports failure rather than success when nothing was written.

Verification

Seven config shapes plus this machine's real ~/.codex/config.toml, on Windows 10 with Python 3.13.15:

input result
[tui.model_availability_nux] subtable only status_line installed
bare [tui] table status_line installed
tui.animations = false status_line installed as dotted key
dotted key followed by [features] installed, lands in tui not features
[features] then [tui.*] subtable installed, both preserved
tui."my key" = 1 (quoted dotted key) no-op, file byte-identical, failure reported
dotted key with no trailing newline status_line installed
real ~/.codex/config.toml installed, model_availability_nux preserved

All eight parse as valid TOML afterwards. End-to-end, usage setup on this machine now leaves is_codex_setup() returning True.

Full suite 1525 passed, 23 skipped (up from 1519 — six new regression tests); ruff and mypy clean.

🤖 Generated with Claude Code

_setup_codex() picked its branch from `isinstance(parsed.get("tui"), dict)`,
which tomllib reports as true for a config that only ever declares `tui`
through a subtable such as `[tui.model_availability_nux]` or through top-level
dotted keys. _insert_table_line() then searched the raw text for a `[tui]`
header, found none, and returned the content untouched -- after which
_setup_codex() wrote the unchanged file and printed its success message. The
status line was never installed and is_codex_setup() kept returning False,
with nothing in the output to say so.

_insert_table_line() now handles both shapes: it inserts a `[tui]` table ahead
of the first `[tui.*]` subtable, and for dotted keys it appends
`tui.status_line = [...]` after the last top-level `tui.*` key and before the
first table header, so the value cannot land in the wrong table. Appending a
`[tui]` header in the dotted-key case would have produced "Cannot declare
('tui',) twice" and left the user with an unparseable config, which is worse
than the no-op it replaced.

Every mutating path now runs its candidate through tomllib.loads and falls
back to the original text if it would not parse, so an unanticipated shape --
a quoted dotted key like `tui."my key" = 1`, for one -- degrades to a no-op
rather than corrupting the file. _setup_codex() compares against the original
content and reports failure instead of success when nothing was written.

Verified on Windows against seven config shapes plus this machine's real
~/.codex/config.toml: all produce parseable TOML, six install the status line,
and the quoted-key shape is left byte-identical. Full suite 1525 passed,
23 skipped; ruff and mypy clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@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: f9aecf2838

ℹ️ 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 thread installer/setup_hook.py
if table is None:
return content
return content[: table.end()] + f"\n{line}" + content[table.end() :]
subtable = re.compile(rf"(?m)^[ \t]*\[{re.escape(name)}\.").search(content)

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 Ignore table-like lines inside multiline strings

When a valid config contains a real dotted tui setting plus a multiline string with a line such as [tui.example], this regex treats the string contents as a subtable and injects the new [tui] block into that string. The resulting document still parses, so validated() accepts it; _setup_codex() then overwrites the user's string and reports success even though the parsed tui.status_line remains absent. Detect the table from TOML syntax rather than raw lines, or at minimum verify the candidate's parsed status line before writing it.

Useful? React with 👍 / 👎.

The dotted-key branch added earlier in this PR writes `tui.status_line`
with no `[tui]` header, a shape `_remove_table_line()` and
`_replace_table_line()` both miss: `_find_table()` returns None, they
hand back the content unchanged, and `_unsetup_codex()` writes the
identical file while printing `setup_codex_removed`. Verified on macOS —
`is_codex_setup()` stayed True after `usage unsetup`, the same silent
no-op this PR exists to fix, moved into uninstall.

Both helpers now fall back to the top-level dotted key, validate every
candidate through `tomllib.loads` before returning it, and
`_unsetup_codex()` compares against the original content on both its
restore and remove paths so it cannot report success on an unchanged
file.

`_setup_codex()`'s no-write path printed `setup_codex_config_unreadable`
even though the config had read fine; the new
`setup_codex_config_unmodifiable` key names the real cause and covers
all three paths.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@aqua5230

Copy link
Copy Markdown
Owner Author

追加:反安裝路徑有同一個無聲失敗,本分支已補

在 macOS 複核這支 PR 時發現,dotted key 分支寫出的形狀讓 usage unsetup 壞掉,而且是這支 PR 讓它變得可達的。

config.toml 內容為 tui.animations = false 時:

  1. usage setup → 本 PR 的 dotted key 分支寫進 tui.status_line = [...],檔案裡仍然沒有 [tui] 標頭
  2. usage unsetup_remove_tui_status_line()_remove_table_line()_find_table(content, "tui")None → 原樣回傳 → _atomic_write_text() 寫回一模一樣的內容 → 印出 ✓ Codex status_line 已移除,但 is_codex_setup() 仍然是 True

實測輸出(unsetup 後檔案一個位元組都沒變):

✓ Codex status_line 已移除
tui.animations = false
tui.status_line = [
  "project",
  ...
]
is_codex_setup: True

就是這支 PR 要修掉的那類無聲失敗,換到了反安裝那邊。本 PR 之前 _setup_codex() 從不會產生「沒有標頭的 tui.status_line」,所以移除路徑碰不到。備份還原分支走 _replace_tui_status_line()_replace_table_line(),同一個 _find_table() 前提,一樣壞。

這次的修法(d8bf3bf)

  • _remove_table_line() / _replace_table_line() 找不到 [name] 標頭時,改處理頂層 dotted key,含多行陣列
  • 兩者都比照 _insert_table_line() 已有的作法,候選內容先過 tomllib.loads,不能解析就退回原文
  • _unsetup_codex() 的還原與移除兩條路徑,在 _atomic_write_text() 之前比對原文,沒變就印失敗、不印成功

順帶修正一個訊息

_setup_codex() 在「內容沒變」時印的是 setup_codex_config_unreadable(「⚠ 無法讀取 ~/.codex/config.toml」),但那條路徑上檔案是讀得好好的,真正讀不到會更早在 _read_codex_config() 就 return,原因講錯了。

新增 setup_codex_config_unmodifiable,五種語言(zh-TWzh-CNenjako)一次補齊,用在 setup 與 unsetup 三個「內容沒變」的失敗點。原本的 setup_codex_config_unreadable 保留給真正讀不到的路徑。

macOS 複核

原 PR 的七種形狀只在 Windows 10 驗過,這次在 macOS 重跑 setup → unsetup 完整往返:

輸入形狀 往返結果
tui.animations = false 回到原文,is_codex_setup() False
[tui.model_availability_nux] 子表 model_availability_nux 保住,False
bare [tui] False
dotted key + 後面接 [features] 回到原文,False
完全沒有 tui False
第三方 status_line + dotted key(備份還原) 還原成 ["third-party"],False
tui."my key" = 1 位元組不變,印 setup_codex_config_unmodifiable

每個案例結束後都通過 tomllib.loads。新增三個測試函式蓋住上述行為。

macOS 完整套件 1759 passed, 5 skippedruff check .mypy .(217 個檔案)、scripts/check_file_size.py 全綠。

範圍外,順手記一筆

installer/setup_hook.py:822_ensure_table_line() 全專案沒有任何呼叫者。它有跟這次修好的三個函式相同的 _find_table is None 盲點,而且沒有 tomllib 驗證 — 但叫不到,所以不是 bug,這支 PR 不動它。

🤖 Generated with Claude Code

@aqua5230
aqua5230 merged commit cd2ff46 into main Aug 27, 2026
7 checks passed
@aqua5230
aqua5230 deleted the fix/codex-status-line-tui-subtable branch August 27, 2026 17:43
SanHsien added a commit to SanHsien/agentdeck that referenced this pull request Aug 29, 2026
sync-points 的 tickets.reviewed_pr_through / reviewed_issue_through 早就寫在
docs/UPSTREAM.md 裡,但沒有任何程式讀它們——檢查器只比對分支的 last_reviewed。
那兩個面向不是「查過沒發現」,是根本沒查,而每週的排程報告長得跟查過一樣綠。
艦隊層級問題:24 個 fork 裡 21 個都這樣。

走本檔既有的 _request(urllib + GITHUB_TOKEN),不引 gh:commit 軸一直走 REST,
多一條行為不同的路是負債。workflow 那一步本來就給了 token。

ticket 不做 relevance 分流:commit 軸可以自動略過上游每天的 AI digest,因為那種
雜訊認得出來;但已審視編號以上的每一筆 ticket 都還要人讀。

測試抓到的實作陷阱:GitHub 的 issue 端點會把 PR 一起回傳(共用編號空間),過濾
要用「pull_request 欄位在不在」,不能用真假值——回傳空物件時 PR 會在兩個面向
都被算一次。

驗證:ruff / mypy(201 檔)全過、pytest 1543 passed(唯一失敗是 dev_check 已
記載的本機符號連結權限例外,CI 上不存在);實跑 has_updates=False,報告多出
兩節,分別停在 aqua5230#116aqua5230#92。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant