[codex] fix editor input regressions#63
Conversation
There was a problem hiding this comment.
Code Review
This pull request enhances the terminal editor by implementing terminal-cell-width-based wrapping (supporting wide CJK characters and emojis), adding visual clipping with scroll indicators for over-tall lines, and introducing input suspension when overlays like the command palette or file picker are active. It also fixes an undo/redo stack issue where redos could not be undone. Feedback was provided to extend the wide character detection in isWideCodePoint to include CJK characters in Plane 2 and Plane 3.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| function isWideCodePoint(cp: number): boolean { | ||
| return ( | ||
| cp >= 0x1100 && | ||
| (cp <= 0x115f || | ||
| cp === 0x2329 || | ||
| cp === 0x232a || | ||
| (cp >= 0x2e80 && cp <= 0xa4cf && cp !== 0x303f) || | ||
| (cp >= 0xac00 && cp <= 0xd7a3) || | ||
| (cp >= 0xf900 && cp <= 0xfaff) || | ||
| (cp >= 0xfe10 && cp <= 0xfe19) || | ||
| (cp >= 0xfe30 && cp <= 0xfe6f) || | ||
| (cp >= 0xff00 && cp <= 0xff60) || | ||
| (cp >= 0xffe0 && cp <= 0xffe6)) | ||
| ); | ||
| } |
There was a problem hiding this comment.
The isWideCodePoint function does not account for CJK characters in Plane 2 (Supplementary Ideographic Plane, 0x20000..0x2FFFF) and Plane 3 (Tertiary Ideographic Plane, 0x30000..0x3FFFF). These characters are also wide and should have a terminal width of 2. We should update the check to include these ranges.
| function isWideCodePoint(cp: number): boolean { | |
| return ( | |
| cp >= 0x1100 && | |
| (cp <= 0x115f || | |
| cp === 0x2329 || | |
| cp === 0x232a || | |
| (cp >= 0x2e80 && cp <= 0xa4cf && cp !== 0x303f) || | |
| (cp >= 0xac00 && cp <= 0xd7a3) || | |
| (cp >= 0xf900 && cp <= 0xfaff) || | |
| (cp >= 0xfe10 && cp <= 0xfe19) || | |
| (cp >= 0xfe30 && cp <= 0xfe6f) || | |
| (cp >= 0xff00 && cp <= 0xff60) || | |
| (cp >= 0xffe0 && cp <= 0xffe6)) | |
| ); | |
| } | |
| function isWideCodePoint(cp: number): boolean { | |
| return ( | |
| (cp >= 0x1100 && | |
| (cp <= 0x115f || | |
| cp === 0x2329 || | |
| cp === 0x232a || | |
| (cp >= 0x2e80 && cp <= 0xa4cf && cp !== 0x303f) || | |
| (cp >= 0xac00 && cp <= 0xd7a3) || | |
| (cp >= 0xf900 && cp <= 0xfaff) || | |
| (cp >= 0xfe10 && cp <= 0xfe19) || | |
| (cp >= 0xfe30 && cp <= 0xfe6f) || | |
| (cp >= 0xff00 && cp <= 0xff60) || | |
| (cp >= 0xffe0 && cp <= 0xffe6))) || | |
| (cp >= 0x20000 && cp <= 0x3ffff) | |
| ); | |
| } |
…ly AgentRunner tests Follow-up to the panel's finding (major/gate-relaxed, agreement:1): file-scoped setDefaultTimeout(15s) also raised the ceiling for the parseAgentSpec and loadAgentSpecs tests, which do NOT construct AgentRunner — over-broadening the relaxation and contradicting the comment's 'every test here' claim. Move those pure config-spec tests to a new agent-specs.test.ts (they belong there anyway — fast, hermetic, no repo). agent-runner.test.ts now contains ONLY AgentRunner-against-repo tests, so setDefaultTimeout(15_000) is precisely scoped to exactly the load-sensitive tests; every other file (including the moved config tests) keeps bun's 5s fail-fast default. Comment corrected to match. 14 tests unchanged (11 AgentRunner + 3 config), all green; typecheck/lint/format clean.
… service) — no timeout change Panel BLOCK on the scoped timeout (major/gate-relaxed): a reviewer held that ANY raised ceiling lets a 5-15s regression in these tests pass — a relaxation, even scoped. They're right that the timeout was treating a symptom. Root cause (found): AgentRunner.run calls buildTsService(cwd) when no tsService is passed. With cwd: REPO that builds a TypeScript service over the ENTIRE tsforge repo — ~2s PER test — even for immediate-abort / no-op cases. That fixed cost, not the loop work, blew bun's 5s default under full-suite CPU contention. Fix at the root: pass tsService: null in every run() here (as many sibling tests already do). These read-only tests exercise tool-gating / events / maxTurns / abort / policy — none use the type-aware tools — so a null service is faithful. Result: the file drops from ~24.6s to ~5.3s (11 tests), every test well under the 5s fail-fast default. setDefaultTimeout REMOVED entirely — NO timeout change, so nothing is relaxed; fail-fast + infinite-loop detection are fully intact. (Pure config tests remain split into agent-specs.test.ts from the prior step.)
…all missed + fix stale comment
Panel caught (agreement:1, major): the two structured-mode tests use the
single-line .run({ provider, cwd: REPO, ... }) form, which my cwd:REPO-line
replace_all didn't match — so they still called buildTsService(REPO) (~2s) and
the 'every run passes tsService: null' comment was false. Add tsService: null to
both; now EVERY run() is stubbed (file ~24.6s -> ~0.4s). Also fix the stale
agent-specs.test.ts header comment that referenced a 'raised per-file timeout'
(removed in this PR). All 11 AgentRunner + 3 config tests green; typecheck/lint/format clean.
…ar-green + test-stability fix stack (#172) * feat(loop): near-green rotating-error detector + completion-only steer (#77) At near-green a build can sit at a stable low count while the error SET rotates — each single-error fix spawns a different one (extract a component → its siblings/tests are now missing → revert → rotate) — so it never reaches 0. Count-only WS-B can't see it. build17 parked on this; build16 crossed only by luck (4/4 harness-diagnose: the count stays at best-ever while the fingerprint rotates). Detector (near-green-checkpoint.ts): errorSetSignature — sorted unique per-error tokens (rule|file family when present, else the key; stack-agnostic, line- independent). isNearGreenRotation requires BOTH a count PLATEAU (the window sits at one count — a moving count like 2→1→1 is progress, not rotation) AND a changing signature (not all-same, which is a stuck single). trackNearGreenRotation (turn.ts) records {count, sig} on near-green cycles, tolerates a bounded run of spikes between them (MAX_NEAR_GREEN_SPIKE_GAP — past it the stale window clears so far-apart episodes can't combine), does NOT record completion-phase churn, and clears on green. On rotation, injectFeedback prepends a GENERIC completion-only steer that cleanly separates the two poles (do NOT open new surface — no new components/ splits/features; but DO create the siblings + colocated test a current error requires, atomically, even though they aren't in the error list yet). Gated on FOUR conditions: flag on (authoritative at emit) AND detector fired AND count ≤N (not a spike) AND not completion phase. The near-green banner drops ONLY its 'do NOT create new files' lockdown clause when the steer fires (it would contradict the steer) but KEEPS the regression callout. Per-drive state cleared at both drive boundaries; a mid-run kill-switch flip clears the sticky flag. Flag nearGreenRotation() default ON, kill TSFORGE_NO_NEAR_GREEN_ROTATION. Generic — no stack strings. Tests cover the detector (plateau/stuck/descent/key-fallback), the tracker (rotation, spike- ignore, spike-gap-clear, completion-skip, green-clear, mid-run-kill), the flag, the drive-boundary clear, and the user-visible injection (prepend, banner lockdown-drop + regression-keep, flag-off/spike/completion/no-detector suppression). * fix(#77): require a genuine per-cycle edit for near-green rotation (panel round-2) Panel major finding: trackNearGreenRotation recorded every gate result and judged rotation purely from gate OUTPUT (error-set signature), with no independent signal that the scope files actually changed between cycles. A flaky/stateful gate — or a re-evaluation of the same unedited files (the check-tool + settleGate double-run, #59) — could emit A->B->C signatures with no fix-one->spawn-another cycle and falsely stand the WS-B rollback net down. - Stamp each near-green sample with an INDEPENDENT worktree rev (a content hash over the glob-resolved scope files via the same snapshot substrate WS-B rolls back with, so scope can't drift). isNearGreenRotation now requires BOTH the signature AND the rev to change on every cycle -> a rotating signature over UNCHANGED files reads as not-rotating and leaves the rollback net engaged. rev is computed only on near-green cycles (the only branch that consumes it). - Extract the rotation-emit decision into rotationEmit() so injectFeedback stays under the cognitive-complexity ceiling (minor finding). - Fix stale near-green-banner.test.ts comment referencing a removed param (minor finding). - Tests: track() helper stamps a fresh rev per cycle (models a real edit); new constant-rev cases assert a flaky/re-run gate is NOT rotation; integration test now writes a genuine per-cycle edit. * fix(#63): scope the raised test timeout to agent-runner.test.ts only Follow-up to the panel's review of the global timeout bump (major/gate-relaxed, agreement:1 across reviewers): a suite-wide 30s ceiling weakened fail-fast for ALL 273 files, not just the load-sensitive integration tests, and could let an unrelated regression that overshoots 5s pass. Root cause (measured): every test in agent-runner.test.ts constructs `new AgentRunner(...).run({ cwd: REPO })`, which pays a fixed ~2s startup cost against the real repo — even the immediate-abort / no-op cases. In isolation the file runs ~1.9s/test, under bun's 5000ms default; only under the full suite's 273-file concurrency does CPU contention inflate that startup past 5s, spuriously timing out and false-BLOCKing the harness-review pre-validate gate. Fix: setDefaultTimeout(15_000) at the top of agent-runner.test.ts — module-scoped, so ONLY this file's ceiling rises. The 5s fail-fast default and its infinite-loop detection stay intact for the other 272 files. 15s is generous margin over the real ~2s work under contention while still failing a genuine hang here. Gate stays FULL; not a relaxation. * fix(#63): split pure config tests out so the raised timeout covers only AgentRunner tests Follow-up to the panel's finding (major/gate-relaxed, agreement:1): file-scoped setDefaultTimeout(15s) also raised the ceiling for the parseAgentSpec and loadAgentSpecs tests, which do NOT construct AgentRunner — over-broadening the relaxation and contradicting the comment's 'every test here' claim. Move those pure config-spec tests to a new agent-specs.test.ts (they belong there anyway — fast, hermetic, no repo). agent-runner.test.ts now contains ONLY AgentRunner-against-repo tests, so setDefaultTimeout(15_000) is precisely scoped to exactly the load-sensitive tests; every other file (including the moved config tests) keeps bun's 5s fail-fast default. Comment corrected to match. 14 tests unchanged (11 AgentRunner + 3 config), all green; typecheck/lint/format clean. * fix(#63): make AgentRunner tests fast at the ROOT (skip whole-repo TS service) — no timeout change Panel BLOCK on the scoped timeout (major/gate-relaxed): a reviewer held that ANY raised ceiling lets a 5-15s regression in these tests pass — a relaxation, even scoped. They're right that the timeout was treating a symptom. Root cause (found): AgentRunner.run calls buildTsService(cwd) when no tsService is passed. With cwd: REPO that builds a TypeScript service over the ENTIRE tsforge repo — ~2s PER test — even for immediate-abort / no-op cases. That fixed cost, not the loop work, blew bun's 5s default under full-suite CPU contention. Fix at the root: pass tsService: null in every run() here (as many sibling tests already do). These read-only tests exercise tool-gating / events / maxTurns / abort / policy — none use the type-aware tools — so a null service is faithful. Result: the file drops from ~24.6s to ~5.3s (11 tests), every test well under the 5s fail-fast default. setDefaultTimeout REMOVED entirely — NO timeout change, so nothing is relaxed; fail-fast + infinite-loop detection are fully intact. (Pure config tests remain split into agent-specs.test.ts from the prior step.) * fix(#63): stub tsService on the two structured-mode runs the replace_all missed + fix stale comment Panel caught (agreement:1, major): the two structured-mode tests use the single-line .run({ provider, cwd: REPO, ... }) form, which my cwd:REPO-line replace_all didn't match — so they still called buildTsService(REPO) (~2s) and the 'every run passes tsService: null' comment was false. Add tsService: null to both; now EVERY run() is stubbed (file ~24.6s -> ~0.4s). Also fix the stale agent-specs.test.ts header comment that referenced a 'raised per-file timeout' (removed in this PR). All 11 AgentRunner + 3 config tests green; typecheck/lint/format clean. * fix(build): forbid the model from running the browser E2E / dev server itself (preflight-trap park) build23 root cause: after reaching fast-gate GREEN, the model ran the browser E2E itself (`cd apps/ui && npx playwright test`, no PLAYWRIGHT_PORT → defaults to 7331), which triggers the scaffold's `bun run dev` webServer whose `preflight-host-dev.sh` HARD-EXITS 1 because the dockerized ui-dev container already holds the port. That's an infra guard, not a code error — the model can't fix it, loops on it (~14×/slice), burns the escalation ladder, and PARKS a feature whose code is already green + tested. Observed on Company AND Contact (both reached GREEN + full CRUD + passing unit tests, then parked identically); Deal/Activity would follow. Very likely the true build22 '0/4 park' cause too — NOT near-green oscillation (#77). The harness runs the full Playwright acceptance automatically after the fast gate (verifyAcceptance, with the correct PLAYWRIGHT_PORT + reuseExistingServer), so the model's self-run is both redundant and harmful. Add an explicit prompt block: the model must NOT run `playwright`/`bun run dev`/`vite`/`dev.sh`; acceptance is harness-owned; when `check` returns passed:true with the required testids, it is DONE and must stop. No gate relaxed — the preflight guard stays; the model is steered away from tripping it. * test(#63b): lock the load-bearing prompt content (command prohibitions, harness-owned acceptance, stop-at-green) Panel finding (agreement:1, missing-test): the assertion only checked the heading + preflight-host-dev.sh, so it would still pass if the actual prohibitions or the stop-at-fast-gate-green instruction were removed. Assert those directly. * style: prettier-format the strengthened refine-prompt test * fix(build): deterministically DENY the model from running the browser E2E / dev server Guidance alone did not stop it (build24: the model ran 'npx playwright test' right after gate-green despite the explicit prompt prohibition, tripped preflight-host-dev.sh, looped, and parked all 4 green slices). Add a deterministic backstop: - policy: mergePolicyRules(base, extra) — append-merge rule sets so a build BACKEND can inject deny rules on top of the user's config (deny-first, so the injected deny wins). - Session.create gains a policyRules input, merged with tsforge.config.json's policy.rules. - BoringStack build injects a shell deny on playwright/bun run dev/vite/dev.sh. The model physically cannot start a second host dev server now; the harness's own acceptance is unaffected (it uses a separate injected exec, not the policy-gated run tool). Verified: the rule denies every invocation form the model used (cd apps/ui && npx playwright test, bunx playwright, bun run dev, vite, preflight+vite) and ALLOWS the gate (bun run check, bun test). No gate relaxed. Pairs with the prompt guidance already on this branch. * fix(acceptance): force PLAYWRIGHT_REUSE_SERVER=true so the harness E2E reuses the dockerized ui-dev THE build22-26 park cause (proven by repro): the scaffold's playwright.config gates reuseExistingServer on '!CI'. In a CI=1 build env reuse flips OFF, so Playwright tries to START its own host dev server on the UI port, collides with the running ui-dev container ('http://localhost:56419 is already used'), and exits with ZERO tests run (.last-run.json: status=failed, failedTests=[]). The harness reads that as a failed acceptance and parks a feature whose code is GREEN — every slice, every build. Repro (ui-dev up): CI=1 (no flag) → 'port already used', run fails; CI=1 + PLAYWRIGHT_REUSE_SERVER=true → 1 passed. Fix: the e2e-runner sets PLAYWRIGHT_REUSE_SERVER=true in the env for both run() and runChain(), forcing reuse regardless of CI. The harness guarantees the dockerized ui-dev is already serving, so Playwright must attach, never self-spawn. No gate relaxed — acceptance still runs the full flow. * fix(acceptance): unique negative-test titles so Playwright doesn't reject the whole spec THE reason acceptance ran ZERO tests (build22-27, masked under the port issue): the E2E generator titled every negative 'negative: <Entity> rejects <field>=<value>'. Two negatives can share the same (field,value) — a required-field rule and a mustNotHappen both yield name='' (the codebase intentionally keeps both, see 'FIX 7') — so two tests got the IDENTICAL title. Playwright HARD-REJECTS a file with duplicate titles: the spec fails to collect, zero tests run, .last-run.json = {status:failed, failedTests:[]}, and the harness reads a GREEN feature as a failed acceptance → park. Fix: append the negative's [index] to the title. parseStep matches on the 'negative: ' PREFIX, so classification is unaffected. Proven by direct repro against the live app: before = 'duplicate test title' error, 0 tests; after = full flow runs (nav/list/create/persist/update/negative pass; delete fails on a REAL app bug the acceptance now correctly catches). Regression test added. * fix(acceptance): confirm-delete testid must be on the CONFIRM BUTTON, not the dialog wrapper The delete acceptance step failed on EVERY build (build22-29) with a real, deterministic cause: the testid guide said to put company-confirm-delete on 'the confirmation dialog', so the model put it on the dialog overlay <div> (fixed inset-0 backdrop). The E2E delete step CLICKS that testid to confirm — clicking the backdrop hits nothing, the delete mutation never fires, the row stays, and toHaveCount(0) fails. The model's delete logic (mutation + invalidateQueries on the correct key) was CORRECT the whole time; only the testid placement was wrong, and the guide invited it. Fix: guide now says confirm-delete goes on the BUTTON whose onClick fires the delete (never the wrapper/overlay). Found by running the harness acceptance directly against build29's parked-but- green Company and reading CompanyPage.tsx: testid on line 190 = overlay div; the real confirm button (onClick=onConfirmDelete) had no testid. Regression test added. * fix(acceptance): scope the sidebar's co-located test so the model keeps its nav-link count in sync build30 got Company to 'verified ✓' (full browser acceptance passed — delete included) but the build stayed 'stuck': the FINAL full-project validate ran the vitest suite and AppSidebar.test.tsx failed — 'expected length 6, got 7' — because the model correctly ADDED its Company NavLink (reachability requires it) and the scaffold test hard-codes the base nav-link count. Every feature addition breaks it; the per-feature fast gate (bun run check) doesn't run vitest, so it only surfaces at the final validate. The scaffold is an external clone we can't edit, so: add AppSidebar.test.tsx to the feature's editable scope and instruct the model (refinePrompt) to bump the sidebar test's expected link count by its one added link. On-pattern with how schema/locale/routes are model-owned + kept consistent. Tests: scope includes the sidebar test; prompt tells the model to bump the count. * feat(acceptance): create step surfaces the root cause when a row never appears (API/console capture) The create/persist acceptance step failed as an opaque 'getByTestId(company-row)... toBeVisible timeout' — giving the model (and me) no idea WHY, so each park needed a manual repro to diagnose and the steer couldn't guide a fix. Now the create step records every /api/ mutation (+ status) and console errors, and appends them to the failure: - POST 4xx present -> API rejected the payload (contract/validation bug) - POST 2xx present -> the mutation worked; the LIST/row render is the bug - no mutation fired -> the form submit isn't wired to the create mutation (build31's actual cause) Proven via direct repro against build31's parked Company: the message now reads 'No API mutation fired at all — the create form's submit is not wired to the create mutation', which is exactly right. This makes acceptance failures self-explaining for the model's steer instead of a symptom. (create step first — the first mutation step; persist/update/delete are a follow-up.)
…l-closed everywhere (panel round-4) Fourth panel BLOCK (4 reviewers, 0 errored), addressed: - scope-bypass (ag2): the NULL branch left the base 'REWRITE IT IN FULL' message intact even though no file was confirmed in scope. enrichUnparsable now ALWAYS replaces the message; the not-located case asserts NO in-scope rewrite target (points the model at its own recent edits, else blocked). - major/other correlation (ag1, the deep one): locateParseError scanned apps/api-then-ui blindly, so a UI cascade + an unrelated broken API file → wrong file named, feature wrongly blocked. Now scan ONLY the app(s) whose ::tsforge-app section actually shows the parserOptions.project cascade (appsWithParseCascade); fall back to both only when the section is unclear. New correlation test. - missing-test wiring (ag2): composeBoringstackGate.scopeGlobs is now REQUIRED — dropping it at the build.ts call site is a COMPILE error, not a silently-green test (it already caught 2 stale call sites in boringstack-acceptance-wiring). Plus real-scopeFor compose wiring tests. - major/other robustness (ag1): per-file safeFirstSyntaxError so a createProgram/read throw skips ONLY that file, never aborts the whole app scan. - minor: empty/default scope is FAIL-CLOSED (blocked, never rewrite); set unparsable.file only when CONFIRMED in scope so stuck-file/expert consumers agree without risking out-of-scope hiding. Full core suite 3042 pass; 1 pre-existing flaky timeout (clean on rerun, #63 class). LIVE: build34 Company crossed the near-green oscillation at turn 136 → final acceptance GREEN on this branch.
Summary
/exitand/quitshutdown plus overlay input ownership/palette and@picker triggers in default editor modeRoot cause
The multiline editor became the default TTY input owner, but parts of the REPL still assumed readline-owned input. Some editor rendering code also reported clamped row counts while returning unclipped frames.
Validation
bun run validate