Skip to content

feat(gui): show the prompt text Codex really sends, as an ordered stack - #2681

Merged
lidge-jun merged 11 commits into
devfrom
codex/prompt-layers-realtext
Aug 26, 2026
Merged

lidge-jun merged 11 commits into
devfrom
codex/prompt-layers-realtext

Conversation

@lidge-jun

Copy link
Copy Markdown
Owner

Summary

The dialog used to say Codex "does not expose" a layer's text. That was wrong: Codex is open source and ships codex debug prompt-input. This layer reads it, so each row shows the bytes it actually costs and the dialog shows the real text rather than a description.

The list also becomes an ordered stack — the numbers are the true assembly indices, gaps included, because renumbering each group from one would show an order Codex does not use. Transition notices (realtime, model-switch) are split out: they announce a change rather than describe state.

Permissions layer dialog showing the real 307-byte prompt text

Stack (merge bottom-up):

# PR Layer Review focus
5 docs guide + 8 locales prose only
4 realtext ← you are here real prompt text, ordered stack probe correctness
3 #2680 custom layers, presets write races
2 #2679 Codex Set page rename + read path
1 #2678 /api/codex-prompt request policy

Depends on #2680. Review this PR's diff only.

Verification

  • bun test ./tests/codex-prompt-text-probe.test.ts ./tests/codex-prompt-route.test.ts
  • bun test ./gui/tests/codex-set-prompt-layers.test.tsx ./gui/tests/codex-set-stack.test.tsx

What the probe honestly does not cover, stated in code rather than implied:

  • base-instructions is absent — prompt_debug.rs returns prompt.input and discards base_instructions.
  • World-state sections are diff-rendered. add_section registers state for diffing; it does not emit text, so a section that renders nothing on a first turn is missing from the sample even though its layer exists. That is reported as not rendered, never as an error.
  • Only six tags are mapped, each read off live output. Tag names are not all snake_case — the rendered tag is permissions instructions, with a space, and a [a-z_]+ pattern silently skipped it. Layers with no verified tag say so instead of claiming silence.
  • An existing but empty file is a fourth state: reporting "sent nothing" would call the layer idle when the truth is the user's file is blank.

The last commit fixes a cascade bug found while screenshotting: .api-code sets white-space: pre and ties on specificity, so source order won and the body scrolled sideways one clipped line at a time. Its guard was driven red against the unchained selector before landing.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults.

The probe runs in CODEX_HOME, never a caller-supplied directory: a cwd parameter let an authenticated request read any readable folder's AGENTS.md. It is bounded in both time and bytes, and every failure degrades to unavailable rather than an error page.

@coderabbitai

coderabbitai Bot commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (2)
  • ^dev$
  • ^preview$

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 7e0f528e-f8b9-4c5e-953a-53310f823968

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@github-actions github-actions Bot added the intake: hygiene-blocked Deterministic PR hygiene checks failed label Aug 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Deterministic hygiene checks failed.

  • new_suppression — A new TypeScript, lint, formatter, or similar suppression was added. Fix the underlying issue or obtain suppression-approved. Paths: gui/src/components/codex-set/CustomLayerDialog.tsx.

@github-actions github-actions Bot added the enhancement New feature or request label Aug 26, 2026
@github-actions

github-actions Bot commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

✅ READY

  • all PR quality gates passed.

Hygiene

✅ Deterministic PR hygiene checks passed.

@github-actions
github-actions Bot marked this pull request as draft August 26, 2026 12:31

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

ℹ️ 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 +85 to +89
join(homedir(), ".codex/packages/standalone/current/bin/codex"),
join(homedir(), ".local/bin/codex"),
"/usr/local/bin/codex",
"/opt/homebrew/bin/codex",
];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Use the canonical Codex runtime resolver

When Codex is installed only through PATH, CODEX_CLI_PATH, the persisted runtime selection, or a Windows .cmd/.exe launcher, none of these hard-coded Unix paths resolves, so /api/codex-prompt/text always reports codex binary not found even though OpenCodex can launch Codex normally. Resolve the binary through resolveCodexRuntime and launch it with the existing platform-safe codexExecInvocation helper instead of maintaining a second incomplete installation search.

AGENTS.md reference: src/AGENTS.md:L15-L15

Useful? React with 👍 / 👎.

Comment on lines +77 to +78
if (lastIdRef.current !== null) {
draftsRef.current.set(lastIdRef.current, { title, body });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Include parked drafts in discard checks

After a user edits layer A and navigates to an untouched layer B, A's changes are stored only in draftsRef, while dirty compares the visible B fields with B's original values. Pressing Cancel or Escape on B therefore closes immediately without the discard confirmation and silently loses A's parked edits; track dirtiness for every parked draft and include it in requestClose.

Useful? React with 👍 / 👎.

Comment on lines +369 to +371
const res = await fetch(apiBase + "/api/codex-prompt/text");
const body = await res.json() as { ok: boolean; layers?: Record<string, { text: string | null; reason: string; bytes: number }> };
if (!cancelled) setLayerText(body);

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 Cancel obsolete prompt-text probes

When a configuration write completes while a prompt-text request is still pending, invalidation often sets layerText from null to the same null, so this effect is not cleaned up and its cancelled flag remains false. The pre-write probe can then land and publish stale text and byte counts beside the newly updated switches; abort or generation-tag every outstanding probe when invalidating, even when the state is already null.

AGENTS.md reference: gui/AGENTS.md:L9-L10

Useful? React with 👍 / 👎.

Comment on lines +361 to +364
useEffect(() => {
// Fetch on panel mount, not on first dialog open: size is what a user needs to
// DECIDE with, and a prompt-budget page that hides which layer costs 15 KB is
// asking them to guess.

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 Document the new dashboard prompt behavior

This commit adds user-visible prompt-text probing, state-versus-transition grouping, byte counts, and in-dialog custom-layer navigation, but no docs-site/ file is updated and the existing docs contain no prompt-layer guidance. Add documentation describing these controls and the probe's stated limitations so the dashboard behavior is not discoverable only from the UI and implementation comments.

AGENTS.md reference: gui/AGENTS.md:L36-L36

Useful? React with 👍 / 👎.

@lidge-jun
lidge-jun force-pushed the codex/prompt-layers-realtext branch from c076ee7 to b42fa4d Compare August 26, 2026 12:42
@github-actions github-actions Bot removed the intake: hygiene-blocked Deterministic PR hygiene checks failed label Aug 26, 2026
@github-actions
github-actions Bot marked this pull request as ready for review August 26, 2026 12:43
@lidge-jun
lidge-jun force-pushed the codex/prompt-layers-realtext branch 2 times, most recently from 319943c to c8de8a3 Compare August 26, 2026 13:00
@lidge-jun

Copy link
Copy Markdown
Owner Author

리뷰 · 우선순위 54 / 80

설명

이 풀 리퀘스트는 레이어 대화 상자에 Codex 가 실제로 보내는 글을 보여 줍니다. codex debug prompt-input 을 읽고, 목록을 조립 순서 스택으로 바꿉니다. realtime 과 model-switch 는 전환 알림으로 나눕니다. 베이스는 #2680 입니다. 지금 current dev HEAD 37e5dd5ec 에는 프로브 모듈이 없습니다.

새 모듈 src/codex/prompt-text-probe.ts 는 바이너리를 찾아 debug prompt-input 을 돌립니다. 시간은 15초, 출력은 8MiB 로 막습니다. 실패는 에러 페이지가 아니라 unavailable 입니다. 작업 디렉터리는 CODEX_HOME 만 씁니다. 호출자가 cwd 를 넣지 못합니다. 예전 cwd 인자는 인증된 요청이 아무 폴더의 AGENTS.md 를 읽게 했습니다. 그 구멍을 막은 것은 맞습니다.

태그 지도는 라이브 출력에서 확인한 것만 둡니다. permissions instructions 처럼 공백이 있는 태그도 잡습니다. 확인하지 못한 레이어는 not-exposed 로 말합니다. 빈 AGENTS.md 는 empty-source 입니다. base-instructions 는 이 경로로 못 읽습니다. 라우트는 GET /api/codex-prompt/text 를 추가합니다. 단위 테스트는 추출만 고정하고, spawn 자체는 라우트·수동에 맡깁니다.

초안이 아닙니다. exact-head CI 는 test 네 조각과 gates, react-doctor 가 실패이고 macos 는 진행 중이었습니다. 아래층이 빨간 스택 위에 있어서 이 층만의 신호로 보기 어렵습니다. types.ts/config.ts 는 건드리지 않습니다. 미리보기 배포는 계획에 없습니다.

프로브는 관리 API 요청마다 외부 바이너리를 띄울 수 있습니다. 시간·바이트 한도는 있지만, 동시 요청이 많으면 프로세스 부담이 됩니다. 캐시나 한 번만 돌리기 규칙은 이 층에 없습니다. PATH 에 있는 codex 는 후보 목록에 없고, 고정 경로 네 곳만 봅니다.

src/codex/prompt-text-probe.ts:178 - 프로브는 CODEX_HOME 만 쓰며 호출자 cwd 를 받지 않습니다
src/codex/prompt-text-probe.ts:83 - 바이너리 후보는 고정 네 경로뿐이라 PATH 의 codex 는 놓칩니다
src/server/management/codex-prompt-routes.ts - GET /api/codex-prompt/text 가 프로브를 붙입니다
tests/codex-prompt-text-probe.test.ts - 공백 태그·AGENTS.md 경계·잘못된 JSON 추출 검사는 맞습니다
경로/심볼 - exact-head CI 가 빨개서 지금 합치면 안 됩니다
경로/심볼 - #2678-#2680 이 먼저 합쳐지지 않으면 이 층만 올릴 수 없습니다

메인테이너의 판단이 필요한 지점

  • 프로브 결과를 짧은 시간 캐시할지, 요청마다 돌릴지 정해야 합니다
  • PATH 의 codex 를 후보에 넣을지 정해야 합니다
  • spawn 실패를 라우트 테스트로 고정할지, 추출 단위 테스트만으로 충분한지 정해야 합니다

너의 추천

지금 합치지 마세요. 아래층 CI 를 먼저 초록으로 만든 뒤 이 층을 다시 맞추세요. cwd 제거와 공백 태그·빈 파일 구분은 유지하세요. 합치기 전에 프로브 동시성이나 짧은 캐시를 한 줄로라도 문서에 적으세요. types.ts/config.ts 분할 캠페인에 무효화되어 닫을 대상이 아닙니다. 미리보기 배포는 계획에 없습니다.

이 댓글은 grok-bot이 작성했습니다

@lidge-jun
lidge-jun force-pushed the codex/prompt-layers-authoring branch from e95b478 to c28cd08 Compare August 26, 2026 13:14
@lidge-jun
lidge-jun force-pushed the codex/prompt-layers-realtext branch 2 times, most recently from 4ca216d to 861a1ce Compare August 26, 2026 13:20
lidge-jun and others added 11 commits August 26, 2026 22:26
… off

The dialog claimed Codex "does not expose the assembled text of a built-in layer".
That was simply wrong: Codex is open source and ships `codex debug prompt-input`,
which renders the model-visible input list as JSON. A new bounded probe reads it and
the dialog shows the actual bytes - 10,740 for skills, 15,354 for AGENTS.md on this
machine.

What is still absent is now named instead of blanket-denied. Three different causes,
three different sentences: the base prompt travels outside the printable list, a
diff-rendered section can legitimately send nothing on the sampled turn, and the
probe itself can fail. Saying which one applies is the difference between a limit
and a bug.

AGENTS.md needed special handling: it is not wrapped in a tag like the other
sections, it arrives as a plain "# AGENTS.md instructions for <path>" block, so tag
matching alone reported it as unrendered while its text sat in the same message.

The switch moved into the dialog as well. Opening a layer to read it and then having
to close the dialog to act on it is a dead end, and the row and the dialog are the
same control over the same key.

Rows also stop rendering a raw checkbox. They reached for a .switch class that
styles a different element; the dashboard's control is .toggle with a knob. Tests
follow the markup: aria-checked on a button, not .checked on an input.

The probe is lazy - it shells out, so a user who never opens a layer never pays for
it - and fail-soft.
This is a prompt-budget page, and it showed fifteen rows of identical visual
weight with no indication that Skills costs 10 KB while Environment context costs
278 bytes. Size now sits on the row, so a user deciding what to keep can see what
they are paying for instead of opening each layer to find out.

That moved the probe from first-dialog-open to panel mount. It costs one bounded
spawn on a page the user opened specifically to manage prompts.

Density follows the surface: this is an operator list of fifteen rows, not a
settings page with three, so row padding drops from 10px to 7px and rows that can
actually be acted on carry more type weight than rows that only report. The
research lane behind this (Figma, Photoshop, Zapier, Atlassian, W3C APG) agrees on
the same point for ordered stacks - order and weight must be visible, not inferred.

Test follows the new request shape: the panel now issues two fetches on mount, so
the cold-skeleton case holds only the snapshot open and lets the size probe answer
immediately. Holding both would have deadlocked rather than measured anything.
…rom a quiet layer

The probe took a cwd from the query string, so an authenticated request could read
any readable folder's AGENTS.md through the dashboard. It also described whichever
directory the caller named rather than the configuration this page claims to show.
It now runs in CODEX_HOME and nowhere else.

An empty file is not a quiet layer. A 0-byte ~/.codex/AGENTS.md was reported as
"sent nothing on the turn we read", which tells a user their layer is idle when the
real answer is that the file they wrote is blank. That is a fourth state now, and it
names the path so the user can go fill it in.

The tag pattern missed <permissions instructions> because it assumed snake_case and
that tag has a space in it. The layer had text the whole time and the panel said it
sent nothing. Verified against live output rather than the Rust section IDs, which
are identifiers and not the rendered tag.

Zero sections from a zero-exit probe is a failed read, not fifteen silent layers, so
it returns ok:false with a reason.

The probe is now bounded in bytes as well as time, settles exactly once, destroys
the stream on the way out, and decodes at the end - per-chunk String() corrupts any
UTF-8 character that straddles a chunk boundary.

Verified both directions by hand: 0 bytes reports empty-source with the path, and
18 bytes of real content reports ok with the text.
… failed

Measured text describes the configuration that produced it. Keeping it across a
toggle showed a layer's old body and old byte count beside a switch that now reads
off, so every write path invalidates it.

Five inventory ids had no tag mapping and were simply absent from the probe result.
The dialog read that absence as "unavailable", which claims the probe failed when it
succeeded. They are listed explicitly as not-exposed now: having no mapping is a
smaller and truer claim than having no answer.

Two route tests pin the boundaries that were wrong an hour ago: /text takes no
caller-supplied directory, and the probe is bounded in bytes as well as in time with
a single settlement path and one decode at the end.
….md body

Three of the tag mappings were guesses, and guesses in this table are lies to the
user: a wrong name reports a layer as having sent nothing while its text sits in the
output. I dumped the rendered tags across the feature flags that enable each section
and kept only what appeared - skills, apps, plugins, environment, permissions, and
the untagged project doc.

personality, realtime and collaboration produced no tag under their own flags in this
environment, so they join the unsupported list rather than claiming silence. Fifteen
layers, and every one of them now says something it can back up.

The AGENTS.md capture ran to end-of-message after deleting every tag-shaped block,
which swept up unrelated trailing prose AND deleted XML-like text the user had
written inside their own file. Codex wraps the body in <INSTRUCTIONS>, so that is
the boundary. Verified with a doc containing <angle> brackets: they survive now and
the following section stays its own entry.

The block is concatenated straight onto the previous closing tag, so the line anchor
I first wrote never matched at all.
… state

Three asks in one panel.

Order was already correct and completely invisible: LAYER_INVENTORY carries it and
the panel sorted by it, but a reader had to infer "this is a stack" from vertical
position. Rows carry their assembly index now.

The index is CANONICAL, gaps included. Lifting the transition notices out leaves 2
and 5 missing from the main list, and that is the point - renumbering each group
1..n would invent an order the runtime does not have. Built-ins get no drag handle
for the same reason: their order comes from world_state.rs and a handle would
promise a reordering nothing honors.

Custom layers are numbered among THEMSELVES rather than continuing the sequence.
They concatenate into one developer_instructions section, so sharing a sequence with
the built-ins would draw a fifteen-plus-n stack that does not exist.

The live-only split is by KIND, not by a scope flag. realtime and model-switch are
notices about a change; they have no steady state to describe. Everything else
describes configuration or context. Note what the split is NOT: "every turn" versus
"sometimes" - sections are diff-rendered, so an unchanged layer of either kind sends
nothing. Getting that wrong is what the last two review rounds were about.

They keep the locked row shape but not the locked LABEL: "Always on" is false for a
notice. It is not on, it fires.

Preset switching lands as asked, with the one addition every reference
implementation insists on. No shipped product cycles presets with bare arrows; VS
Code Profiles, Arc Boosts and Cursor Rules all pair movement with an explicit
current item, and arrows without a position lose the user. So prev/next plus "2 / 3".

Navigation moves the EDIT TARGET only. Custom layers are not mutually exclusive -
every enabled one composes together - so moving between them must never toggle which
is active.

Unsaved edits are parked per layer while navigating. Blocking navigation whenever the
draft is dirty would have avoided the data loss and made the feature pointless:
comparing two layers mid-edit is the whole reason to move between them. Driven red by
deleting the parking to confirm the test earns its place.
…he keyboard

Navigation introduced a state nothing handled: a refresh can remove the layer under
an open editor - another tab, a hand edit, a delete on the row behind the dialog.
The lookup fell back to null, which turned the editor into a NEW-layer form still
holding the deleted text, so Save would have recreated exactly what the user had
just removed. The position indicator read "0 / 3" while it happened.

Index -1 is now the signal. The editor closes and says why. Silently swapping to a
neighbour was the other option and it is worse: it puts someone else's text under a
cursor that was mid-sentence.

Custom rows reorder with Alt+Arrow as well as the buttons, per the W3C
rearrangeable-listbox pattern. The buttons stay - a shortcut nobody discovers is not
an affordance - and a bare arrow stays ordinary navigation. Moving the top row up
writes nothing rather than burning a revision on an unchanged list.

Three contracts the reviewer asked for now have tests: custom layers numbered among
themselves rather than continuing the built-in sequence, a save after navigating
writing the layer actually on screen with its siblings intact, and the deletion case
above. The last was driven red by restoring the null fallback.
.api-code sets white-space: pre, and both it and the dialog rule are
single-class selectors - a specificity tie that source order decides.
styles.css loads after styles-codex-set.css, so pre won and a 307-byte
permissions body rendered as two clipped lines behind a horizontal
scrollbar. Reading the layer text is the whole point of the dialog.

Chaining the selector with .api-code outranks it outright. The guard
asserts on the stylesheet rather than a computed style: happy-dom applies
no cascade, so a computed-style assertion passes against the broken rule
too. Driven red against the unchained selector before landing.
A 500 body still parses as JSON, and the payload was adopted verbatim, so
an error response carrying a `layers` key was reported to the user as
measured truth - byte counts and prompt text that were never read.

The narrow case matters more than the obvious one: an error with no
`layers` already landed in the same "unavailable" branch, so the visible
bug only appears when the failing response happens to carry layer-shaped
data. The guard was driven red against exactly that case.

The test Call body is typed rather than `any`; every assertion already
read `body.layers[].id`.
…effect

Calling setState synchronously in an effect body cascades an extra render,
which is what `bun run lint` rejects here - and it also meant the stale
editor was on screen for that intermediate frame. Every input the branch
needs is already known while rendering, so it is a derived boolean.
The catalog entries moved down to the layer whose dialog actually calls
them, so repeating them here made every locale an object literal with
duplicate properties (TS1117) and broke this tip's build.
MSGEOF && bun test ./gui/tests/codex-set-stack.test.tsx ./gui/tests/codex-set-custom-layers.test.tsx ./gui/tests/codex-set-prompt-layers.test.tsx ./gui/tests/codex-set-shell.test.tsx ./gui/tests/codex-set-presets.test.tsx ./gui/tests/locale-parity.test.ts ./gui/tests/fr-localization.test.ts 2>&1 | tail -4
@lidge-jun
lidge-jun force-pushed the codex/prompt-layers-authoring branch from 9b39484 to 3462ef9 Compare August 26, 2026 13:29
@lidge-jun
lidge-jun force-pushed the codex/prompt-layers-realtext branch from 861a1ce to cd71a1d Compare August 26, 2026 13:29
Base automatically changed from codex/prompt-layers-authoring to dev August 26, 2026 14:38
@lidge-jun
lidge-jun merged commit 186b8ef into dev Aug 26, 2026
27 checks passed
@lidge-jun
lidge-jun deleted the codex/prompt-layers-realtext branch August 26, 2026 14:39
tarunravi pushed a commit to tarunravi/opencodex that referenced this pull request Sep 14, 2026
…realtext

feat(gui): show the prompt text Codex really sends, as an ordered stack
agentHits pushed a commit to agentHits/opencodex that referenced this pull request Sep 17, 2026
…realtext

feat(gui): show the prompt text Codex really sends, as an ordered stack
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant