feat(theme): add /theme command with markdown rendering fixes#80
Merged
Conversation
Implement `/theme` as a persistent, interactive theme picker with live preview. Theme selection should affect explicit app UI colors, `CodeBlock` Shiki highlighting, and markdown rendering. For ambiguous families, expose explicit light/dark variants in the picker and map them directly in Shiki, while `marked-terminal` continues using its single family theme. Behavior: - `/theme` opens a picker - moving through options previews the theme immediately - `Enter` saves - `Esc` restores the previously active theme without saving Key Changes: - Extend `Config` with `theme: ThemeId`, defaulting to `github-dark`. - Add a central typed theme registry with explicit presets: - `github-light` - `github-dark` - `nord` - `dracula` - `solarized-light` - `solarized-dark` - Each preset definition should include: - Shiki theme name - `marked-terminal` theme name - semantic UI colors for accent, border, secondary text, error, approval/warning, message-role accents, and footer mode colors - Add `/theme` to slash-command discovery. - Add a `ThemeSettings` screen with live preview state. - Wire `App` to manage: - persisted config theme - transient preview theme while the picker is open - Replace hard-coded explicit colors with semantic theme tokens in: - header - footer - messages - search/settings screens - approval/error UI - code block border/text styling - Update markdown rendering so it receives the active theme mapping instead of fixed `gitHub`. - Update `CodeBlock` so Shiki receives the active theme instead of fixed `github-light`. - Update `CodeBlock` highlight cache keys to include theme. Theme Mapping: Shiki should use exact preset names: - `github-light` -> `github-light` - `github-dark` -> `github-dark` - `nord` -> `nord` - `dracula` -> `dracula` - `solarized-light` -> `solarized-light` - `solarized-dark` -> `solarized-dark` `marked-terminal` should use family mapping: - `github-light` -> `gitHub` - `github-dark` -> `gitHub` - `nord` -> `nord` - `dracula` -> `dracula` - `solarized-light` -> `solarized` - `solarized-dark` -> `solarized` The app UI palette should still differ between `*-light` and `*-dark` presets even when `marked-terminal` uses the same family theme. Public Interfaces: - `Config` gains `theme: ThemeId` - New exported `ThemeId` and theme registry/types - `renderMarkdown` becomes theme-aware - `CodeBlock` highlight/prewarm helpers accept or derive the active theme - `ThemeSettings` needs preview and confirm/cancel callbacks Test Plan: - Config tests: - default load returns `github-dark` - theme persists across save/load - legacy config without theme still loads safely - Slash command tests: - `/theme` appears in suggestions - submitting `/theme` opens the picker - App tests: - preview applies while navigating - `Enter` saves - `Esc` restores the original theme and does not save - Theme screen tests: - all presets render - current theme is shown - navigation triggers preview updates - Markdown/rendering tests: - `marked-terminal` receives the mapped family theme - Code block tests: - Shiki receives the exact preset theme - cache keys include theme - switching themes re-renders correctly - UI color tests: - explicit colors come from theme semantics - no light/dark-unsafe `black` usage remains for normal content Assumptions: - v1 uses built-in presets only. - `/theme <name>` inline parsing is out of scope. - Theme is global config, not per session. - Terminal background remains user-controlled. - No auto light/dark detection in v1; explicit `*-light` and `*-dark` presets remove that ambiguity.
The bug was that one unfinished inline marker, like an open `**`, caused the entire remaining streamed suffix to fall back to plain text. In practice that meant later lines in the same in-flight assistant message stopped rendering as markdown even when they were already complete. The fix limits the plain-text fallback to the affected line and lets later lines continue through the markdown renderer during streaming.
Right now, `stickyHeightRef` prevents the streaming frame from shrinking. That helps with upward jumps, but it does not stop content above the cursor from being rewrapped when new tokens arrive. Fix: Freeze all completed lines/blocks and only re-render the live tail. - Split streaming assistant text into `stablePrefix` and `activeSuffix`. - Treat anything before the last incomplete paragraph/list item/code fence as committed-for-layout. - Render `stablePrefix` once as markdown. - Only run `splitStreamingInlineContent()` and markdown reflow on `activeSuffix`. Why this helps: - Most layout shift comes from `renderMarkdown()` being rerun on the whole text segment every chunk in `src/components/Messages/layout.ts` and `src/components/Messages/Message.tsx`. - If only the tail can change, earlier wrapped lines stop moving. Update: Changed the TUI streaming path to freeze completed lines and only treat the last line as the live tail. The tradeoff is intentional: once a line has ended with `\n`, it no longer gets reinterpreted on later chunks. That reduces layout shift because earlier wrapped lines stop moving, but it also means an unmatched markdown opener on a completed line can remain visible until commit.
Bug:
- `marked-terminal` emits ANSI sequences for parts of the markdown line,
including foreground resets like `\x1B[39m`.
- The outer Ink `<Text color={messageColor}>` is trying to make the whole
assistant message cyan.
- On the first rendered line, ANSI resets from `marked-terminal` override
that outer color and drop text back to the terminal default foreground,
which looks black in your screenshot.
- When the long line wraps, the continuation text is no longer covered
the same way by those inline ANSI style segments, so Ink’s outer cyan
color shows through on the wrapped lines.
Fix:
- Stop applying `color={messageColor}` to markdown text and let
`marked-terminal` own all coloring.
- If `renderMarkdown()` returns ANSI-styled output from `marked-terminal`,
the component no longer applies an outer Ink `color` prop
- If the rendered output has no ANSI styles, the parent `color` still
applies as before
That avoids the conflict where `marked-terminal` resets the foreground on
the first line and Ink’s parent color leaks differently onto wrapped lines,
which is what produced the black first line and cyan continuation lines.
In `src/components/Messages/Message.tsx`, assistant markdown no longer
passes `color={messageColor}` into `src/components/Markdown/Markdown.tsx`.
That means markdown output is now styled solely by `marked-terminal` and
its theme, while user/system/plain streaming text still keeps the role
color behavior.
Removed the ANSI-detection workaround from `src/components/Markdown/Markdown.tsx`.
…ering Changed the TUI so all streaming assistant text goes through markdown rendering. In `src/components/Messages/Message.tsx`, the live assistant text path no longer splits into plain fallback fragments. Streaming assistant text segments now render as markdown end-to-end, and the streaming height calculation uses that same single markdown path. That should eliminate the color changes caused by switching between plain `Text` and markdown during the stream.
/theme command with markdown rendering fixes
The CI failure was in the test, not the wrap logic in `ThemeSettings`. `src/components/ThemeSettings.test.tsx:257` was asserting after calling a manually captured `useInput` handler. Under the current React 19 + Ink/Vitest setup, that bypasses Ink’s normal input/render cycle, so `setSelectedIndex(...)` never flushes in the test. The only recorded `onPreview` call is the mount effect, which is why CI saw `github-light` instead of the wrapped `solarized-dark`. Changed `src/components/ThemeSettings.test.tsx` to use Ink’s real stdin path with `stdin.write(KEY.UP|DOWN|ENTER|ESCAPE|CTRL_C)` instead of mocking `useInput`. That makes the tests exercise the component the same way the app does and removes the stale/racy behavior.
Codecov Report✅ All modified and coverable lines are covered by tests.
🚀 New features to boost your workflow:
|
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.
What is the motivation for this pull request?
Adds a
/themecommand for switching the UI color theme, along with several fixes to markdown rendering and component structure.What is the current behavior?
marked-terminalWhat is the new behavior?
/themecommand allows runtime theme switchingmarked-terminalowns coloringMessagesplit into its own fileSelectPromptsplit into a directory structuremessageAssistantcolor token removed (no longer needed)plan.md
Checklist: