diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f0054c1..97d98d55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ All notable user-visible changes to Hunk are documented in this file. ### Fixed +- Preserved the resolved auto theme across `--watch` refreshes instead of falling back to the default dark theme. - Included the bundled Hunk review skill in standalone prebuilt release archives so `hunk skill path` works after extracting a tarball or installing via Homebrew. ## [0.12.0] - 2026-05-12 diff --git a/src/ui/App.tsx b/src/ui/App.tsx index 5f4239f3..13bc77bf 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -107,6 +107,8 @@ export function App({ ? "auto" : resolveTheme(bootstrap.initialTheme, bootstrap.initialThemeMode ?? null).id, ); + // Soft reloads replace bootstrap without re-running startup terminal theme detection. + const [detectedThemeMode] = useState(() => bootstrap.initialThemeMode); const [showAgentNotes, setShowAgentNotes] = useState(bootstrap.initialShowAgentNotes ?? false); const [showLineNumbers, setShowLineNumbers] = useState(bootstrap.initialShowLineNumbers ?? true); const [wrapLines, setWrapLines] = useState(bootstrap.initialWrapLines ?? false); @@ -120,7 +122,7 @@ export function App({ const [resizeDragOriginX, setResizeDragOriginX] = useState(null); const [resizeStartWidth, setResizeStartWidth] = useState(null); - const activeTheme = resolveTheme(themeId, bootstrap.initialThemeMode ?? null); + const activeTheme = resolveTheme(themeId, detectedThemeMode ?? null); const review = useReviewController({ files: bootstrap.changeset.files }); const filteredFiles = review.visibleFiles; const selectedFile = review.selectedFile; diff --git a/src/ui/AppHost.interactions.test.tsx b/src/ui/AppHost.interactions.test.tsx index 61e52267..0338d12c 100644 --- a/src/ui/AppHost.interactions.test.tsx +++ b/src/ui/AppHost.interactions.test.tsx @@ -434,6 +434,29 @@ async function waitForFrame( return frame; } +/** Open the top-level Theme menu and wait for the expected active light theme marker. */ +async function openThemeMenu(setup: Awaited>) { + await act(async () => { + await setup.mockInput.pressKey("F10"); + }); + + const openedFrame = await waitForFrame( + setup, + (frame) => frame.includes("Toggle files/filter focus"), + 12, + ); + expect(openedFrame).toContain("Toggle files/filter focus"); + + for (let index = 0; index < 3; index += 1) { + await act(async () => { + await setup.mockInput.pressArrow("right"); + }); + await flush(setup); + } + + return waitForFrame(setup, (frame) => frame.includes("[x] Paper"), 12); +} + async function pressHunkNavigationKey( setup: Awaited>, key: "]" | "[", @@ -1255,6 +1278,55 @@ describe("App interactions", () => { } }); + test("watch mode preserves the resolved auto theme after refreshing the file diff", async () => { + const dir = mkdtempSync(join(tmpdir(), "hunk-watch-theme-")); + const left = join(dir, "before.ts"); + const right = join(dir, "after.ts"); + + writeFileSync(left, "export const answer = 41;\n"); + writeFileSync(right, "export const answer = 42;\n"); + + const bootstrap = await loadAppBootstrap({ + kind: "diff", + left, + right, + options: { + mode: "split", + theme: "auto", + watch: true, + }, + }); + // loadAppBootstrap does not do startup-time terminal theme detection in tests. + bootstrap.initialThemeMode = "light"; + + const setup = await testRender(, { + width: 220, + height: 20, + }); + + try { + await flush(setup); + + writeFileSync(right, "export const answer = 42;\nexport const added = true;\n"); + + const refreshedFrame = await waitForFrame( + setup, + (currentFrame) => currentFrame.includes("export const added = true;"), + 40, + ); + expect(refreshedFrame).toContain("export const added = true;"); + + const menuFrame = await openThemeMenu(setup); + expect(menuFrame).toContain("[x] Paper"); + expect(menuFrame).toContain("[ ] Graphite"); + } finally { + await act(async () => { + setup.renderer.destroy(); + }); + rmSync(dir, { force: true, recursive: true }); + } + }); + test("a shows notes that are visible in the current review viewport", async () => { const bootstrap = createBootstrap(); bootstrap.changeset.files[1]!.agent = {