From 4e2c641449336fafa970c620dd128d48940d294d Mon Sep 17 00:00:00 2001 From: Artem Loenko Date: Fri, 15 May 2026 07:00:55 +0100 Subject: [PATCH 1/3] fix(ui): preserve auto theme across watch refreshes --- CHANGELOG.md | 1 + src/ui/App.tsx | 4 +- src/ui/AppHost.interactions.test.tsx | 79 ++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 1 deletion(-) 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..a56dd1c1 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 = useRef(bootstrap.initialThemeMode).current; 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..108c4af0 100644 --- a/src/ui/AppHost.interactions.test.tsx +++ b/src/ui/AppHost.interactions.test.tsx @@ -434,6 +434,36 @@ 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>) { + let opened = false; + + for (let attempt = 0; attempt < 2; attempt += 1) { + await act(async () => { + await setup.mockInput.pressKey("F10"); + }); + + const menuFrame = await waitForFrame(setup, (frame) => + frame.includes("Toggle files/filter focus"), + ); + if (menuFrame.includes("Toggle files/filter focus")) { + opened = true; + break; + } + } + + expect(opened).toBe(true); + + 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 +1285,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 = { From 9f29fe3d82237017bea2387505bac72ecab1e80e Mon Sep 17 00:00:00 2001 From: Artem Loenko Date: Fri, 15 May 2026 07:14:00 +0100 Subject: [PATCH 2/3] refactor(ui): use state for detected theme snapshot --- src/ui/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/App.tsx b/src/ui/App.tsx index a56dd1c1..13bc77bf 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -108,7 +108,7 @@ export function App({ : resolveTheme(bootstrap.initialTheme, bootstrap.initialThemeMode ?? null).id, ); // Soft reloads replace bootstrap without re-running startup terminal theme detection. - const detectedThemeMode = useRef(bootstrap.initialThemeMode).current; + 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); From 7421ba481d4bccdca95ff8165e6ff9ec69c73a5d Mon Sep 17 00:00:00 2001 From: Artem Loenko Date: Fri, 15 May 2026 07:19:01 +0100 Subject: [PATCH 3/3] test(ui): avoid toggling menu in watch theme test --- src/ui/AppHost.interactions.test.tsx | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/src/ui/AppHost.interactions.test.tsx b/src/ui/AppHost.interactions.test.tsx index 108c4af0..0338d12c 100644 --- a/src/ui/AppHost.interactions.test.tsx +++ b/src/ui/AppHost.interactions.test.tsx @@ -436,23 +436,16 @@ async function waitForFrame( /** Open the top-level Theme menu and wait for the expected active light theme marker. */ async function openThemeMenu(setup: Awaited>) { - let opened = false; - - for (let attempt = 0; attempt < 2; attempt += 1) { - await act(async () => { - await setup.mockInput.pressKey("F10"); - }); - - const menuFrame = await waitForFrame(setup, (frame) => - frame.includes("Toggle files/filter focus"), - ); - if (menuFrame.includes("Toggle files/filter focus")) { - opened = true; - break; - } - } + await act(async () => { + await setup.mockInput.pressKey("F10"); + }); - expect(opened).toBe(true); + 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 () => {