* feat(native-chat): show a Codex chat's goal above the composer and set it from goal mode
Structured Codex chat now treats the thread goal as session state: a banner above the
composer shows the current goal (pursuing / paused) with clear, pause/resume and expand;
/goal enters a goal mode whose send calls thread/goal/set; the objective is journaled as a
user message marked as sent as a goal. The banner is derived from the journaled goal rows,
which Codex's resume snapshot refreshes, so a reopened or adopted chat shows its goal.
Fixes STA-8159
* fix(native-chat): replace a recorded goal by clearing first, and recover a lost goal-change response
- A set while the journal records a goal (any status) clears it before setting,
so the new goal starts with its own time and token counters instead of
rewriting the old goal's objective in place.
- The threadGoal plan answers an unknown outcome from the goal the journal
records and reruns otherwise, so one request timeout no longer refuses every
later Clear/Pause/Resume as unknown for the mounted session.
- The goal-mode chip says "Exit goal mode"; "Clear goal" stays the banner's
action on the provider goal.
- A typed bare /goal on Enter enters goal mode, the same as picking it.
- The renderer reads the goal off the tail of its ordered snapshot; the host's
unordered map keeps the by-sequence reader.
- Drop the composer's duplicate in-flight guard; the goal controller already
serializes changes.
- Pin that a counter-only revision reaches a subscriber's live page under its
original sequence.
* fix(native-chat): keep a bare /goal inside goal mode as the entrance, and pin goal delivery and serialization
- A bare `/goal` submitted while already in goal mode re-enters the mode instead
of setting a goal whose objective is the literal text "/goal".
- The counter-only revision pin now drives the host's own event sink bound to a
real journal, so it goes red when the publish after a lifecycle transition is
dropped; the previous fake sink never published.
- Pin that a set which threw after journaling its objective puts that objective
back exactly once when the ledger reruns the same operation id.
- Cover the goal controller hook: absent without host support, the loaded window
wins over the host's answer, a second change while one is unsettled answers
false without a request, and a refused change frees the next one.
* fix(native-chat): resume a blocked or usage-limited goal, and keep goal-mode drafts honest
- The goal bar offers Resume on a blocked or usage-limited goal, which the
provider resumes exactly as it resumes a paused one; a goal whose token budget
is spent still offers only Clear. The rule lives beside the other goal facts
in shared code so every reader answers it the same way.
- A `/goal <text>` typed inside goal mode sets the objective `<text>`, as it
does outside goal mode, instead of a goal whose objective is the literal
command.
- Setting a goal is a host round trip; a draft edited while it was in flight is
no longer wiped when the goal lands, matching every other host command.
- Pin that a lost status-change response is read as applied only when the
recorded goal is in that status, that a cleared row in the loaded window
outranks the host's earlier answer, and that the PTY lane is untouched.
* fix(native-chat): keep the load-older anchor on the loaded window when a live revision lands below it
A live revision of a row keeps that row's original sequence. When the row is
older than the client's loaded window, the shared reducer merged it in and it
became the load-older anchor, so paging `before` it skipped every row between.
A goal's counter-only revisions during a long goal turn reach any client that
attached after the goal row left its window, so a reopened chat lost rows on
scroll-back.
The reducer now admits live rows only at or above the window's oldest row
while older rows remain on the host; the journal keeps the revision and the
page reader serves it once the window reaches the row. With nothing older on
the host the window is the whole journal, so a row below the head is admitted
as before.
Also drain accepted provider events before a goal set reads the journal to
decide whether it replaces a recorded goal.
* fix(native-chat): pass agent-implemented slash commands through to the agent
Claim what the host implements; pass through what the agent implements.
Claude's harness expands a slash command out of the message text, so the
host claimed catalog commands it had no way to run and answered "/init is
not available in chat sessions" for commands Claude does run. Codex's
app-server has no slash parser at all, so its catalog stays claimed —
except /goal, which the model carries out through its own goal tools.
* fix(native-chat): offer the agent-run commands in the structured picker
Codex reports no command catalog, so its structured `/` menu is the host
fallback -- which listed only the host's own commands and hid `/goal`, the
one command the model itself acts on. The picker now appends the profile's
text-driven commands, described from the curated catalog, so a command that
passes through is discoverable and not merely typable.
The menu invariant holds either way: a pick is answered by the host or run
by the agent, never refused with "not available in chat sessions".
* fix mobile structured command reconciliation
* fix(mobile): keep native chat controller within lint budget
* fix mobile controller lint budget
---------
Co-authored-by: Merge Sim <sim@local>
* fix(native-chat): render Claude structured chat through the same UI as Codex
Structured native chat is one shared, agent-agnostic component tree, but three
Codex-hardcoded terms on that path made a Claude session render differently.
- `showTurnStatus` was `agent === 'codex'`, which gated the whole structured
presentation: the live tool-progress row, the completion check and activity
grouping (`structuredActivityUi`), and the Thinking / Working for N /
Worked for N status rows. Claude fell back to the legacy compact chrome.
- `runtimeContext` was likewise Codex-only, so a Claude transcript rendered
images as filename chips instead of previews.
- The composer's structured slash menu always served the Codex catalog,
ignoring `agent`. That disagreed with the dispatcher, which does branch per
agent: Codex-only tokens offered to Claude missed the command guard and were
sent to the model as literal prompt text, where Codex shows an error.
The first two gates landed Codex-first (#17597, #18266) before the Claude
structured lane existed; they were rollout scoping, not capability limits.
Neither `useNativeChatTurnStatus` nor `useNativeChatImageRuntimeContext` has
any agent-specific logic. The menu now reads `structuredSlashCommands(agent)`,
the function the dispatcher already used, so both read one list.
No new rendering logic: two gates removed and one existing shared function
reused. There are now zero `agent === 'codex' | 'claude'` branches in any
native-chat component.
* docs(native-chat): update structured turn status contract
---------
Co-authored-by: Merge Sim <sim@local>