Skip to content

fix: pr draft actions - #12

Merged
ahmedragab20 merged 47 commits into
mainfrom
codex/fix-pr-draft-actions
Sep 8, 2026
Merged

fix: pr draft actions#12
ahmedragab20 merged 47 commits into
mainfrom
codex/fix-pr-draft-actions

Conversation

@ahmedragab20

Copy link
Copy Markdown
Owner

No description provided.

Add bounded lifecycle and owned preparation admission, strict provider handling, and safe cancellation.

Add server-owned plan and patch snapshots, prompt evidence coverage, and offline evaluation/browser baselines.

Keep full UI/toolkit, durable-state, provider certification, and release acceptance work open.
The Codex "90-second timeout regression" was a test-harness fault, not a
product one: vite externalizes `node:*`, so `vi.mock("node:child_process")`
never reached `adapters.ts` and the suites spawned the real `codex` binary.
Route provider spawning through a local `child-process.js` seam that tests
can mock, and serve the model catalog offline where option gating reads it.

- codex-run: 33 failing / 243s -> 34 passing / 2.4s
- codex-image-snapshot: fake child now speaks the correlated thread/turn
  protocol and emits `close` on SIGTERM, matching the adapter's ownership
  contract

Also clear `tsc --noEmit` from 31 errors to 0. Two were real: `PrSessionResponse`
never declared `body`/`createdAt`/`issueComments`/`timelineEvents` even though
the server sends them, so the PR timeline was type-blind to the description and
issue comments; and `panelRef` was typed non-null against a `useRef(null)`.
`buildPrTimeline` now takes the conversation fields it actually reads rather
than a whole `PrSession`. The rest are fixture and mock-signature updates.
tsconfig drops `rootDir` so the vite-config and pi-extension contract tests
typecheck; nothing emits with tsc.
`validateProviderOptions` calls `adapter.models()` directly, bypassing the
service-level model cache, so every run carrying reasoning effort, service
tier or images spawned a second `codex app-server` purely to validate those
options against the catalog.

Retain successful catalogs for the same 15s the service uses, and clear them
from `AiService.invalidateCatalog()` so the existing credential-change and
refresh paths stay live. Failures are never retained. `catalog.ts` now spawns
through the `child-process.js` seam, which is what makes this testable.
Introduces the C18 tool layer: `review.map`, `source.read`, `diff.read` and
`source.search`, all derived from a captured `ReviewSnapshot`. The snapshot
passed in is the entire world a run may read — the tools reach no shell,
network or filesystem, and cannot widen their own scope.

- cursors on the map and on search results
- batch reads with a shared byte budget, per-item errors, and identical ranges
  read once and shared rather than re-billed
- an exhausted budget is reported, never silently truncated
- reading is separated by representation, so patch offsets can never be handed
  back as original-file line numbers
- `ReviewSnapshot.search` locates matches without returning their text, so
  searching issues no evidence and leaves returned-line coverage honest; the
  query is a literal substring, never a caller-supplied regular expression

Still outstanding for C18: definitions, references, history, discussion,
snapshot/version queries, cache authorization and rehydration.
The typed tools had no caller: a run captured its evidence inline and threw it
away, leaving external callers nothing to address. Retain each capture in a
bounded, in-memory store and serve it on the existing surfaces.

- `SnapshotStore`: LRU + TTL retention, no persistence. A caller may pin the
  revision it captured against; a mismatch is reported stale rather than
  silently served a different generation.
- `/api/ai/evidence[/:id/{map,read,search}]`, mirrored by the `ai_evidence_*`
  MCP tools, all annotated read-only.
- Listing and mapping are not reading, so neither adds to returned-line
  coverage; search returns positions only, so a match must be read before it
  can be cited.

Documents the tools in the MCP schema reference.
Definitions and references need a real language server, so this adds one,
discovered on PATH like the provider runtimes and reporting itself unavailable
rather than pretending when absent.

The client speaks only initialize, definition and references. It correlates
every response by id, bounds frames, total bytes, pending requests, locations
and time, and never grants the server authority: a server-initiated request is
answered "method not found", its notifications are dropped, and no workspace
edit, command or file write is accepted. A server that dies mid-request fails
the session instead of surfacing an unhandled stream error.

`locateInSnapshot` maps returned locations onto the capture. A language server
may point anywhere in the repository, so a location outside the captured
evidence is named but marked out of scope and never read — navigation cannot
widen a run's scope. Non-file URIs, paths outside the repository root, and
traversal segments all resolve to out-of-scope, and a patch source is never
resolved, since its lines are not file lines.
The published skills carry a tool and route catalog that the contract test
holds to the server and MCP registrations. Adding the evidence surfaces
without updating it broke that contract.

Documents `ai_evidence_*` and the four `/api/ai/evidence*` routes, corrects
the note that claimed every AI surface is HTTP-only, and mirrors the file into
the repo-local skill tree the byte-identical check compares against.
Adds the configuration, pooling and surfaces that make the LSP client usable.

- `settings.aiLanguageServers` maps a file extension to a server. It is empty
  by default: no toolchain is presumed, so lookups report themselves
  unavailable until one is configured. The settings file is user-editable, so
  entries are validated on load and a malformed one is dropped rather than
  repaired; the command is resolved on PATH and never run through a shell.
- `LanguageServers` starts a server at most once per command and reuses it,
  since a cold start costs far more than a query, and shuts it down once idle
  so a review does not leave servers running. Its idle timers are unref'd.
- `lookupSymbols` maps every returned location back onto the capture and
  reports how many fell outside it. An absent or failing server yields an
  explicit `unavailable` result rather than an empty one, so a caller never
  reads "no server" as "no references". A patch source is refused outright,
  since its offsets are not file positions.
- `POST /api/ai/evidence/:id/symbols` and the `ai_evidence_symbols` MCP tool,
  both read-only, documented in the catalog and the settings reference.
Completes the addressable half of the evidence surface.

`review.map` now names the generation it describes, so a caller can tell which
local, PR or plan capture it is holding rather than inferring it from a
revision string alone.

`verifyCitation` re-checks a stored citation against a capture: one this
capture never issued is invalid, and one held against another generation is
stale. Neither is silently accepted, so an agent that saved evidence can
confirm it still anchors instead of trusting it. Exposed as
`POST /api/ai/evidence/:id/verify` and the `ai_evidence_verify` MCP tool.

Also adds authorization coverage for every evidence route, asserted against
the real app rather than a stand-in, and updates the settings fixture for the
new `aiLanguageServers` default.
The outgoing URI was concatenated, so a path containing a space or other
character needing percent-encoding produced a malformed URI that no language
server would resolve, while returned locations were already being decoded.
Build it with pathToFileURL so both directions agree.
Completes the read-only tool list.

`sourceHistory` lists commits touching a captured source. It is addressed by
snapshot key, never by a caller-supplied path, so a run can only ask about
files it was already given, and it returns commit metadata only — the git
invocation passes `--no-patch` and separates the path with `--` after
`--end-of-options`, so history cannot become a way to read content the capture
withheld. A failing invocation is reported as missing rather than leaking git's
diagnostics. Records use ASCII unit/record separators, so a subject containing
one is still parsed intact, and an unparsable record is rejected rather than
turned into an invented commit.

`sourceDiscussion` lists review threads anchored to captured paths. Threads on
paths outside the capture are counted but never returned, bodies are bounded
and marked when truncated, and comment text is treated as human-written data
rather than instructions to the run.

Both are exposed as read-only routes and MCP tools, paged by cursor, and
documented in the catalog.
Evidence built from a patch alone cannot cite a real file line, so the local
capture now reads the originals behind each changed path at the exact revisions
it identified, and `resolveDiffSnapshot` carries them alongside the patch.

Revision correctness, proven against real git rather than hand-written patches:
staged reads the index and ignores a dirtied working tree, working reads the
tree, revision mode reads each side at its own commit, and a rename reads each
side at its own path. Additions have no old side and deletions no new side.

Honesty rules:
- A git object read is `recorded`; a working-tree read is `reconstructed`,
  since it is optimistic. Those contents are hashed at capture and re-read in
  `assertFresh`, because the index hash does not cover them.
- `mixed` mode still asserts no single old/new pair, so originals are omitted
  rather than attributed to a revision that does not exist.
- Binary, oversized and unreadable paths are omitted explicitly, on the source
  and in the capture's omission list.
- The patch sources keep saying their offsets are not file lines, and the
  capture stops claiming no original coverage once originals exist.

Fixes a real defect found by the repeated-path fixture: a path occurring twice
in one patch produced colliding snapshot keys, which `ReviewSnapshot` rejects.
The first occurrence now wins and later ones are not re-read.
Asserts the invariants the evidence surface depends on, rather than one code
path, so a change that widens authority has to break a test to land.

- Scope: a source outside the capture cannot be read, a line range cannot
  reach past what was captured, out-of-repository and traversal URIs and
  non-file schemes never become readable, discussion outside the capture is
  counted not returned, and the git path comes from the capture rather than
  the caller's key.
- Diagnostics: snapshot and language-server messages come from fixed sets, and
  no caller-supplied key, query, cursor or git failure text is echoed back.
- Authority: the evidence modules reference no filesystem, process or network
  API; history runs execFile with an argv, `--no-patch` and `--end-of-options`
  and never a shell; every `ai_evidence_*` MCP tool is annotated read-only;
  and the language-server client advertises no mutating capability.
- Freshness: a citation from another generation is stale, one minted against a
  different capture is invalid, and a patch source cannot be read as file lines.
Parity was documented but never enforced, so a tool could drift from the route
it calls, or a route could exist that no client can reach.

Every `ai_evidence_*` tool must call a route registered in server.ts with a
matching method, no evidence route may be unreachable from MCP, and no evidence
route may be registered with a mutating verb.
The legacy baseline buys its byte reduction by discarding evidence: on the
large fixture it drops the `unchanged-conversion` line the review needs. That
was documented as "not an acceptable optimization" and left there.

Measures the alternative the typed tools make possible — map the capture,
locate each required text, read only those lines — over the same fixtures so
the two are comparable rather than separately reported.

Measured, on identical fixtures:
- Small: 1,764 bytes retrieved versus 6,627 prompt; evidence preserved.
- Large: 1,794 bytes retrieved versus 101,214 prompt; evidence preserved,
  including the line the legacy prompt drops.

`returnedLines` counts lines a read actually returned, so it is evidence
coverage and never a claim about model attention. These remain measurements on
synthetic fixtures: `humanApprovedThresholds` stays false, browser and UI
responsiveness are still unmeasured, and no threshold is asserted as passed.
Driver spike first, since the plan required one before choosing: `node:sqlite`
does not exist on Node 20, which `engines` still supports, and better-sqlite3
is a native module needing prebuilds for all seven packaged platforms. Neither
is acceptable for a local-first CLI today, so this is built on the append-only
journal the rest of the product already persists with, behind a narrow
interface so a SQLite driver can replace it without touching callers.

Each durability property is tested against a real filesystem:
- Idempotent: a repeated key returns the stored record and writes nothing,
  across a reload and under concurrent duplicates of the same key.
- Concurrent: appends are serialized, so lines never interleave and one
  failure does not stall or lose the writes after it.
- Replayable and recoverable: a torn trailing line is discarded and reported
  by line number, everything before it is kept, appends continue afterward,
  and a record whose checksum does not match its payload is not trusted.
- Non-destructive: a journal from a newer version is refused and left byte
  for byte intact, and compaction keeps the previous journal alongside.
- Honest under disk failure: an I/O error is typed, leaves no half-applied
  record, and never leaks a path or errno into its message.

Single-writer by design, which the existing server lock already guarantees.
Not yet wired into the run path; legacy conversation data is untouched.
- `aiEvidenceTools` gates the evidence routes and, through them, the MCP tools
  that call them. It is read per request, so rollback takes effect without a
  restart, and answers not-found so a disabled build does not advertise the
  routes. Disabling it leaves model listing and runs untouched.
- Default-provider gate: the five shipped providers must stay exactly Codex,
  Claude Code, OpenCode, Cursor and Grok, and the dormant direct OpenAI and
  Anthropic paths must stay out of the defaults. Drift now fails a test.
- Packaging gate: the declared Node floor stays `>=20`, no SQLite or language
  server becomes a runtime dependency, and no AI module imports `node:sqlite`,
  which does not exist on that floor.
- `docs/ai-operations.md` covers the flags, language-server configuration,
  storage rollback/recovery/migration, provider defaults, the verification
  commands and an incident checklist — including that an unavailable language
  server is not the same as no references, and that the baseline runners are
  not acceptance gates.
Wires the storage engine into the run path, so a run's boundaries survive a
restart instead of living only in memory.

`streamAiRun` records a `request` when the run starts and a `result` or
`error` when it terminates, keyed `<runId>:<kind>` so a retry cannot duplicate.
Deltas are deliberately not journaled — one record per token would be a
firehose, and the terminal record already establishes what the run produced.
A failure that never reached a terminal event is still recorded.

Journalling is best-effort: a storage failure is swallowed so it can never
take down a live run, which is asserted by test.

`createApp` takes the store as an optional dependency and journals nothing
without it, so an embedded app — and the test suite — never writes to a user's
storage directory. `startServer` supplies the real per-repo store. An earlier
draft of this constructed the store inside `createApp`, which wrote a journal
into `~/.diffing` during the test run.
A PR capture held only the unified patch, so evidence built from it could not
cite a real file line — the gap left when local originals landed.

Reuses the local originals capture in revision mode with a GitHub-backed blob
reader, so renames, additions, deletions, omissions and deduplication behave
identically on both surfaces. The old side reads the merge base when one is
known, since that is what the PR is diffed against.

Every read costs a network round trip, so this is stricter than local: at most
20 files, an explicit omission for anything past that instead of refusing the
capture, and a byte-bounded cache keyed by host/owner/repo/revision/path so a
repeated capture of the same PR re-fetches nothing. A fetch failure or binary
content is an absent original recorded as incomplete, never a failed capture
and never empty content presented as real.

Also lets the originals bound be lowered per capture, with local keeping its
existing refusal since a local read is cheap.
Parity was HTTP and MCP only, and the catalog said so. `diffing evidence
<list|map|read|search|symbols|verify|history|discussion>` now reaches the same
routes, so all three transports read the same bounded evidence.

Batch reads are mirrored properly: `--range KEY:START:END` repeats, so one
invocation can read several ranges under the shared budget rather than losing
the batching the tool layer provides. The key is split from the right, so a key
containing a colon still parses.

The parity test now asserts three-way coverage — every registered route must be
reachable from MCP and documented as a CLI resource — and the usage text must
list every resource. Usage errors exit 5 with a specific message.

Because the CLI calls the HTTP routes, `aiEvidenceTools: false` disables it
along with the MCP tools; the operations doc and catalog say so.
Findings, proposals and questions that cannot assert what the capture does not
support. Every entry carries at least one citation; each cited range is re-read
and its quote compared byte for byte, and each citation is verified against the
snapshot that issued it. A fabricated quote, a citation minted against another
capture, and a range reaching past the captured lines are all rejected rather
than stored and later rendered as if they were evidence.

Two separations the tests pin down:
- Authoring is not deciding. A run may write entries; only `decide` records an
  outcome, and it records who decided it and when, without mutating the
  notebook it was given.
- Coverage is not quality. What is reported is the lines reads returned, with
  the basis stated verbatim. Verifying a quote re-reads lines already returned,
  so the coverage union cannot be inflated by verification — asserted by test.

Explicit links must resolve to a real entry and cannot be self-referential, so
a notebook never implies a relationship to something absent. Stale re-review
reports a notebook built on a replaced capture as wholly stale, and otherwise
names only the entries whose cited text has drifted.

This is the validated model. Authoring surfaces and storage are deliberately
not wired: who may write entries is a product decision, not one to guess.
The file-mention dropdown intercepted ArrowDown, ArrowUp, Enter, Tab and
Escape whenever it was open, with no composition guard. While an input method
is composing, those keys belong to it: arrows move through the candidate list
and Enter commits a candidate. Intercepting them replaced a CJK user's chosen
candidate with a file mention — silent input corruption, not a cosmetic issue.

The handler now yields while composing, honouring both `isComposing` and the
legacy `keyCode === 229` signal browsers use when it is unavailable, and
resumes handling once composition ends.

The tests first written for this passed vacuously: the dropdown was never
opened, so nothing was asserted. They now assert it is open, and reverting the
guard fails six of them.
Eight call sites passed `behavior: "smooth"` to scrollIntoView or scrollTo.
The global `prefers-reduced-motion` stylesheet rule cannot help there: an
explicit behavior option takes precedence over the CSS `scroll-behavior`
property, so a user who asked for reduced motion still got animated scrolling
in the diff minimap, plan and PR review panes, theme and font pickers, comment
navigation and plan line sync.

Routes them through a `scrollBehavior()` helper that resolves the preference,
treats a missing or throwing `matchMedia` as no preference rather than
failing, and never upgrades an explicitly instant scroll.

`useScrollToNextFile` already checked the preference correctly; it now shares
the helper rather than carrying a second copy of the query.

A sweep test fails if any `src/ui` source reintroduces a literal smooth
behavior, so this cannot silently regress. It skips type declarations such as
`behavior: "smooth" | "auto"`, which are not calls.
The rail clamped to a fixed 320–720px regardless of the window, so a width
persisted on a wide display overflowed a narrow one and left no room for the
diff the rail exists to discuss. Nothing re-clamped when the window resized
either, so narrowing a window kept the old width.

`clampRailWidth` resolves a requested width against the viewport, always
leaving a diff gutter, and lets a narrow window override the usual minimum
rather than overflowing. When no usable rail fits it returns null so the caller
collapses instead of rendering something unreadable. It is idempotent, so
re-clamping on every resize event is stable, and it falls back to the minimum
for a non-finite width rather than producing NaN geometry.

The rail now re-clamps on mount and on window resize.
C22's safety property is testable without a browser: a run must never start
unprompted, and the AI surface must never be able to publish.

- No module under src/lib/ai or src/ui/ai may reference a GitHub mutation
  helper — merge, state change, metadata update, suggestion apply, or pending
  review comments — so no run path can reach publication however it is prompted.
- The only inference trigger anywhere in the AI surface is `user`. Introducing
  a background trigger fails this test, which was verified by doing so.
- The server-side refusal of a non-user trigger stays in both the request
  validator and the service.
I had been deferring this as an unspecified product decision. It is not: the
approved plan defines all three activities and their bounds, so this implements
that spec rather than inventing one.

- Ask answers with citations and explicit unknowns; Investigate returns
  findings, questions and coverage; Propose drafts a comment, reply, summary,
  code suggestion or plan revision.
- Investigate is the only iterative activity, so it is the only one with a step
  budget above one. Each is bounded in duration, steps and output, and an
  exhausted budget names which bound stopped it, so partial work is never
  presented as complete.
- A proposal is inert. `requiresHumanApproval` is true for every proposal kind,
  the admitted shape is plain data exposing no operation, and a proposal must
  say what it drafts while a non-proposal may not.
- Only an explicit user trigger admits an activity.

Also asserts the plan's stated retrieval target as a test: at least 50% fewer
serialized evidence bytes on the focused large diff, and only while every
labeled evidence item survives — the target is not permission to drop context.
Meeting it is still not a human-approved threshold, which the test also pins.
Another item I had called unspecified that the plan spells out. It enumerates
the state matrix — empty/disconnected, queued/preparing, tool activity,
streaming, interrupted, canceled, retry — and states the honesty rules for the
activity list: report actual reads, outcomes, elapsed time and coverage, and
do not invent percentages, pretend to expose private model reasoning, or
animate a false success.

Those rules are enforced structurally rather than by convention:
- No progress fraction is computed, so a caller cannot render one. A test
  asserts no progress/percentage key or reasoning channel exists.
- `succeeded` is true only after a terminal complete event actually arrived. A
  stream that merely stops is interrupted, never quietly successful.
- Partial text from a canceled or interrupted run is preserved and labelled
  partial, rather than discarded or presented as the answer.
- A retry is a new attempt with its own state and does not overwrite the one
  before it.
- Elapsed time comes from an injected clock and never goes negative on skew.
The plan requires importing legacy history "without overwriting originals" and
using "revisions and idempotency keys rather than whole-transcript
last-write-wins saves". Both are enforced rather than intended.

- The legacy store is only ever read. A test asserts it is byte for byte
  identical after an import, so a run can be repeated, interrupted or
  abandoned without damaging the history it came from.
- Every turn is journaled under a key derived from its conversation, position
  and id, so a re-run adds nothing and an interrupted import resumes by
  writing only what never landed. Position is part of the key because legacy
  turns need not carry an id and two may be textually identical.
- A turn that cannot be trusted is skipped and counted, never rewritten into
  something that would import cleanly, and an undated turn inherits the
  conversation's timestamp rather than inventing one.

The report distinguishes imported from already-present, so an incomplete
import is visible instead of reading as a clean one.
Completes the notebook: entries are now authored, stored and decided through
the same surfaces as evidence. With an external agent as the producer — the
architecture chosen for this PR — that agent can record cited findings and a
person can decide them.

- Entries are journaled on the durable store, so they inherit its idempotency,
  replay and recovery. Validation runs before the write, so an entry that
  cannot cite the capture is never stored, not stored then rejected.
- Deciding is a separate record from authoring, so a writer cannot smuggle an
  outcome in with the entry it wrote. The latest decision applies and earlier
  ones stay journaled: changing your mind is a new record, never an erasure.
- Read, author and decide are reachable over HTTP, MCP and the CLI, keeping
  the three-way parity the contract test enforces.

Two of my own tests caught mistakes in this change and both were right:
- The parity test failed because I added HTTP routes without the MCP and CLI
  mirrors.
- The adversarial test failed because I put two writing tools in the
  `ai_evidence_*` namespace, which is asserted read-only. Renamed them to
  `ai_notebook_*` rather than weakening the invariant, and added an assertion
  that the writers are idempotent, non-destructive mutations.
The rail re-rendered and re-parsed every completed turn on each streamed
token. The plan forbids exactly this — "avoid reparsing the entire transcript
per token" and "memoize completed turns and completed Markdown blocks" — and
on a long conversation it is the difference between a smooth stream and a
stuttering one.

Completed assistant turns render through a memoized component whose comparator
is explicit: a turn re-renders only when its own id, text or copied state
changes, never because a sibling is streaming. A test drives five tokens
through a transcript and asserts the completed turns are not re-parsed once.

The copy handler is now stable across renders too. Without that the memo would
have bought nothing, since a fresh handler each render fails the comparator —
the kind of detail that makes a memoization look right and do nothing.
The plan specifies both the card's contents — claim, uncertainty, evidence
references, verification status — and the rule that governs them: "invalid
citations are rejected or shown as unverified, not converted into
authoritative findings."

Verification is therefore presented, never assumed:
- A citation absent from the verification map is treated as unverified, so
  omission cannot pass for confirmation.
- An unverified citation is still shown, since dropping it would conceal what
  the claim rests on, and the card states how many could not be verified.
- The card marks itself unverified structurally, with a border weight as well
  as a label, so it does not depend on color alone.

Styling uses Gridline tokens rather than literal colors, which a test enforces
so a card cannot drift from the theme or break in dark mode. Long quotes
scroll inside the card rather than overflowing the rail.
The plan's compact, expandable account of what a run actually did, with its
prohibition enforced rather than intended: "do not invent percentages, pretend
to expose private model reasoning, or animate a false success."

- No progress fraction can reach the DOM, because the activity model computes
  none. A test asserts no percentage text, no `progress` element and no
  progressbar role is rendered.
- A terminal phase that is not success states itself — interrupted, canceled
  and failed each carry their own border weight as well as a label, so an
  interrupted run does not read like a finished one at a glance.
- Partial output is labelled partial, so a reader is not left assuming the
  visible text is the whole answer, and a retry names its attempt number.
- Steps report the reads and tool outcomes that actually happened, with
  failures distinguished, and an empty list says so rather than implying work.

Status is announced through a visually-hidden live region, so assistive
technology hears the phase change without rereading the transcript on every
token.
The assistant's thinking indicator loops infinitely and nothing anywhere
observed page visibility, so it kept animating in a background tab. The plan
requires activity animation to pause "when inactive/hidden"; browsers throttle
such animations but do not stop them.

CSS cannot observe visibility, so the state is reflected onto the root element
and the stylesheet pauses on that. Reflecting state rather than stopping each
animation means one added later is covered without anyone remembering to opt
in, and pseudo-elements are included for the same reason.

A page that is already hidden when observation starts is marked immediately,
so a first paint in a background tab is paused rather than animating until the
first visibility change. Installed before the app renders, which a test pins.

No visible change while the page is visible.
The resize handle advertised aria-valuemin 320 and aria-valuemax 720. Those
were correct until this branch clamped the rail to the viewport; since then a
narrow window has had a lower real maximum and can force a minimum below 320,
so the announced range no longer matched what a resize could produce. A
screen-reader user was told a range that does not exist.

`railWidthBounds` derives the announced range from the same rule the clamp
uses, and a test asserts every width the clamp can return falls inside the
announced bounds, so the two cannot drift apart again.

This was a defect introduced earlier in this branch, not a pre-existing one.
My earlier clamping fix covered only the keyboard path. Dragging the resize
handle still used the hardcoded 320-720 range, so the more commonly used path
could still overflow a narrow window and squeeze out the diff — an incomplete
fix that looked complete because the keyboard test passed.

Both paths now share the same rule. A test drives each of them in a 900px
window and asserts no persisted width exceeds what the viewport allows, and
reverting the drag clamp fails it.

Also asserts the handle announces that window's real maximum, so the ARIA
range and the two resize paths cannot drift apart independently.
Puts the reviewed parts together — completed turns, the active response, run
activity, cited findings, retry — so there is something concrete for the human
visual review the plan requires before rollout.

Deliberately not wired into the rail. The plan says review "must confirm that
the result still looks and feels like diffing before rollout", so replacing
what users see today is not mine to do unprompted. A test asserts the rail does
not reference it, so this cannot become the shipped path by accident.

Composition rules that are not cosmetic:
- Completed turns render through the memoized component, so a streamed token
  does not re-parse the transcript above it.
- The active response is kept out of the completed list, so a stream cannot
  rewrite settled history.
- Retry appears only after a terminal failure, and says it starts a new
  attempt and keeps the previous one, matching the model's behaviour.

I first added an `aiTranscriptShell` setting to gate this and then removed it:
nothing reads it, and a setting no code consumes is worse than no setting.
Responsiveness measured as an algorithmic property rather than a stopwatch.
Wall-clock timings in jsdom say nothing about a real browser, but the shape of
the work does: if a streamed token re-parses completed turns, cost grows with
conversation length and a long review degrades.

Counting parses measures that directly and does not flake. Streaming ten
tokens into transcripts of 5, 50 and 200 turns re-parses nothing in every
case, and ten times the tokens costs the same as one — so the work is bounded
by the active response, not by history.

This is the honest half of C24's UI target: it proves the scaling property
without claiming browser latency numbers this environment cannot produce.
The review UI now renders completed turns, the in-flight response, run
activity, cited findings, and retry-after-failure on the shipped rail
instead of leaving TranscriptShell unwired.
Cancel and error used to leave the rail in a terminal phase that kept
the New conversation, switch, and delete buttons enabled while those
handlers still required idle, so they no-op'd. They now match start():
clear the pending attempt and run from a terminal-failure phase.
Browser tests now drive ask, stream, retry and unverified findings on
diff, PR-diff and plan. Live compatibility is a real discovery/ping
probe that never flips liveVerified. Latency and heap are sampled;
ceilings stay unapproved.
A Tip: footer wiped the Cursor model list, and thinking events aborted
live pings as protocol errors. Those are product bugs. Claude 429 and a
missing Grok key are still environmental; liveVerified stays false.
The CSRF Origin check required an exact match with the backend URL, so
pnpm dev (UI on :5173, API on :3433) 403'd every /api call. Loopback
http(s) origins are accepted when the server itself is loopback-bound;
off-loopback Origins still have to match.
Replace the boxed 3x3 dots with ⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏ and drop the thinking
chip background so it sits in the transcript instead of a pill.
Add license, metadata, and user_invocable so skills.sh validate passes.
Wire evidence/notebook tools through the router and review recipe, note
the Vite loopback Origin rule, and keep published and repo-local trees
byte-identical.
@ahmedragab20
ahmedragab20 merged commit 437c28e into main Sep 8, 2026
7 checks passed
@ahmedragab20
ahmedragab20 deleted the codex/fix-pr-draft-actions branch September 8, 2026 16:03
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