Skip to content

Fix post-refactor permission and feed rendering regressions - #2

Merged
Juliusolsson05 merged 10 commits into
mainfrom
fix/post-refactor-backlog
Apr 24, 2026
Merged

Juliusolsson05 merged 10 commits into
mainfrom
fix/post-refactor-backlog

Conversation

@Juliusolsson05

Copy link
Copy Markdown
Owner

Summary

  • render Claude permission approval prompts again and route approve/deny actions through session IPC
  • bump claude-code-headless to the permission prompt parser/runtime commit
  • add a workspace command to copy a provider resume command
  • fix semantic live-turn duplicate suppression so repeated text in later turns is not hidden

Dependency

Verification

  • npm run build

Juliusolsson05 and others added 10 commits April 23, 2026 22:41
…l unconditionally

The optimistic-user row is appended directly to `runtime.entries`
by addOptimisticCodexUserEntry on submit. The reconciliation path
in the Codex rollout ingest was supposed to drop it when the
committed user entry arrived, but the check was gated behind
`appended.length === 0` — "only look at the current.entries tail
if we haven't appended anything in this burst yet."

That's wrong when Codex emits the `<environment_context>` synthetic
user message BEFORE the real user message. The env_context is a
non-matching user entry that gets appended first; when the real
user message lands next, `appended.length > 0` takes the if-branch,
finds env_context (not optimistic) on the appended tail, and
never touches `current.entries`. The optimistic row survives and
the user sees their prompt rendered twice — the top-of-feed dup
in the 2026-04-23 Codex pane screenshot.

Fix: run both checks independently. They target different data
(appended[] is this burst's accumulator, current.entries is the
pre-burst state), so there's no logical conflict. The current.entries
tail is always a candidate for removal when we see a matching
committed user message, regardless of what else the burst already
appended.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…response_items

Three improvements to the Codex rollout → feed-entry mapper, all
in workspace/codex/rollout.ts.

Env_context filter. Codex injects a synthetic user message on every
conversation that wraps cwd / shell / timezone / date metadata
for the model (`<environment_context>…</environment_context>`). It
landed in the feed as a real user bubble, which is what the
top-of-feed dup in the 2026-04-23 Codex pane screenshot came
from (three stacked user rows: optimistic + env_context + real).
Drop at the mapper so the shim never enters runtime.entries;
downstream surfaces (visibleDecisions, debug panels, copy-
assistant picker) don't have to know about it. Match permissively
on the outer tag so variations across Codex versions still
catch, and require the message content to be just one env_context
block before filtering — a real user prompt that quotes the tag
still renders.

Refusal preservation. The message-content filter previously only
accepted blocks with a `.text` field. `CodexContentItem` also
allows `{ type: 'refusal'; refusal: string }` (per
packages/codex-headless/src/transcript/TranscriptTypes.ts:108);
those items have their payload under `.refusal`, not `.text`, so
refusals silently vanished from the committed feed even though the
live semantic renderer painted them. Map to a text block prefixed
"(refused) " so the row reads cleanly without needing its own row
component.

New response_item mappers. SemanticLiveBlockRow has explicit live
UI for web_search_call, image_generation_call, local_shell_call,
and tool_search_call (plus the tool_search_output result variant).
The rollout mapper used to `return []` for all of them, so those
blocks rendered once live and disappeared on commit / reload.
Synthesize Claude-shaped tool_use / tool_result entries so the
existing CodexToolRow / CodexToolResultRow dispatcher paints a
committed row. The headlines don't match the specialized live
UI exactly (web_search emoji, image-gen chip, shell prefix) —
consider a dedicated CodexSpecialToolRow in a follow-up — but
the block no longer disappears on commit and the data round-
trips through disk for reload parity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ds in pending-tools

Two changes to the live-turn rendering + lifecycle, sharing the
same theme (live vs committed ownership).

Live-block suppression against the committed tool_use index.
SemanticStreamingTurn used to render every block in
`semantic.currentTurn.blocks` unconditionally, even ones whose
`toolUseId` / `callId` already existed as a committed tool_use
entry in the main feed above. The 2026-04-23 debug bundle showed
seven exec_command rows painted at the bottom of a Codex pane
while their committed counterparts (call_8jIH…, call_b5Dv…, …)
had already rendered inline — because the live turn stays mounted
for the full multi-minute agent turn and keeps painting the block
list long after individual tool rounds commit.

buildSemanticRenderUnits now takes an optional committed
ToolUseIndex and skips any block whose tool correlation id is
already there. The check looks at both Claude's `toolUseId` and
Codex's `callId` (see workspace/codex/entries.ts::codexToolUseEntry
for why the committed Codex index is keyed by call_id). Text and
thinking and citation blocks carry no tool id and pass through
unchanged — the assistant-text-in-live-vs-committed dup is a
separate class, handled by a future committed-tail guard.

StreamingTurn.tsx pulls the committed index from ToolUseIndexContext
— already provided by Feed at a higher scope, so no new prop is
needed. The filter runs purely at render time: the live turn's
state shape is unchanged, and when the turn's own `currentTurn`
clears the filter goes with it.

Pending-tools covers Codex kinds. hasPendingSemanticTools only
returned true for Claude's `tool_use` / `server_tool_use` /
`mcp_tool_use` kinds. Codex blocks are `function_call` /
`custom_tool_call`, which the check trivially returned false for.
If turn_completed ever fires on a Codex turn with in-flight
function calls, the turn seals while results are still pending —
late results land in a closed turn. Doesn't bite today because
CodexResponsesAdapter isn't emitting turn_completed at all (the
separate watchdog fix in packages/codex-headless), but the check
should be right independently.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ith output

Two independent render-polish fixes in the feed.

WorkIndicator on empty feed. The early-return branch at
`visible.length === 0 && !hasSemanticStreaming` used to render
only the "waiting for Codex…" / "waiting for Claude Code…"
placeholder, swallowing the WorkIndicator during `submitting` /
`requesting` / early `thinking`. A fresh submit on a new session
looked like a silent stall until the first delta landed. Reuse
the same positioning the non-empty branch uses — WorkIndicator
at the natural composer-adjacent bottom, wrapped in a column
flex so the placeholder text centers above it.

Semantic collapse skips blocks with real output. classify-
SemanticToolActivity marked Read / Grep / Bash / list tools
collapsible unconditionally, so finished blocks with meaningful
`resultContent` or a `resultIsError` got folded into
"worked: 3 reads, 1 search" — the content the user cares about
disappeared. Gate the classifier so collapse only fires on
blocks without resolved output; running churn and stub-success
results still collapse, but a tool that actually produced
something renders as its own SemanticLiveBlockRow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…g cursor post-append

Two small correctness fixes on workspace-level bookkeeping.

Older-history pagination trusts the chunk's own hasMore signal.
The old rule OR'd in `prepend.length === 0`, i.e. "re-enable
loading when nothing renderable came back." That legitimately
loops forever when the loader returns a final chunk whose
entries are all non-renderable (Codex metadata: turn_context,
session_meta, event_msg variants the mapper drops). Those
chunks carry `hasMore: false`; honoring that ends the pagination
even when prepend is empty. If a chunk with `hasMore: true`
produces zero renderable entries we still fall through with
hasOlderHistory=true and the user can request the next one.

Feed-debug persistence advances the cursor after the IPC append
resolves, not before it fires. The old ordering advanced the
`persistedFeedDebugIdRef` optimistically; a transient failure
(disk full, IPC timeout, main-process mid-restart) would mark
the entries as persisted and the next effect pass would skip
them, permanently dropping that window of debug logs. Moving
the assignment into `.then()` leaves the cursor at its previous
value on failure so the next runtime update retries. The
main-side appender is idempotent on (sessionId, id) because
entries are written append-order with monotonic ids, so
duplicate writes from a retry just no-op at the file layer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Points packages/codex-headless at Juliusolsson05/codex-headless
branch fix/proxy-flow-watchdog (commit aa0e187). The only change
is CodexResponsesAdapter.ts: add a watchdog that forcibly releases
a silent activeFlowId after 60s, with a 10s poll interval.

Fixes the underlying cause of the frozen-live-turn scenario the
earlier Feed.tsx fix in this branch only half-addressed: the A3
renderer filter hides duplicate live blocks after commit, but the
turn itself used to stay mounted indefinitely because the flow
slot never cleared. With the watchdog, a dropped upstream now
surfaces as a sealed turn within ~70s instead of freezing the
session's semantic view for the rest of the run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Juliusolsson05
Juliusolsson05 merged commit 7b47ec6 into main Apr 24, 2026
@Juliusolsson05
Juliusolsson05 deleted the fix/post-refactor-backlog branch April 24, 2026 09:34
Juliusolsson05 added a commit that referenced this pull request Jul 20, 2026
… imports

Three bugs, all found by actually running it. The symptom was an empty modal
with no error anywhere.

1. PER-MOUNT ELEMENT. React.StrictMode runs effects mount -> cleanup -> mount.
An extension whose disposer defers its unmount — queueMicrotask,
requestAnimationFrame, a transition — tears down root #1 AFTER root #2 has
rendered into the same node, and root1.unmount() empties the element root2 lives
in. The view mounts "successfully" and shows nothing. The timer extension defers
exactly this way, which is how it surfaced.

Each mount now gets its own child element, discarded on cleanup, so a late
disposer operates on a detached node it cannot reach anything through. That
makes the host immune to extension cleanup timing rather than requiring every
extension to get it right.

2. NO SILENT EARLY RETURN. `if (!element) return` left status at whatever it
started as — and when the extension already activated at startup that is
'ready', producing a visible empty view with nothing logged. Any exit from the
mount path now sets a status.

3. CACHE-BUSTED IMPORT URL. The ES module registry caches by URL and caches
FAILURES: once import(url) rejects, every later import of that URL returns the
same rejection for the life of the realm, even after the file on disk is
replaced. So an extension that failed once could never be fixed by reinstalling
— the user updates it, sees the identical error, and has no signal that their
fix landed. Reloading the window was the only escape and nothing said so.

The key is version + the installer's sha256, which changes exactly when the
bytes change. The query never reaches the filesystem: the scheme handler builds
its path from url.pathname, which excludes the search string.

typecheck / 1187 tests / build green.

Co-Authored-By: Claude Opus 4.8 <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