[Bug]: Word-wrap setting leaves open code blocks on old value #35898
Send to my agentSummary#
Changing the word-wrap default in Settings leaves already-open chat code blocks on the previous value, even without a local wrap override. Open views should follow the changed default until the user changes their local wrap control. A focused browser test reproduced the stale chat block beside a new block that followed the setting; source traces show the same mount-time state pattern in the diff pane and pull request Code tab.
Before submitting#
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Area#
apps/web
Steps to reproduce#
- In the browser test below, set the client word-wrap default to false and mount a chat code block.
- Change the client setting to true through the same persistence function used by the Settings update path, keep the original block mounted, and mount a second block as a control.
- Compare both blocks'
data-wrapvalues. The test failed with the old block false and the new block true in two completed runs.
Save the following as repro.sh and run sh repro.sh ./t3code-checkout from a macOS host with the checkout dependencies and Playwright Chromium installed. It prints the tool versions, uses disposable home, temporary, and T3 state directories, writes one uniquely named browser test in the checkout, and removes that test on exit. Exit 1 with RESULT: defect reproduced means the control passed and the mounted block stayed stale; exit 0 means the expected behavior passed; exit 2 means setup or the control was inconclusive.
Reproduction script: repro.sh
#!/bin/sh
set -eu
checkout=${1:?pass checkout path}
browser_cache="$HOME/Library/Caches/ms-playwright"
scratch=$(mktemp -d)
test_file="$checkout/apps/web/src/components/chat/WordWrap.y11-01.browser.test.tsx"
if [ -e "$test_file" ]; then
echo 'RESULT: inconclusive'
exit 2
fi
trap 'rm -f "$test_file"; rm -rf "$scratch"' EXIT HUP INT TERM
export HOME="$scratch/home" TMPDIR="$scratch/tmp" T3CODE_HOME="$scratch/t3"
export PLAYWRIGHT_BROWSERS_PATH="$browser_cache"
mkdir -p "$HOME" "$TMPDIR" "$T3CODE_HOME"
uname -sm
node --version
vp --version
cat > "$test_file" <<'TEST'
import { expect, it } from "vite-plus/test";
import "~/index.css";
import { render } from "../../../test/render";
import { ensureClientSettingsHydrated, persistClientSettingsPatch } from "../../hooks/useSettings";
import { MarkdownCodeBlock } from "./MarkdownCodeBlock";
const block = (key: string) => (
<MarkdownCodeBlock
key={key}
code="a long line"
language="text"
fenceTitle={null}
theme="light"
isStreaming={false}
>
<pre><code>a long line</code></pre>
</MarkdownCodeBlock>
);
it("applies a changed word-wrap default to an already-mounted code block", async () => {
await ensureClientSettingsHydrated();
await persistClientSettingsPatch({ wordWrap: false }, async () => {});
await using screen = await render(<>{block("old")}</>, { router: false });
const wraps = () => [...screen.container.querySelectorAll(".chat-markdown-codeblock")]
.map((node) => node.getAttribute("data-wrap"));
expect(wraps()).toEqual(["false"]);
await persistClientSettingsPatch({ wordWrap: true }, async () => {});
await screen.rerender(<>{block("old")}{block("new")}</>);
const observed = wraps();
expect(observed[1]).toBe("true");
console.log("CONTROL_OK new block follows changed setting");
console.log(`OBSERVED first=${observed[0]} new=${observed[1]}`);
expect(observed[0]).toBe("true");
});
TEST
if (cd "$checkout" && vp test run apps/web/src/components/chat/WordWrap.y11-01.browser.test.tsx > "$scratch/test-output" 2>&1); then
cat "$scratch/test-output"
echo 'RESULT: expected behavior'
exit 0
fi
cat "$scratch/test-output"
if grep -q 'CONTROL_OK new block follows changed setting' "$scratch/test-output" &&
grep -q 'OBSERVED first=false new=true' "$scratch/test-output"; then
echo 'RESULT: defect reproduced'
exit 1
fi
echo 'RESULT: inconclusive'
exit 2
Expected behavior#
The Settings control promises to wrap long lines in code blocks, diffs, and file previews by default (apps/web/src/components/settings/TypographySettings.tsx#L241-L262). An open view without a local wrap override should follow a changed default, while its own wrap control may override that value. The existing file preview implements that live-default and nullable-override behavior (apps/web/src/components/files/FilePreviewPanel.tsx#L1058-L1060); the chat route retains its thread beneath Settings instead of remounting it (apps/web/src/routes/_chat.tsx#L25-L40, apps/web/src/components/ThreadRouteView.tsx#L225-L234).
Actual behavior#
In a focused browser test, a code block mounted with word wrap off kept data-wrap="false" after the client setting changed to true; a second code block mounted afterwards had data-wrap="true". The old block's wrap button also derives its label and pressed state from that unchanged local value (apps/web/src/components/chat/MarkdownCodeBlock.tsx#L164-L165, apps/web/src/components/chat/MarkdownCodeBlock.tsx#L223-L232). Source trace: the diff pane and pull request Code tab each initialize their wordWrap state once from the setting and use that local state for their wrap controls and diff rendering (apps/web/src/components/DiffPanel.tsx#L223-L237, apps/web/src/components/DiffPanel.tsx#L1216-L1229, apps/web/src/components/pullRequest/PullRequestCodeTab.tsx#L382-L399, apps/web/src/components/pullRequest/PullRequestCodeTab.tsx#L1384-L1397).
Impact and scope#
Impact: P3 - word wrapping is a presentation preference; the underlying code and diff content remain available.
The browser test proves the chat code block case. The same stale-state result for an open diff pane and pull request Code tab follows from the cited state and rendering paths but was not separately run. The desktop renderer uses this web view, but the desktop shell was not separately run. The path is deterministic while those views remain mounted. Reloading the page or closing and reopening a view lets it read the new default; this was inferred from the initialization path, not separately tested.
Earliest failure stage#
State or storage
Analysis or suspected cause#
Source: the affected views copy the shared word-wrap default into local component state at mount, then use only their local controls to change that state. The parent chat route keeps the old component instances while Settings is open. The browser test establishes that this is observable for chat code blocks.
Additional context#
- The first attempt at this reproduction stopped before tests because the isolated home hid Playwright Chromium. The second attempt stopped before tests because the temporary test had an incorrect import path. After correcting both setup mistakes, two runs reproduced the stale mounted block.
- A related source-only pattern exists for
diffIgnoreWhitespacein the pull request Code tab (apps/web/src/components/pullRequest/PullRequestCodeTab.tsx#L387-L399); that separate setting was not exercised by the reproduction script.
The Settings switch updates wordWrap (apps/web/src/components/settings/TypographySettings.tsx#L241-L262), and the client patch path replaces its snapshot and notifies subscribers (apps/web/src/hooks/useSettings.ts#L97-L105, apps/web/src/hooks/useSettings.ts#L209-L239). The code block instead reads the snapshot only in a useState initializer (apps/web/src/components/chat/MarkdownCodeBlock.tsx#L164-L165). In the diff pane and Code tab, later settings renders do not reinitialize their local wordWrap state (apps/web/src/components/DiffPanel.tsx#L223-L237, apps/web/src/components/pullRequest/PullRequestCodeTab.tsx#L382-L399).
Impact#
Cosmetic issue
Version or commit#
main @ c00a373
Environment#
Darwin arm64; Node.js v26.12.1; Vite+ CLI v1.5.1 with repository-local vite-plus v1.4.0 and Vitest v5.0.5; Chromium browser test.
Logs or stack traces#
The focused browser test printed this before the expected assertion failure; checkout paths and unrelated browser-runner output were omitted.
CONTROL_OK new block follows changed setting
OBSERVED first=false new=true
AssertionError: expected 'false' to be 'true'
RESULT: defect reproduced
Screenshots, recordings, or supporting files#
No response
Workaround#
Reloading the page or closing and reopening the view should let it read the changed default, based on the initialization path; this was not separately tested.
References#
- https://github.com/pingdotgg/t3code/pull/3480 — introduced the persisted default and initially seeded per-view state from it.
- https://github.com/pingdotgg/t3code/pull/26432 — separated file-preview wrap controls from the global setting; the file preview now retains a live default under a local override.
- https://github.com/pingdotgg/t3code/pull/8004 — a closed, unmerged proposal for the different problem of local code-block wrap choices resetting after virtualizer remounts. The maintainer called that problem real but rejected making each local toggle change the global setting.
tanstack-source added labelsbug
juliusmarminge closed this
Sign in to join this conversation.