Repository navigation
fix(core): Claude Code compatibility fixes (#167-#177) - #178
Merged
Merged
Conversation
) xterm's DOM renderer underlines a hovered link by replacing the row's contents, with no onRender, so the ink was wiped until a scroll redrew the rows. A MutationObserver inks any replaced row that holds no ink. The e2e hovers and leaves a link on the alternate screen (failed before the fix). Core 0.28.3, app 0.35.3. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t
One spec and one plan for the eleven issues from the 2026-10-10 compat research, in five build groups: G1 #167; G2 #168 + #172; G3 #169 + #173; G4 #171 + #176 + #177 + #174; G5 #170 + #175. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t
) One tab's found path woke every tab's link painter, and each repainted its whole scrollback in one task (Stable's diag log, 2026-10-10 16:58Z: 2009 ms and 2046 ms page stacks in scan around a 2153 ms term:path-kinds). - termPathLinks: listeners, queued and in-flight keys are kept per owner (the session id); a hit wakes only the owners that asked, a later asker joins an in-flight key; takeAsked tells the painter which lines wait on an answer. - termLinkPaint: revisit() repaints only the waiting scrollback lines (cap 500 markers) and leaves the live screen to the next pass; on the alternate screen it redraws only the rows holding a link. A full pass paints the live screen, then the scrollback bottom up in slices of 8 ms or less, a marker as the cursor; a crumb when a pass takes over 50 ms. #163's watchRows is unchanged. - linkScanPlan (pure): liveRange, sliceRows. Headless bench: one full pass's text work alone is 65-79 ms over 10,000 lines; sliced, the longest slice is 8.0 ms. - e2e linkPaintStall (new, not run here): no long task of 200 ms or more when a path lights up beside a 10,000-line tab, or on a theme switch, and the backlog finishes within 10 s. A core change: it ships to Prism too. onPathsFound now takes the owner first. Fixes #167 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t
#172) OSC 10 and 11 queries are answered from the theme on the ground the panel really paints, not xterm's clear canvas (which answered rgb:0000/0000/0000, so Claude's "auto" theme was dark on every ground). Mode 2031 is followed from the stream; a look change that flips light/dark pushes CSI ? 997 ; 1|2 n once to a program that turned reports on, CSI ? 996 n is answered, and DECRQM 2031 reports set/reset. Light/dark is the chrome's own measurement (luminance > 0.4), pinned to agree with chromeTokens on every preset. Measured (Claude Code 2.1.296, bare node-pty + bundled ConPTY): Claude does NOT send DECRQM ?2031$p before trusting 2031 (it writes ?2031h straight away), never asks ?996n or OSC 10, and on a pushed ?997;2n re-asks OSC 11 and switches ("detected=dark" to "detected=light" in its debug log). All six queries and replies pass through the bundled ConPTY untouched. New must-not-regress rule replies-only-when-asked; CLAUDE.md rule 2 links it. New e2e scenario termReplies (not run here; the Gate runs it). Core change: reaches Prism with the core release, no new TermHostConfig field. Fixes #168 Fixes #172 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t
…169) xterm 6.0's default for an OSC 8 link asked confirm() and then opened a blank window that main denies, on a click of any button. A linkHandler now opens http(s) through openExternal on a LEFT click only (the right one is the menu's), allowNonHttpProtocols off, never a confirm. The cells are painted from the stream (core/renderer/lib/termOsc8.ts): the cursor at the open and at the close bracket the label, a marker holds the line on the normal screen, and a span stands only while its cells hold the text it was printed with. Both screens paint them; the menu's Copy link / Open link take the uri. Measured 2026-10-11 (node-pty with the bundled ConPTY, @xterm/headless): Claude Code 2.1.296 (haiku) printed "[OSC8LABEL](https://example.com/osc8)" as one OSC 8 link on the alternate screen, and following the stream found "OSC8LABEL" at row 9, cells 2 to 11; a pwsh OSC 8 line on the normal screen, and one wrapping from cell 35 of a 40-cell row, came out exact too. New e2e scenario osc8Links (not run here: the Gate runs e2e). Fixes #169 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t
ptyEnv sets FORCE_HYPERLINK=1 for every shell unless the user's environment already has it, under any spelling and with any value (0 included: their choice). It lands after #169's linkHandler, so no link ever becomes a dead confirm box. Measured 2026-10-11 (Claude Code 2.1.296 haiku, node-pty with the bundled ConPTY, WT_SESSION and TERM_PROGRAM removed): with FORCE_HYPERLINK=1 one OSC 8 link for a markdown link, without it none. Who reads it (searched the same day): Claude Code (its bundled supports-hyperlinks; Claude's own "hyperlinks" setting still wins over it), the globally installed vercel CLI. Not Codex (codex.exe has no such string), and no package in this repo's node_modules. Fixes #173 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t
) xterm 6.0.0 has no XTVERSION handler, and Claude Code turns on synchronized output (?2026) only after a reply. CSI > q and CSI > 0 q now answer DCS >|PrismTerminal <core version> (xterm.js <version>) ST through term.input(.., false); any other parameter stays xterm's. The name does not start with "xterm.js", which Claude reads as VS Code's terminal. Both apps answer PrismTerminal: the emulator is this core (spec decision 5). The versions are constants in core/shared/termVersion.ts (Prism's renderer cannot import package.json); termVersion.test.ts holds them to core/package.json and node_modules/@xterm/xterm, and core/README.md's release section says so. Measured (Claude Code 2.1.296 --model haiku over the bundled ConPTY, xterm 6.0's other replies stood in, one streamed answer, 45 s): no reply, synchronizedOutput=no and 0 ?2026h; with this reply, "terminal identified as PrismTerminal 0.32.0 (xterm.js 6.0.0)", synchronizedOutput=yes and 65 frames wrapped in ?2026h/?2026l. e2e termReplies extended: CSI > q and CSI > 0 q each get the reply once, CSI > 1 q none, all before the DA1 sentinel. Not run here (the Gate runs e2e). Fixes #171 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t
xterm 6.0.0 has no OSC 52 handler, so a program's copy (Claude Code's /copy, anything over ssh) was dropped. The panel now handles every OSC 52 (none is drawn): parseOsc52 (core/renderer/lib/termOsc52.ts, pure) takes only the "c" or empty selection, base64 of strict UTF-8, at most OSC52_MAX (1 MB of text, core/shared/termLimits.ts), and ignores a clear. A READ (?) is swallowed and never answered: a program in a tab must not read the user's clipboard. The write goes through main on its own channel, clipboard:term-write (TermApi.writeClipboardFromTerm, createTermApi, registerTermIpc), which checks the string and the cap again and refuses rather than trims; clipboard:write's 4000 cap stays the help panel's. The channel is opaque to the diagnostics log like the other clipboard write. A copy that landed raises the Copied badge. Both apps get it through the core's bridge with no host change. PRIVACY.md says what programs in a tab can and cannot do. Tests: termOsc52.test.ts, ipc.clipboard.test.ts (the new channel), ipcTiming.test.ts (the copied text is never logged). e2e termClipboard added: a write lands with the badge, a read gets no reply and leaves the clipboard, a "p" selection changes nothing. Not run here (the Gate runs e2e). Fixes #176 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t
The bell was silent: xterm fires onBell and nothing listened. Decided under the owner's delegation (spec decision 1): a bell flashes the taskbar button while the window is NOT focused, does nothing while it is (most often the user's own bad key; PSReadLine rings on one), and never makes a sound. - Core (both apps): term.onBell goes through the pure bellGate (at most one a second per tab; a cat of a binary rings hundreds of times) to TermApi.termBell?(id), channel term:bell, and registerTermIpc hands it to the new optional TermIpcDeps.attention. A bridge member, not a TermHostConfig field (spec decision 2): the behaviour is the same in both apps, the host only lends its window. No new TermHostConfig field. - This app: src/main/bellFlash.ts flashes an unfocused window and stops the flash on focus (one handler per window). Under --e2e it counts on globalThis.__e2eFlashes and flashes nothing. - Prism: needs one line (attention in its registerTermIpc call) in its core-release PR; until then its bell stays silent, as today. Tests: bellGate in termReplies.test.ts, term:bell in ipc.clipboard.test.ts, bellFlash.test.ts. e2e bell added (one bell one flash, ten in a burst one); not run here (the Gate runs e2e). Docs: regression-rules #bell-flashes, core/README.md, docs/two-apps.md. Fixes #177 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t
xterm.js #6117 (open in 6.0.0): the viewport syncs its scroll range in a render callback, and the renderer is paused while a tab's element is detached, so a tab resized while hidden could come back with a stale slider until the next scroll. The attach now runs a second pass after a frame and a task, once the renderer has resumed: refit() (a no-op when the size already matches; it calls suppressActivity itself) and term.refresh over every row, which runs the render callbacks the sync waits on. Cancelled in the effect's cleanup, and skipped if the element has moved. Public xterm API only. NOT REPRODUCED: the build group runs no e2e, so the plan's red run of the new hiddenResize scenario on the code before this is left to the Gate. The refresh is included up front (the spec's step 2 fallback), since a second fit alone cannot change anything when the size already matches. e2e hiddenResize added: tab B on the alternate screen with mouse tracking, the window shrunk by 300 px while B is hidden, back on B the slider is hidden or fills its track and the program's frame fills the rows. Fixes #174 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t
…the title (#170, #175) #170: an image on Ctrl+V sends the AGENT's key. Claude Code 2.1.296 binds image paste to alt+v on Windows (read in its binary: `Me=Ie?"alt+v":"ctrl+v"`), so the ^V we forwarded did nothing on a fresh install; the owner's keybindings.json binds both, which hid it. `imagePasteKey`: ESC v to Claude, ^V to everything else, exactly one (two would paste twice where both are bound). Measured through the bundled ConPTY: ESC v reaches Claude as alt+v. The help entry says so. #175: Shift+Enter's newline, measured 2026-10-11 through the bundled ConPTY, typing `ab` (and `a\`) into an empty prompt, then each candidate, then `c`: bytes Claude 2.1.296 Codex 0.153.2 `\` + CR newline, no `\` newline, no `\` LF (Ctrl+J) newline DROPPED (`abc`) ESC CR newline newline Nothing submitted; a typed trailing `\` survived everywhere. LF is not a newline in both, so per the spec's rule `newlineKey` is per agent: Ctrl+J (Claude's own chat:newline) for Claude, `\` + CR (today's) otherwise. Claude's title glyphs and Codex's "Action Required" now arm Shift+Enter before the poll's first verdict (`titleArmsAgent`); a bare braille spinner does not (any CLI draws one, and a plain shell would stay armed, #21). Only the poll takes it back. Keys are still heard on onKey (rule 10). New e2e `imagePaste` (the gate runs it). Fixes #170 Fixes #175 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t
- An OSC 8 label that shows another host's address opens what it shows (osc8Target): xterm's OSC 8 provider outranks the visible-text match, so https://github.com/... printed over a hidden evil.example opened evil.example with no confirm. The menu's Copy/Open link follow it. - The app no longer sets FORCE_HYPERLINK (#173): supports-hyperlinks reads it before its isTTY test (read in claude.exe 2.1.296), so it put raw OSC 8 into every pipe and redirect, Claude's Bash tool commands among them. - Theme reports (?2031h) end with their program: the prompt report, the poll's agent-left and RIS turn them off, so a later theme switch no longer types ESC[?997;1n into a pwsh prompt after a killed Claude. - A click on an OSC 8 label no longer also walks the caret. - The prompt report takes back agent keys only a title armed (agentArm): a WSL tab's poll never says left, so Shift+Enter stayed Ctrl+J at pwsh. - Claude under WSL wants Alt+V too: comment and spec corrected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t
…ct keeps theme reports - Shift+Enter under an agent sent Ctrl+J and THEN a CR: returning false only stops xterm's keydown, and xterm sent Enter on the keypress that followed, so Claude submitted. preventDefault cancels the keypress (e2e imagePaste measured "IN C 0a" then "IN C 0d"). - The review's agent-left reset of ?2031h fired on the poll's first "no agent" verdict too, so a program that asked for reports before it landed heard no theme switch (e2e termReplies). Only an agent that was there and leaves ends them now (themeReportsOnPoll, tested). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t
) The MutationObserver re-inked every replaced alternate-screen row holding no ink, so each row onRender had just scanned was scanned again, and it stayed attached on the normal screen. It now re-inks only a row that held ink and holds none (rowsToReink, unit tested), and disconnects when leaving the alternate screen; the first onRender there attaches it again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t
termLinkPaint.ts: the row watch re-inks only rows that lost ink (#163), and inkRow keeps its screen row for the OSC 8 spans. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t
…t goes with its title Review findings on #178: - A first "no agent" from the poll is no departure: theme reports end only when an agent the poll saw leaves, and a resume's agent is asked about again rather than disarmed (agentArm's gone and recheck). - An agent only its title vouches for (WSL: no prompt report, the poll sees wsl.exe) is disarmed, reports off, when the title stops naming it. - The shell's prompt takes every agent key back at once, and asks main's poll for its next answer even if unchanged (term:agent-again, optional termAgentAgain on TermApi), so a Codex restarted between looks is armed again. - The prompt's arm and report reset live in the one OSC 9 handler. - On the normal screen a URL match inside an OSC 8 span is painted once (piecesOverlap). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t
The owner saw Claude stay dark after a dark-to-light switch on 0.38.0. Traced in the app and over bare node-pty: PT pushes ?997;2n, Claude asks OSC 11 again and detects light, then redraws only its live frame; rows it already committed keep the old palette. Claude Code's side; the PT-side options (Ctrl+L, a resize nudge) break two rules and are the owner's call. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t
Owner, 2026-10-11, choosing "Tiny resize nudge": Claude Code 2.1.296 hears the ?997 push and switches palette but redraws only its live frame, so text it drew before kept the old colours. After a push the program asked for, the panel resizes that terminal one column narrower and refits it back, once the tab is shown (the theme is switched in Settings, so it is usually hidden). A resize, never a key. e2e termReplies: the stand-in sees 151 then 152. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t
# Conflicts: # core/package.json # core/renderer/lib/termLinkPaint.ts # package-lock.json # package.json
github-actions Bot
pushed a commit
that referenced
this pull request
Oct 11, 2026
Claude Code compatibility fixes from the 2026-10-10 research, plus the redraw nudge after a theme push (owner, 2026-10-11). #173 (FORCE_HYPERLINK) was withdrawn and closed as not planned. Fixes #167 Fixes #168 Fixes #169 Fixes #170 Fixes #171 Fixes #172 Fixes #174 Fixes #175 Fixes #176 Fixes #177 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the Claude Code compatibility issues from the 2026-10-10 research. Stacked on #165 (both touch
termLinkPaint.ts); retarget to main before #165 merges.Fixes #167, Fixes #168, Fixes #169, Fixes #170, Fixes #171, Fixes #172, Fixes #174, Fixes #175, Fixes #176, Fixes #177
#173 stays open (withdrawn here).
FORCE_HYPERLINK=1was added and then taken out after review:supports-hyperlinks(inside claude.exe 2.1.296) honours it before its isTTY check, so piped and redirected output from every child would carry raw OSC 8 escapes. The only other signals Claude accepts would mean claiming to be another terminal. Owner decision on whether to pursue it differently. (The squash message must not keep theFixes #173line from commit 884ca47.)What changes
link-paintcrumb in the diag log.replies-only-when-asked.PrismTerminal 0.32.0 (xterm.js 6.0.0). Measured: Claude then wraps frames in ?2026 (65 synchronized frames in 45 s, against 0 before).cselection, capped at 1 MB, and reads are never answered. A Copied badge shows, and the copied text is never written to the diag log.PRIVACY.mdis updated.attention. Prism's bell stays silent until Prism adds one line of wiring.hiddenResizee2e only guards against it.\+ Enter to everything else. This was measured: Codex drops LF.Core change
Core 0.32.0 and app 0.38.0. This ships to Prism. No new
TermHostConfigfield. New optional bridge members:TermApi.writeClipboardFromTerm?,termBell?, andTermIpcDeps.attention?.Verification
settingsLookfails only its 8 known checks, which are fixed in feat: see-through window, theme order and Most recent tab switching (#156, #157, #158) #161.linkPaintStall,termReplies,osc8Links,termClipboard,bell,hiddenResize,imagePaste.Known gap
In a WSL tab, after Claude exits inside WSL bash, Shift+Enter and image paste keep the agent's keys until you
exitback to pwsh or cmd. This is because WSL's prompt does not report itself.Check by hand with a real Claude
🤖 Generated with Claude Code
https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t