diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx
index e1ddd4ca8..c2541ba7b 100644
--- a/.storybook/preview.tsx
+++ b/.storybook/preview.tsx
@@ -1,13 +1,50 @@
+import React, { useEffect } from "react";
import type { Preview } from "@storybook/react-vite";
+import {
+ ThemeProvider,
+ useTheme,
+ type ThemeMode,
+} from "../src/browser/contexts/ThemeContext";
import "../src/browser/styles/globals.css";
+const ThemeStorySync: React.FC<{ mode: ThemeMode }> = ({ mode }) => {
+ const { theme, setTheme } = useTheme();
+
+ useEffect(() => {
+ if (theme !== mode) {
+ setTheme(mode);
+ }
+ }, [mode, setTheme, theme]);
+
+ return null;
+};
+
const preview: Preview = {
+ globalTypes: {
+ theme: {
+ name: "Theme",
+ description: "Choose between light and dark UI themes",
+ defaultValue: "dark",
+ toolbar: {
+ icon: "mirror",
+ items: [
+ { value: "dark", title: "Dark" },
+ { value: "light", title: "Light" },
+ ],
+ dynamicTitle: true,
+ },
+ },
+ },
decorators: [
- (Story) => (
- <>
-
- >
- ),
+ (Story, context) => {
+ const mode = (context.globals.theme ?? "dark") as ThemeMode;
+ return (
+
+
+
+
+ );
+ },
],
parameters: {
controls: {
@@ -16,6 +53,12 @@ const preview: Preview = {
date: /Date$/i,
},
},
+ chromatic: {
+ modes: {
+ dark: { globals: { theme: "dark" } },
+ light: { globals: { theme: "light" } },
+ },
+ },
},
};
diff --git a/bun.lock b/bun.lock
index 2d4ab805b..8f535ca94 100644
--- a/bun.lock
+++ b/bun.lock
@@ -34,6 +34,7 @@
"ghostty-web": "next",
"jsonc-parser": "^3.3.1",
"lru-cache": "^11.2.2",
+ "lucide-react": "^0.553.0",
"markdown-it": "^14.1.0",
"minimist": "^1.2.8",
"motion": "^12.23.24",
@@ -964,7 +965,7 @@
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
- "@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
+ "@types/node": ["@types/node@24.9.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA=="],
"@types/plist": ["@types/plist@3.0.5", "", { "dependencies": { "@types/node": "*", "xmlbuilder": ">=11.0.1" } }, "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA=="],
@@ -1788,7 +1789,7 @@
"get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="],
- "ghostty-web": ["ghostty-web@0.2.1-next.2.g6ea6f04", "", {}, "sha512-88Cf6NUnVoqD6oS5tmXnI/gEEKicZYucAXK6RGtH/Tlfd4lKddy+tIIkUZ7GnMaxkaFnyzjrQ2kAFJcy4Ld+/w=="],
+ "ghostty-web": ["ghostty-web@0.2.1-next.3.g5e035a2", "", { "bin": { "ghostty-web": "bin/ghostty-web.js" } }, "sha512-iQFTRn3N3+yZJkr2AA3EUpDglL/Vyc2BiPfJuOBHt7b+JyrjLPJxIU37fsf/2+WnS9y0yFvMI3WAPrkGPA2iBw=="],
"glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="],
@@ -2226,7 +2227,7 @@
"lru-cache": ["lru-cache@11.2.2", "", {}, "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg=="],
- "lucide-react": ["lucide-react@0.542.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-w3hD8/SQB7+lzU2r4VdFyzzOzKnUjTZIF/MQJGSSvni7Llewni4vuViRppfRAa2guOsY5k4jZyxw/i9DQHv+dw=="],
+ "lucide-react": ["lucide-react@0.553.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-BRgX5zrWmNy/lkVAe0dXBgd7XQdZ3HTf+Hwe3c9WK6dqgnj9h+hxV+MDncM88xDWlCq27+TKvHGE70ViODNILw=="],
"lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
@@ -3286,7 +3287,7 @@
"dom-serializer/entities": ["entities@2.2.0", "", {}, "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="],
- "electron/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="],
+ "electron/@types/node": ["@types/node@22.18.13", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Bo45YKIjnmFtv6I1TuC8AaHBbqXtIo+Om5fE4QiU1Tj8QR/qt+8O3BAtOimG5IFmwaWiPmB3Mv3jtYzBA4Us2A=="],
"electron-builder/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
@@ -3514,6 +3515,8 @@
"stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="],
+ "streamdown/lucide-react": ["lucide-react@0.542.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-w3hD8/SQB7+lzU2r4VdFyzzOzKnUjTZIF/MQJGSSvni7Llewni4vuViRppfRAa2guOsY5k4jZyxw/i9DQHv+dw=="],
+
"string-length/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
"string_decoder/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
diff --git a/index.html b/index.html
index a96f986c4..464c26382 100644
--- a/index.html
+++ b/index.html
@@ -16,13 +16,38 @@
margin: 0;
padding: 0;
overflow: hidden;
- background: #1e1e1e;
+ background: var(--color-background, #1e1e1e);
}
#root {
height: 100vh;
overflow: hidden;
}
+
diff --git a/package.json b/package.json
index 0c74a3ab8..3d846310e 100644
--- a/package.json
+++ b/package.json
@@ -74,6 +74,7 @@
"ghostty-web": "next",
"jsonc-parser": "^3.3.1",
"lru-cache": "^11.2.2",
+ "lucide-react": "^0.553.0",
"markdown-it": "^14.1.0",
"minimist": "^1.2.8",
"motion": "^12.23.24",
diff --git a/src/browser/App.tsx b/src/browser/App.tsx
index 585a75550..eea416ab8 100644
--- a/src/browser/App.tsx
+++ b/src/browser/App.tsx
@@ -21,6 +21,7 @@ import { CommandRegistryProvider, useCommandRegistry } from "./contexts/CommandR
import type { CommandAction } from "./contexts/CommandRegistryContext";
import { ModeProvider } from "./contexts/ModeContext";
import { ProviderOptionsProvider } from "./contexts/ProviderOptionsContext";
+import { ThemeProvider, useTheme, type ThemeMode } from "./contexts/ThemeContext";
import { ThinkingProvider } from "./contexts/ThinkingContext";
import { CommandPalette } from "./components/CommandPalette";
import { buildCoreSources, type BuildSourcesParams } from "./utils/commands/sources";
@@ -48,6 +49,13 @@ function AppInner() {
beginWorkspaceCreation,
clearPendingWorkspaceCreation,
} = useWorkspaceContext();
+ const { theme, setTheme, toggleTheme } = useTheme();
+ const setThemePreference = useCallback(
+ (nextTheme: ThemeMode) => {
+ setTheme(nextTheme);
+ },
+ [setTheme]
+ );
const {
projects,
removeProject,
@@ -389,6 +397,7 @@ function AppInner() {
projects,
workspaceMetadata,
selectedWorkspace,
+ theme,
getThinkingLevel: getThinkingLevelForWorkspace,
onSetThinkingLevel: setThinkingLevelFromPalette,
onStartWorkspaceCreation: openNewWorkspaceFromPalette,
@@ -401,6 +410,8 @@ function AppInner() {
onToggleSidebar: toggleSidebarFromPalette,
onNavigateWorkspace: navigateWorkspaceFromPalette,
onOpenWorkspaceInTerminal: openWorkspaceInTerminal,
+ onToggleTheme: toggleTheme,
+ onSetTheme: setThemePreference,
};
useEffect(() => {
@@ -587,7 +598,7 @@ function AppInner() {
})()
) : (
-
-
+
+
+
+
+
);
}
diff --git a/src/browser/components/CommandPalette.tsx b/src/browser/components/CommandPalette.tsx
index 94ed22516..105b04fdb 100644
--- a/src/browser/components/CommandPalette.tsx
+++ b/src/browser/components/CommandPalette.tsx
@@ -378,7 +378,7 @@ export const CommandPalette: React.FC
= ({ getSlashContext
}}
>
e.stopPropagation()}
shouldFilter={shouldUseCmdkFilter}
filter={(value, search) => {
@@ -395,7 +395,7 @@ export const CommandPalette: React.FC = ({ getSlashContext
}}
>
= ({ getSlashContext
{groupsWithItems.map((group) => (
+ {group.name}
+
+ }
+ className="px-1.5 py-2"
>
{group.items.map((item) => (
{
if ("prompt" in item && item.prompt) {
addRecent(item.id);
@@ -459,12 +463,14 @@ export const CommandPalette: React.FC = ({ getSlashContext
{"subtitle" in item && item.subtitle && (
<>
- {item.subtitle}
+
+ {item.subtitle}
+
>
)}
{"shortcutHint" in item && item.shortcutHint && (
-
+
{item.shortcutHint}
)}
@@ -473,7 +479,9 @@ export const CommandPalette: React.FC = ({ getSlashContext
))}
{!hasAnyItems && (
- {emptyText ?? "No results"}
+
+ {emptyText ?? "No results"}
+
)}
diff --git a/src/browser/components/KebabMenu.tsx b/src/browser/components/KebabMenu.tsx
index 3b79182e8..18126ba53 100644
--- a/src/browser/components/KebabMenu.tsx
+++ b/src/browser/components/KebabMenu.tsx
@@ -115,8 +115,8 @@ export const KebabMenu: React.FC = ({ items, className }) => {
item.disabled
? "bg-dark text-muted-light cursor-not-allowed opacity-50 hover:bg-dark hover:text-muted-light"
: item.active
- ? "bg-white/15 text-foreground cursor-pointer hover:bg-white/15 hover:text-white"
- : "bg-dark text-foreground cursor-pointer hover:bg-white/15 hover:text-white"
+ ? "bg-white/15 text-foreground cursor-pointer hover:bg-white/15 hover:text-[var(--color-hover-foreground)]"
+ : "bg-dark text-foreground cursor-pointer hover:bg-white/15 hover:text-[var(--color-hover-foreground)]"
)}
>
{item.emoji && (
diff --git a/src/browser/components/Messages/HistoryHiddenMessage.tsx b/src/browser/components/Messages/HistoryHiddenMessage.tsx
index 6b3dfae50..2113ce294 100644
--- a/src/browser/components/Messages/HistoryHiddenMessage.tsx
+++ b/src/browser/components/Messages/HistoryHiddenMessage.tsx
@@ -14,8 +14,8 @@ export const HistoryHiddenMessage: React.FC = ({
return (
diff --git a/src/browser/components/Messages/MarkdownComponents.tsx b/src/browser/components/Messages/MarkdownComponents.tsx
index 61f7b9e4f..9263cf592 100644
--- a/src/browser/components/Messages/MarkdownComponents.tsx
+++ b/src/browser/components/Messages/MarkdownComponents.tsx
@@ -4,8 +4,10 @@ import { Mermaid } from "./Mermaid";
import {
getShikiHighlighter,
mapToShikiLang,
- SHIKI_THEME,
+ SHIKI_DARK_THEME,
+ SHIKI_LIGHT_THEME,
} from "@/browser/utils/highlighting/shikiHighlighter";
+import { useTheme } from "@/browser/contexts/ThemeContext";
import { extractShikiLines } from "@/browser/utils/highlighting/shiki-shared";
import { CopyButton } from "@/browser/components/ui/CopyButton";
@@ -45,6 +47,7 @@ interface CodeBlockProps {
*/
const CodeBlock: React.FC
= ({ code, language }) => {
const [highlightedLines, setHighlightedLines] = useState(null);
+ const { theme: themeMode } = useTheme();
// Split code into lines, removing trailing empty line
const plainLines = code
@@ -53,6 +56,9 @@ const CodeBlock: React.FC = ({ code, language }) => {
useEffect(() => {
let cancelled = false;
+ const shikiTheme = themeMode === "light" ? SHIKI_LIGHT_THEME : SHIKI_DARK_THEME;
+
+ setHighlightedLines(null);
async function highlight() {
try {
@@ -79,7 +85,7 @@ const CodeBlock: React.FC = ({ code, language }) => {
const html = highlighter.codeToHtml(code, {
lang: shikiLang,
- theme: SHIKI_THEME,
+ theme: shikiTheme,
});
if (!cancelled) {
@@ -100,7 +106,7 @@ const CodeBlock: React.FC = ({ code, language }) => {
return () => {
cancelled = true;
};
- }, [code, language]);
+ }, [code, language, themeMode]);
const lines = highlightedLines ?? plainLines;
diff --git a/src/browser/components/Messages/MessageWindow.tsx b/src/browser/components/Messages/MessageWindow.tsx
index cb95a5e5c..32f9742f0 100644
--- a/src/browser/components/Messages/MessageWindow.tsx
+++ b/src/browser/components/Messages/MessageWindow.tsx
@@ -66,7 +66,7 @@ export const MessageWindow: React.FC = ({
className={cn(
"mt-4 mb-1 flex w-full flex-col relative isolate w-fit",
variant === "user" && "ml-auto",
- variant === "assistant" && "text-white",
+ variant === "assistant" && "text-foreground",
isLastPartOfMessage && "mb-4"
)}
data-message-block
@@ -74,7 +74,7 @@ export const MessageWindow: React.FC = ({
@@ -82,7 +82,7 @@ export const MessageWindow: React.FC
= ({
{showJson ? (
-
+
{JSON.stringify(message, null, 2)}
) : (
@@ -95,7 +95,7 @@ export const MessageWindow: React.FC = ({
diff --git a/src/browser/components/Messages/UserMessage.tsx b/src/browser/components/Messages/UserMessage.tsx
index 41b9c8add..9f7ffdd6e 100644
--- a/src/browser/components/Messages/UserMessage.tsx
+++ b/src/browser/components/Messages/UserMessage.tsx
@@ -99,7 +99,7 @@ export const UserMessage: React.FC
= ({
variant="user"
>
{content && (
-
+
{content}
)}
@@ -110,7 +110,7 @@ export const UserMessage: React.FC = ({
key={idx}
src={img.url}
alt={`Attachment ${idx + 1}`}
- className="max-h-[300px] max-w-72 rounded-xl border border-white/10 object-cover"
+ className="max-h-[300px] max-w-72 rounded-xl border border-[var(--color-attachment-border)] object-cover"
/>
))}
diff --git a/src/browser/components/Modal.tsx b/src/browser/components/Modal.tsx
index edd311a19..690922955 100644
--- a/src/browser/components/Modal.tsx
+++ b/src/browser/components/Modal.tsx
@@ -29,7 +29,7 @@ export const ModalContent: React.FC<
(
"text-[11px] font-monospace py-1.5 px-2.5 cursor-pointer transition-colors duration-100",
"first:rounded-t last:rounded-b",
index === highlightedIndex
- ? "text-white bg-hover"
- : "text-light bg-transparent hover:bg-hover hover:text-white"
+ ? "text-foreground bg-hover"
+ : "text-light bg-transparent hover:bg-hover hover:text-foreground"
)}
onClick={() => handleSelectModel(model)}
>
diff --git a/src/browser/components/ProjectCreateModal.tsx b/src/browser/components/ProjectCreateModal.tsx
index 6a7f51bec..a339d4d97 100644
--- a/src/browser/components/ProjectCreateModal.tsx
+++ b/src/browser/components/ProjectCreateModal.tsx
@@ -1,7 +1,5 @@
import React, { useState, useCallback } from "react";
import { Modal, ModalActions, CancelButton, PrimaryButton } from "./Modal";
-import type { IPCApi } from "@/common/types/ipc";
-import { DirectoryPickerModal } from "./DirectoryPickerModal";
import type { ProjectConfig } from "@/node/config";
interface ProjectCreateModalProps {
@@ -23,13 +21,7 @@ export const ProjectCreateModal: React.FC
= ({
}) => {
const [path, setPath] = useState("");
const [error, setError] = useState("");
- // Detect desktop environment where native directory picker is available
- const isDesktop =
- window.api.platform !== "browser" && typeof window.api.projects.pickDirectory === "function";
- const api = window.api as unknown as IPCApi;
- const hasWebFsPicker = window.api.platform === "browser" && !!api.fs?.listDirectory;
const [isCreating, setIsCreating] = useState(false);
- const [isDirPickerOpen, setIsDirPickerOpen] = useState(false);
const handleCancel = useCallback(() => {
setPath("");
@@ -37,23 +29,6 @@ export const ProjectCreateModal: React.FC = ({
onClose();
}, [onClose]);
- const handleWebPickerPathSelected = useCallback((selected: string) => {
- setPath(selected);
- setError("");
- }, []);
-
- const handleBrowse = useCallback(async () => {
- try {
- const selectedPath = await window.api.projects.pickDirectory();
- if (selectedPath) {
- setPath(selectedPath);
- setError("");
- }
- } catch (err) {
- console.error("Failed to pick directory:", err);
- }
- }, []);
-
const handleSelect = useCallback(async () => {
const trimmedPath = path.trim();
if (!trimmedPath) {
@@ -103,14 +78,6 @@ export const ProjectCreateModal: React.FC = ({
}
}, [path, onSuccess, onClose]);
- const handleBrowseClick = useCallback(() => {
- if (isDesktop) {
- void handleBrowse();
- } else if (hasWebFsPicker) {
- setIsDirPickerOpen(true);
- }
- }, [handleBrowse, hasWebFsPicker, isDesktop]);
-
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === "Enter") {
@@ -122,55 +89,35 @@ export const ProjectCreateModal: React.FC = ({
);
return (
- <>
-
-
- {
- setPath(e.target.value);
- setError("");
- }}
- onKeyDown={handleKeyDown}
- placeholder="/home/user/projects/my-project"
- autoFocus
- disabled={isCreating}
- className="bg-modal-bg border-border-medium focus:border-accent placeholder:text-muted w-full flex-1 rounded border px-3 py-2 font-mono text-sm text-white focus:outline-none disabled:opacity-50"
- />
- {(isDesktop || hasWebFsPicker) && (
-
- )}
-
- {error && {error}
}
-
-
- Cancel
-
- void handleSelect()} disabled={isCreating}>
- {isCreating ? "Adding..." : "Add Project"}
-
-
-
- setIsDirPickerOpen(false)}
- onSelectPath={handleWebPickerPathSelected}
+
+ {
+ setPath(e.target.value);
+ setError("");
+ }}
+ onKeyDown={handleKeyDown}
+ placeholder="/home/user/projects/my-project"
+ autoFocus
+ disabled={isCreating}
+ className="bg-modal-bg border-border-medium focus:border-accent placeholder:text-muted text-foreground mb-5 w-full rounded border px-3 py-2 font-mono text-sm focus:outline-none disabled:opacity-50"
/>
- >
+ {error && {error}
}
+
+
+ Cancel
+
+ void handleSelect()} disabled={isCreating}>
+ {isCreating ? "Adding..." : "Add Project"}
+
+
+
);
};
diff --git a/src/browser/components/RightSidebar.tsx b/src/browser/components/RightSidebar.tsx
index 15134529d..fb032c785 100644
--- a/src/browser/components/RightSidebar.tsx
+++ b/src/browser/components/RightSidebar.tsx
@@ -226,7 +226,7 @@ const RightSidebarComponent: React.FC = ({
className={cn(
"w-full py-2.5 px-[15px] border-none border-solid cursor-pointer font-primary text-[13px] font-medium transition-all duration-200",
selectedTab === "costs"
- ? "text-white bg-separator border-b-2 border-b-plan-mode"
+ ? "bg-separator border-b-2 border-b-plan-mode text-[var(--color-sidebar-tab-active)]"
: "bg-transparent text-secondary border-b-2 border-b-transparent hover:bg-background-secondary hover:text-foreground"
)}
onClick={() => setSelectedTab("costs")}
@@ -247,7 +247,7 @@ const RightSidebarComponent: React.FC = ({
className={cn(
"w-full py-2.5 px-[15px] border-none border-solid cursor-pointer font-primary text-[13px] font-medium transition-all duration-200",
selectedTab === "review"
- ? "text-white bg-separator border-b-2 border-b-plan-mode"
+ ? "bg-separator border-b-2 border-b-plan-mode text-[var(--color-sidebar-tab-active)]"
: "bg-transparent text-secondary border-b-2 border-b-transparent hover:bg-background-secondary hover:text-foreground"
)}
onClick={() => setSelectedTab("review")}
diff --git a/src/browser/components/RightSidebar/CodeReview/ReviewControls.tsx b/src/browser/components/RightSidebar/CodeReview/ReviewControls.tsx
index 10f19aa0a..6946a1332 100644
--- a/src/browser/components/RightSidebar/CodeReview/ReviewControls.tsx
+++ b/src/browser/components/RightSidebar/CodeReview/ReviewControls.tsx
@@ -115,7 +115,7 @@ export const ReviewControls: React.FC = ({
)}
-
-
- {buildDate}
- Built at {extendedTimestamp}
-
+
+
+
+ {buildDate}
+ Built at {extendedTimestamp}
+
+
);
}
diff --git a/src/browser/components/shared/DiffRenderer.tsx b/src/browser/components/shared/DiffRenderer.tsx
index 44212612a..f82092730 100644
--- a/src/browser/components/shared/DiffRenderer.tsx
+++ b/src/browser/components/shared/DiffRenderer.tsx
@@ -9,6 +9,7 @@ import { cn } from "@/common/lib/utils";
import { getLanguageFromPath } from "@/common/utils/git/languageDetector";
import { Tooltip, TooltipWrapper } from "../Tooltip";
import { groupDiffLines } from "@/browser/utils/highlighting/diffChunking";
+import { useTheme, type ThemeMode } from "@/browser/contexts/ThemeContext";
import {
highlightDiffChunk,
type HighlightedChunk,
@@ -176,7 +177,8 @@ function useHighlightedDiff(
content: string,
language: string,
oldStart: number,
- newStart: number
+ newStart: number,
+ themeMode: ThemeMode
): HighlightedChunk[] | null {
const [chunks, setChunks] = useState(null);
// Track if we've already highlighted with real syntax (to prevent downgrading)
@@ -199,7 +201,7 @@ function useHighlightedDiff(
// Highlight each chunk (without search decorations - those are applied later)
const highlighted = await Promise.all(
- diffChunks.map((chunk) => highlightDiffChunk(chunk, language))
+ diffChunks.map((chunk) => highlightDiffChunk(chunk, language, themeMode))
);
if (!cancelled) {
@@ -216,7 +218,7 @@ function useHighlightedDiff(
return () => {
cancelled = true;
};
- }, [content, language, oldStart, newStart]);
+ }, [content, language, oldStart, newStart, themeMode]);
return chunks;
}
@@ -240,12 +242,13 @@ export const DiffRenderer: React.FC = ({
maxHeight,
}) => {
// Detect language for syntax highlighting (memoized to prevent repeated detection)
+ const { theme } = useTheme();
const language = React.useMemo(
() => (filePath ? getLanguageFromPath(filePath) : "text"),
[filePath]
);
- const highlightedChunks = useHighlightedDiff(content, language, oldStart, newStart);
+ const highlightedChunks = useHighlightedDiff(content, language, oldStart, newStart, theme);
// Show loading state while highlighting
if (!highlightedChunks) {
@@ -446,6 +449,7 @@ export const SelectableDiffRenderer = React.memo(
searchConfig,
enableHighlighting = true,
}) => {
+ const { theme } = useTheme();
const [selection, setSelection] = React.useState(null);
// Detect language for syntax highlighting (memoized to prevent repeated detection)
@@ -459,7 +463,8 @@ export const SelectableDiffRenderer = React.memo(
content,
enableHighlighting ? language : "text",
oldStart,
- newStart
+ newStart,
+ theme
);
// Build lineData from highlighted chunks (memoized to prevent repeated parsing)
diff --git a/src/browser/contexts/ThemeContext.tsx b/src/browser/contexts/ThemeContext.tsx
new file mode 100644
index 000000000..1ead055a5
--- /dev/null
+++ b/src/browser/contexts/ThemeContext.tsx
@@ -0,0 +1,85 @@
+import React, {
+ createContext,
+ useCallback,
+ useContext,
+ useLayoutEffect,
+ useMemo,
+ type ReactNode,
+} from "react";
+import { usePersistedState } from "@/browser/hooks/usePersistedState";
+import { UI_THEME_KEY } from "@/common/constants/storage";
+
+export type ThemeMode = "light" | "dark";
+
+interface ThemeContextValue {
+ theme: ThemeMode;
+ setTheme: React.Dispatch>;
+ toggleTheme: () => void;
+}
+
+const ThemeContext = createContext(null);
+
+const DARK_THEME_COLOR = "#1e1e1e";
+const LIGHT_THEME_COLOR = "#f5f6f8";
+
+function resolveSystemTheme(): ThemeMode {
+ if (typeof window === "undefined" || !window.matchMedia) {
+ return "dark";
+ }
+
+ return window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
+}
+
+function applyThemeToDocument(theme: ThemeMode) {
+ if (typeof document === "undefined") {
+ return;
+ }
+
+ const root = document.documentElement;
+ root.dataset.theme = theme;
+ root.style.colorScheme = theme;
+
+ const themeColor = theme === "light" ? LIGHT_THEME_COLOR : DARK_THEME_COLOR;
+ const meta = document.querySelector('meta[name="theme-color"]');
+ if (meta) {
+ meta.setAttribute("content", themeColor);
+ }
+
+ const body = document.body;
+ if (body) {
+ body.style.backgroundColor = "var(--color-background)";
+ }
+}
+
+export function ThemeProvider(props: { children: ReactNode }) {
+ const [theme, setTheme] = usePersistedState(UI_THEME_KEY, resolveSystemTheme(), {
+ listener: true,
+ });
+
+ useLayoutEffect(() => {
+ applyThemeToDocument(theme);
+ }, [theme]);
+
+ const toggleTheme = useCallback(() => {
+ setTheme((current) => (current === "dark" ? "light" : "dark"));
+ }, [setTheme]);
+
+ const value = useMemo(
+ () => ({
+ theme,
+ setTheme,
+ toggleTheme,
+ }),
+ [setTheme, theme, toggleTheme]
+ );
+
+ return {props.children};
+}
+
+export function useTheme(): ThemeContextValue {
+ const context = useContext(ThemeContext);
+ if (!context) {
+ throw new Error("useTheme must be used within a ThemeProvider");
+ }
+ return context;
+}
diff --git a/src/browser/styles/globals.css b/src/browser/styles/globals.css
index c2473a18b..ee0cf3cd5 100644
--- a/src/browser/styles/globals.css
+++ b/src/browser/styles/globals.css
@@ -82,9 +82,27 @@
--color-button-hover: hsl(0 0% 29%);
/* Messages */
- --color-user-border: hsl(0 0% 45%);
- --color-user-border-hover: hsl(0 0% 44%);
+ --color-user-border: hsla(0 0% 100% / 0.1);
+ --color-user-surface: hsla(0 0% 100% / 0.06);
+ --color-user-border-hover: hsla(0 0% 100% / 0.16);
+ --color-user-text: #f1f5f9;
+ --color-message-debug-bg: rgba(0, 0, 0, 0.3);
+ --color-message-debug-border: rgba(255, 255, 255, 0.1);
+ --color-message-debug-text: rgba(255, 255, 255, 0.8);
+ --color-message-hidden-bg: rgba(255, 255, 255, 0.03);
+ --color-attachment-border: rgba(255, 255, 255, 0.1);
+ --color-line-number-bg: rgba(0, 0, 0, 0.2);
+ --color-line-number-text: rgba(255, 255, 255, 0.4);
+ --color-line-number-border: rgba(255, 255, 255, 0.1);
--color-assistant-border: hsl(207 45% 40%);
+ --color-hover-foreground: hsl(0 0% 96%);
+ --color-command-surface: hsl(0 0% 15%);
+ --color-command-border: hsl(240 2% 25%);
+ --color-command-input: hsl(0 0% 9%);
+ --color-command-input-border: hsla(0 0% 100% / 0.08);
+ --color-command-foreground: hsl(0 0% 83%);
+ --color-command-subdued: hsl(0 0% 63%);
+ --color-sidebar-tab-active: hsl(0 0% 100%);
--color-assistant-border-hover: hsl(207 45% 50%);
--color-message-header: hsl(0 0% 80%);
@@ -249,6 +267,208 @@
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}
+:root[data-theme="light"] {
+ color-scheme: light;
+
+ --color-plan-mode: hsl(210 70% 40%);
+ --color-plan-mode-hover: hsl(210 70% 52%);
+ --color-plan-mode-light: hsl(210 70% 68%);
+ --color-plan-mode-alpha: hsla(210 70% 40% / 0.08);
+
+ --color-exec-mode: hsl(268.56 94.04% 55.19%);
+ --color-exec-mode-hover: hsl(268.56 94.04% 67%);
+ --color-exec-mode-light: hsl(268.56 94.04% 78%);
+
+ --color-edit-mode: hsl(120 50% 35%);
+ --color-edit-mode-hover: hsl(120 50% 45%);
+ --color-edit-mode-light: hsl(120 50% 60%);
+
+ --color-read: hsl(210 70% 40%);
+ --color-editing-mode: hsl(30 100% 50%);
+ --color-editing-mode-alpha: hsla(30 100% 50% / 0.08);
+ --color-pending: hsl(30 100% 64%);
+
+ --color-debug-mode: hsl(214 100% 56%);
+ --color-debug-light: hsl(214 100% 68%);
+ --color-debug-text: hsl(214 100% 32%);
+
+ --color-thinking-mode: hsl(271 70% 46%);
+ --color-thinking-mode-light: hsl(271 70% 62%);
+ --color-thinking-border: hsl(271 70% 46%);
+
+ --color-background: hsl(210 33% 98%);
+ --color-background-secondary: hsl(210 36% 95%);
+ --color-border: hsl(210 24% 82%);
+ --color-foreground: hsl(210 18% 16%);
+ --color-text: hsl(210 18% 16%);
+ --color-text-light: hsl(210 15% 28%);
+ --color-text-secondary: hsl(210 12% 42%);
+ --color-muted-foreground: hsl(210 14% 48%);
+ --color-secondary: hsl(210 18% 60%);
+
+ --color-code-bg: hsl(210 40% 96%);
+
+ --color-button-bg: hsl(210 35% 94%);
+ --color-button-text: hsl(210 20% 20%);
+ --color-button-hover: hsl(210 32% 90%);
+
+ --color-user-surface: hsl(0 0% 91%);
+ --color-user-border: hsl(210 20% 78%);
+ --color-user-border-hover: hsl(210 20% 68%);
+ --color-user-text: hsl(210 18% 20%);
+ --color-message-debug-bg: hsl(210 40% 95%);
+ --color-message-debug-border: hsl(210 26% 82%);
+ --color-message-debug-text: hsl(210 28% 32%);
+ --color-message-hidden-bg: hsl(210 36% 94%);
+ --color-attachment-border: hsl(210 24% 82%);
+ --color-line-number-bg: hsl(210 34% 93%);
+ --color-line-number-text: hsl(210 14% 46%);
+ --color-line-number-border: hsl(210 22% 84%);
+ --color-assistant-border: hsl(207 65% 50%);
+ --color-hover-foreground: hsl(210 18% 12%);
+ --color-command-surface: hsl(210 32% 97%);
+ --color-command-border: hsl(210 22% 82%);
+ --color-command-input: hsl(210 36% 95%);
+ --color-command-input-border: hsl(210 24% 84%);
+ --color-command-foreground: hsl(210 18% 18%);
+ --color-command-subdued: hsl(210 14% 46%);
+ --color-sidebar-tab-active: hsl(210 18% 18%);
+ --color-assistant-border-hover: hsl(207 65% 40%);
+ --color-message-header: hsl(210 18% 20%);
+
+ --color-token-prompt: hsl(210 18% 32%);
+ --color-token-completion: hsl(207 90% 40%);
+ --color-token-variable: hsl(207 90% 40%);
+ --color-token-fixed: hsl(210 18% 32%);
+ --color-token-input: hsl(125 45% 36%);
+ --color-token-output: hsl(207 90% 40%);
+ --color-token-cached: hsl(210 16% 50%);
+
+ --surface-plan-gradient: linear-gradient(135deg, color-mix(in srgb, var(--color-plan-mode), transparent 94%) 0%, color-mix(in srgb, var(--color-plan-mode), transparent 97%) 100%);
+ --surface-plan-border: color-mix(in srgb, var(--color-plan-mode), transparent 78%);
+ --surface-plan-border-subtle: color-mix(in srgb, var(--color-plan-mode), transparent 85%);
+ --surface-plan-border-strong: color-mix(in srgb, var(--color-plan-mode), transparent 70%);
+ --surface-plan-divider: color-mix(in srgb, var(--color-plan-mode), transparent 86%);
+ --surface-plan-chip-bg: color-mix(in srgb, var(--color-plan-mode), transparent 94%);
+ --surface-plan-chip-hover: color-mix(in srgb, var(--color-plan-mode), transparent 90%);
+ --surface-plan-chip-active: color-mix(in srgb, var(--color-plan-mode), transparent 72%);
+ --surface-plan-chip-border: color-mix(in srgb, var(--color-plan-mode), transparent 78%);
+ --surface-plan-chip-border-strong: color-mix(in srgb, var(--color-plan-mode), transparent 68%);
+ --surface-plan-neutral-border: hsl(210 16% 72% / 0.5);
+
+ --surface-assistant-chip-bg: hsl(from var(--color-assistant-border) h s l / 0.15);
+ --surface-assistant-chip-hover: hsl(from var(--color-assistant-border) h s l / 0.45);
+ --surface-assistant-chip-border: hsl(from var(--color-assistant-border) h s l / 0.45);
+ --surface-assistant-chip-border-strong: hsl(from var(--color-assistant-border) h s l / 0.65);
+
+ --border-warning-dashed: color-mix(in srgb, var(--color-warning), transparent 55%);
+
+ --color-toggle-bg: hsl(210 34% 95%);
+ --color-toggle-active: hsl(210 48% 88%);
+ --color-toggle-hover: hsl(210 38% 92%);
+ --color-toggle-text: hsl(210 16% 32%);
+ --color-toggle-text-active: hsl(210 18% 18%);
+ --color-toggle-text-hover: hsl(210 20% 26%);
+
+ --color-interrupted: hsl(38 92% 50%);
+ --color-review-accent: hsl(48 70% 52%);
+ --color-git-dirty: hsl(38 92% 50%);
+ --color-error: hsl(0 68% 46%);
+ --color-error-bg: hsl(0 82% 94%);
+
+ --color-input-bg: hsl(0 0% 100%);
+ --color-input-text: hsl(210 18% 16%);
+ --color-input-border: hsl(207 75% 52%);
+ --color-input-border-focus: hsl(193 85% 56%);
+
+ --color-scrollbar-track: hsl(210 38% 95%);
+ --color-scrollbar-thumb: hsl(210 18% 78%);
+ --color-scrollbar-thumb-hover: hsl(210 18% 70%);
+
+ --color-muted: hsl(210 14% 52%);
+ --color-muted-light: hsl(210 20% 60%);
+ --color-muted-dark: hsl(210 12% 42%);
+ --color-placeholder: hsl(210 16% 52%);
+ --color-subtle: hsl(210 20% 64%);
+ --color-dim: hsl(210 16% 50%);
+ --color-light: hsl(210 22% 40%);
+ --color-lighter: hsl(210 24% 52%);
+ --color-bright: hsl(210 60% 48%);
+ --color-subdued: hsl(210 18% 60%);
+ --color-label: hsl(210 18% 46%);
+ --color-gray: hsl(210 14% 54%);
+ --color-medium: hsl(210 18% 58%);
+
+ --color-border-light: hsl(210 24% 84%);
+ --color-border-medium: hsl(210 22% 78%);
+ --color-border-darker: hsl(210 20% 70%);
+ --color-border-subtle: hsl(210 18% 64%);
+ --color-border-gray: hsl(210 22% 76%);
+
+ --color-dark: hsl(210 33% 98%);
+ --color-darker: hsl(210 30% 94%);
+ --color-hover: hsl(210 28% 92%);
+ --color-bg-medium: hsl(210 26% 88%);
+ --color-bg-light: hsl(210 24% 84%);
+ --color-bg-subtle: hsl(210 32% 92%);
+
+ --color-separator: hsl(0 0% 91%);
+ --color-separator-light: hsl(0 0% 96%);
+ --color-modal-bg: hsl(210 35% 96%);
+
+ --color-accent: hsl(207 90% 42%);
+ --color-accent-hover: hsl(207 90% 46%);
+ --color-accent-dark: hsl(207 90% 38%);
+ --color-accent-darker: hsl(204 92% 30%);
+ --color-accent-light: hsl(198 100% 70%);
+
+ --color-success: hsl(122 39% 45%);
+ --color-success-light: hsl(123 46% 60%);
+ --color-on-success: hsl(0 0% 100%);
+
+ --color-danger: hsl(4 78% 52%);
+ --color-danger-light: hsl(4 78% 70%);
+ --color-danger-soft: hsl(6 80% 76%);
+ --color-on-danger: hsl(0 0% 100%);
+
+ --color-warning: hsl(45 100% 50%);
+ --color-warning-light: hsl(38 100% 70%);
+
+ --color-code-type: hsl(200 84% 28%);
+ --color-code-keyword: hsl(210 88% 24%);
+
+ --color-toast-success-bg: hsl(207 90% 46% / 0.18);
+ --color-toast-success-text: hsl(207 90% 34%);
+ --color-toast-error-bg: hsl(5 80% 55% / 0.18);
+ --color-toast-error-text: hsl(5 78% 46%);
+ --color-toast-error-border: hsl(5 78% 46%);
+ --color-toast-fatal-bg: hsl(0 72% 94%);
+ --color-toast-fatal-border: hsl(0 74% 82%);
+
+ --color-danger-overlay: hsl(4 80% 52% / 0.12);
+ --color-warning-overlay: hsl(45 100% 50% / 0.12);
+ --color-gray-overlay: hsl(210 16% 30% / 0.08);
+ --color-white-overlay-light: hsl(210 14% 12% / 0.04);
+ --color-white-overlay: hsl(210 14% 12% / 0.08);
+ --color-selection: hsl(204 100% 45% / 0.3);
+ --color-vim-status: hsl(210 18% 32% / 0.6);
+ --color-code-keyword-overlay-light: hsl(210 88% 28% / 0.16);
+ --color-code-keyword-overlay: hsl(210 88% 28% / 0.32);
+
+ --color-info-light: hsl(5 90% 70%);
+ --color-info-yellow: hsl(38 100% 60%);
+
+ --color-review-bg-blue: hsl(212 57% 82%);
+ --color-review-bg-info: hsl(200 60% 80%);
+ --color-review-bg-warning: hsl(38 88% 82%);
+ --color-review-warning: hsl(34 86% 45%);
+ --color-review-warning-medium: hsl(34 86% 52%);
+ --color-review-warning-light: hsl(38 100% 88%);
+
+ --color-error-bg-dark: hsl(0 65% 86%);
+
+ --radius: 0.5rem;
+}
:root[data-theme="dark"] {
color-scheme: dark;
/* Theme override hook: redeclare tokens inside this block to swap palettes */
@@ -841,12 +1061,12 @@ span.search-highlight {
}
.line-number {
- background: rgba(0, 0, 0, 0.2);
+ background: var(--color-line-number-bg);
padding: 0 8px 0 6px;
text-align: right;
- color: rgba(255, 255, 255, 0.4);
+ color: var(--color-line-number-text);
user-select: none;
- border-right: 1px solid rgba(255, 255, 255, 0.1);
+ border-right: 1px solid var(--color-line-number-border);
}
.code-line {
diff --git a/src/browser/utils/commandIds.ts b/src/browser/utils/commandIds.ts
index e30ae8854..cce0e5ad2 100644
--- a/src/browser/utils/commandIds.ts
+++ b/src/browser/utils/commandIds.ts
@@ -50,6 +50,10 @@ export const CommandIds = {
projectRemove: (projectPath: string) =>
`${COMMAND_ID_PREFIXES.PROJECT_REMOVE}${projectPath}` as const,
+ // Appearance commands
+ themeToggle: () => "appearance:theme:toggle" as const,
+ themeSet: (theme: "light" | "dark") => `appearance:theme:set:${theme}` as const,
+
// Help commands
helpKeybinds: () => "help:keybinds" as const,
} as const;
diff --git a/src/browser/utils/commands/sources.test.ts b/src/browser/utils/commands/sources.test.ts
index 8b6d613d7..c322ea63a 100644
--- a/src/browser/utils/commands/sources.test.ts
+++ b/src/browser/utils/commands/sources.test.ts
@@ -27,6 +27,7 @@ const mk = (over: Partial[0]> = {}) => {
});
const params: Parameters[0] = {
projects,
+ theme: "dark",
workspaceMetadata,
selectedWorkspace: {
projectPath: "/repo/a",
@@ -46,6 +47,8 @@ const mk = (over: Partial[0]> = {}) => {
onToggleSidebar: () => undefined,
onNavigateWorkspace: () => undefined,
onOpenWorkspaceInTerminal: () => undefined,
+ onToggleTheme: () => undefined,
+ onSetTheme: () => undefined,
getBranchesForProject: () =>
Promise.resolve({
branches: ["main"],
diff --git a/src/browser/utils/commands/sources.ts b/src/browser/utils/commands/sources.ts
index 2c3aa9348..bf64d38b9 100644
--- a/src/browser/utils/commands/sources.ts
+++ b/src/browser/utils/commands/sources.ts
@@ -1,3 +1,4 @@
+import type { ThemeMode } from "@/browser/contexts/ThemeContext";
import type { CommandAction } from "@/browser/contexts/CommandRegistryContext";
import { formatKeybind, KEYBINDS } from "@/browser/utils/ui/keybinds";
import type { ThinkingLevel } from "@/common/types/thinking";
@@ -12,6 +13,7 @@ export interface BuildSourcesParams {
projects: Map;
/** Map of workspace ID to workspace metadata (keyed by metadata.id, not path) */
workspaceMetadata: Map;
+ theme: ThemeMode;
selectedWorkspace: {
projectPath: string;
projectName: string;
@@ -41,6 +43,8 @@ export interface BuildSourcesParams {
onToggleSidebar: () => void;
onNavigateWorkspace: (dir: "next" | "prev") => void;
onOpenWorkspaceInTerminal: (workspaceId: string) => void;
+ onToggleTheme: () => void;
+ onSetTheme: (theme: ThemeMode) => void;
}
const THINKING_LEVELS: ThinkingLevel[] = ["off", "low", "medium", "high"];
@@ -56,12 +60,14 @@ export const COMMAND_SECTIONS = {
MODE: "Modes & Model",
HELP: "Help",
PROJECTS: "Projects",
+ APPEARANCE: "Appearance",
} as const;
const section = {
workspaces: COMMAND_SECTIONS.WORKSPACES,
navigation: COMMAND_SECTIONS.NAVIGATION,
chat: COMMAND_SECTIONS.CHAT,
+ appearance: COMMAND_SECTIONS.APPEARANCE,
mode: COMMAND_SECTIONS.MODE,
help: COMMAND_SECTIONS.HELP,
projects: COMMAND_SECTIONS.PROJECTS,
@@ -305,6 +311,38 @@ export function buildCoreSources(p: BuildSourcesParams): Array<() => CommandActi
},
]);
+ // Appearance
+ actions.push(() => {
+ const list: CommandAction[] = [
+ {
+ id: CommandIds.themeToggle(),
+ title: `Switch to ${p.theme === "dark" ? "Light" : "Dark"} Theme`,
+ section: section.appearance,
+ run: () => p.onToggleTheme(),
+ },
+ ];
+
+ if (p.theme !== "dark") {
+ list.push({
+ id: CommandIds.themeSet("dark"),
+ title: "Use Dark Theme",
+ section: section.appearance,
+ run: () => p.onSetTheme("dark"),
+ });
+ }
+
+ if (p.theme !== "light") {
+ list.push({
+ id: CommandIds.themeSet("light"),
+ title: "Use Light Theme",
+ section: section.appearance,
+ run: () => p.onSetTheme("light"),
+ });
+ }
+
+ return list;
+ });
+
// Chat utilities
actions.push(() => {
const list: CommandAction[] = [];
diff --git a/src/browser/utils/highlighting/highlightDiffChunk.ts b/src/browser/utils/highlighting/highlightDiffChunk.ts
index ca41d361d..0444d4573 100644
--- a/src/browser/utils/highlighting/highlightDiffChunk.ts
+++ b/src/browser/utils/highlighting/highlightDiffChunk.ts
@@ -1,7 +1,8 @@
import {
getShikiHighlighter,
mapToShikiLang,
- SHIKI_THEME,
+ SHIKI_DARK_THEME,
+ SHIKI_LIGHT_THEME,
MAX_DIFF_SIZE_BYTES,
} from "./shikiHighlighter";
import type { DiffChunk } from "./diffChunking";
@@ -25,6 +26,8 @@ export interface HighlightedLine {
originalIndex: number; // Index in original diff
}
+type ThemeMode = "light" | "dark";
+
export interface HighlightedChunk {
type: DiffChunk["type"];
lines: HighlightedLine[];
@@ -37,7 +40,8 @@ export interface HighlightedChunk {
*/
export async function highlightDiffChunk(
chunk: DiffChunk,
- language: string
+ language: string,
+ themeMode: ThemeMode = "dark"
): Promise {
// Fast path: no highlighting for text files
if (language === "text" || language === "plaintext") {
@@ -81,9 +85,10 @@ export async function highlightDiffChunk(
}
}
+ const shikiTheme = themeMode === "light" ? SHIKI_LIGHT_THEME : SHIKI_DARK_THEME;
const html = highlighter.codeToHtml(code, {
lang: shikiLang,
- theme: SHIKI_THEME,
+ theme: shikiTheme,
});
// Parse HTML to extract line contents
diff --git a/src/browser/utils/highlighting/shiki-shared.ts b/src/browser/utils/highlighting/shiki-shared.ts
index a07cd252b..45110e64c 100644
--- a/src/browser/utils/highlighting/shiki-shared.ts
+++ b/src/browser/utils/highlighting/shiki-shared.ts
@@ -3,8 +3,9 @@
* Used by both the main app and documentation theme
*/
-// Shiki theme used throughout the application
-export const SHIKI_THEME = "min-dark";
+// Shiki themes used throughout the application
+export const SHIKI_DARK_THEME = "min-dark";
+export const SHIKI_LIGHT_THEME = "min-light";
/**
* Map language names to Shiki-compatible language IDs
diff --git a/src/browser/utils/highlighting/shikiHighlighter.ts b/src/browser/utils/highlighting/shikiHighlighter.ts
index b98a54e60..7d725dd20 100644
--- a/src/browser/utils/highlighting/shikiHighlighter.ts
+++ b/src/browser/utils/highlighting/shikiHighlighter.ts
@@ -1,7 +1,7 @@
import { createHighlighter, type Highlighter } from "shiki";
+import { SHIKI_DARK_THEME, SHIKI_LIGHT_THEME } from "./shiki-shared";
-// Shiki theme used throughout the application
-export const SHIKI_THEME = "min-dark";
+export { SHIKI_DARK_THEME, SHIKI_LIGHT_THEME } from "./shiki-shared";
// Maximum diff size to highlight (in bytes)
// Diffs larger than this will fall back to plain text for performance
@@ -21,7 +21,7 @@ export async function getShikiHighlighter(): Promise {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
if (!highlighterPromise) {
highlighterPromise = createHighlighter({
- themes: [SHIKI_THEME],
+ themes: [SHIKI_DARK_THEME, SHIKI_LIGHT_THEME],
langs: [], // Load languages on-demand via highlightDiffChunk
});
}
diff --git a/src/common/constants/storage.ts b/src/common/constants/storage.ts
index e6e9e485e..bfaa80a16 100644
--- a/src/common/constants/storage.ts
+++ b/src/common/constants/storage.ts
@@ -30,6 +30,12 @@ export function getPendingScopeId(projectPath: string): string {
*/
export const GLOBAL_SCOPE_ID = "__global__";
+/**
+ * Get the localStorage key for the UI theme preference (global)
+ * Format: "uiTheme"
+ */
+export const UI_THEME_KEY = "uiTheme";
+
/**
* Helper to create a thinking level storage key for a workspace
* Format: "thinkingLevel:{workspaceId}"