Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 48 additions & 5 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -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 />
</>
),
(Story, context) => {
const mode = (context.globals.theme ?? "dark") as ThemeMode;
return (
<ThemeProvider>
<ThemeStorySync mode={mode} />
<Story />
</ThemeProvider>
);
},
],
parameters: {
controls: {
Expand All @@ -16,6 +53,12 @@ const preview: Preview = {
date: /Date$/i,
},
},
chromatic: {
modes: {
dark: { globals: { theme: "dark" } },
light: { globals: { theme: "light" } },
},
},
},
};

Expand Down
11 changes: 7 additions & 4 deletions bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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=="],

Expand Down Expand Up @@ -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=="],

Expand Down Expand Up @@ -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=="],

Expand Down Expand Up @@ -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=="],

Expand Down Expand Up @@ -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=="],
Expand Down
27 changes: 26 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,38 @@
margin: 0;
padding: 0;
overflow: hidden;
background: #1e1e1e;
background: var(--color-background, #1e1e1e);
}
#root {
height: 100vh;
overflow: hidden;
}
</style>
<script>
(function () {
const THEME_KEY = "uiTheme";
try {
const stored = window.localStorage.getItem(THEME_KEY);
const parsed = stored ? JSON.parse(stored) : null;
const prefersLight = window.matchMedia
? window.matchMedia("(prefers-color-scheme: light)").matches
: false;
const theme = parsed === "light" || parsed === "dark" ? parsed : prefersLight ? "light" : "dark";

document.documentElement.dataset.theme = theme;
document.documentElement.style.colorScheme = theme;

const metaThemeColor = document.querySelector('meta[name="theme-color"]');
if (metaThemeColor) {
metaThemeColor.setAttribute("content", theme === "light" ? "#f5f6f8" : "#1e1e1e");
}
} catch (error) {
console.warn("Failed to apply preferred theme early", error);
document.documentElement.dataset.theme = "dark";
document.documentElement.style.colorScheme = "dark";
}
})();
</script>
</head>
<body>
<div id="root"></div>
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
21 changes: 17 additions & 4 deletions src/browser/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -389,6 +397,7 @@ function AppInner() {
projects,
workspaceMetadata,
selectedWorkspace,
theme,
getThinkingLevel: getThinkingLevelForWorkspace,
onSetThinkingLevel: setThinkingLevelFromPalette,
onStartWorkspaceCreation: openNewWorkspaceFromPalette,
Expand All @@ -401,6 +410,8 @@ function AppInner() {
onToggleSidebar: toggleSidebarFromPalette,
onNavigateWorkspace: navigateWorkspaceFromPalette,
onOpenWorkspaceInTerminal: openWorkspaceInTerminal,
onToggleTheme: toggleTheme,
onSetTheme: setThemePreference,
};

useEffect(() => {
Expand Down Expand Up @@ -587,7 +598,7 @@ function AppInner() {
})()
) : (
<div
className="[&_p]:text-muted mx-auto w-full max-w-3xl text-center [&_h2]:mb-4 [&_h2]:font-bold [&_h2]:tracking-tight [&_h2]:text-white [&_p]:leading-[1.6]"
className="[&_p]:text-muted [&_h2]:text-foreground mx-auto w-full max-w-3xl text-center [&_h2]:mb-4 [&_h2]:font-bold [&_h2]:tracking-tight [&_p]:leading-[1.6]"
style={{
padding: "clamp(40px, 10vh, 100px) 20px",
fontSize: "clamp(14px, 2vw, 16px)",
Expand Down Expand Up @@ -619,9 +630,11 @@ function AppInner() {

function App() {
return (
<CommandRegistryProvider>
<AppInner />
</CommandRegistryProvider>
<ThemeProvider>
<CommandRegistryProvider>
<AppInner />
</CommandRegistryProvider>
</ThemeProvider>
);
}

Expand Down
24 changes: 16 additions & 8 deletions src/browser/components/CommandPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ export const CommandPalette: React.FC<CommandPaletteProps> = ({ getSlashContext
}}
>
<Command
className="bg-separator border-border text-lighter font-primary w-[min(720px,92vw)] overflow-hidden rounded-lg border shadow-[0_10px_40px_rgba(0,0,0,0.4)]"
className="font-primary w-[min(720px,92vw)] overflow-hidden rounded-lg border border-[var(--color-command-border)] bg-[var(--color-command-surface)] text-[var(--color-command-foreground)] shadow-[0_10px_40px_rgba(0,0,0,0.4)]"
onMouseDown={(e: React.MouseEvent) => e.stopPropagation()}
shouldFilter={shouldUseCmdkFilter}
filter={(value, search) => {
Expand All @@ -395,7 +395,7 @@ export const CommandPalette: React.FC<CommandPaletteProps> = ({ getSlashContext
}}
>
<Command.Input
className="bg-darker text-lighter border-hover w-full border-b border-none px-3.5 py-3 text-sm outline-none"
className="w-full border-b border-[var(--color-command-input-border)] bg-[var(--color-command-input)] px-3.5 py-3 text-sm text-[var(--color-command-foreground)] outline-none placeholder:text-[var(--color-command-subdued)]"
value={query}
onValueChange={handleQueryChange}
placeholder={
Expand Down Expand Up @@ -428,13 +428,17 @@ export const CommandPalette: React.FC<CommandPaletteProps> = ({ getSlashContext
{groupsWithItems.map((group) => (
<Command.Group
key={group.name}
heading={group.name}
className="[&[cmdk-group-heading]]:text-subdued [&[cmdk-group-heading]]:px-2.5 [&[cmdk-group-heading]]:py-1 [&[cmdk-group-heading]]:text-[11px] [&[cmdk-group-heading]]:tracking-[0.08em] [&[cmdk-group-heading]]:uppercase [&[cmdk-group]]:px-1.5 [&[cmdk-group]]:py-2"
heading={
<div className="px-2.5 py-1 text-[11px] tracking-[0.08em] text-[var(--color-command-subdued)] uppercase">
{group.name}
</div>
}
className="px-1.5 py-2"
>
{group.items.map((item) => (
<Command.Item
key={item.id}
className="hover:bg-hover aria-selected:bg-hover mx-1 my-0.5 grid cursor-pointer grid-cols-[1fr_auto] items-center gap-2 rounded-md px-3 py-2 text-[13px]"
className="hover:bg-hover aria-selected:bg-hover mx-1 my-0.5 grid cursor-pointer grid-cols-[1fr_auto] items-center gap-2 rounded-md px-3 py-2 text-[13px] aria-selected:text-[var(--color-command-foreground)]"
onSelect={() => {
if ("prompt" in item && item.prompt) {
addRecent(item.id);
Expand All @@ -459,12 +463,14 @@ export const CommandPalette: React.FC<CommandPaletteProps> = ({ getSlashContext
{"subtitle" in item && item.subtitle && (
<>
<br />
<span className="text-subdued text-xs">{item.subtitle}</span>
<span className="text-xs text-[var(--color-command-subdued)]">
{item.subtitle}
</span>
</>
)}
</div>
{"shortcutHint" in item && item.shortcutHint && (
<span className="text-subdued font-monospace text-[11px]">
<span className="font-monospace text-[11px] text-[var(--color-command-subdued)]">
{item.shortcutHint}
</span>
)}
Expand All @@ -473,7 +479,9 @@ export const CommandPalette: React.FC<CommandPaletteProps> = ({ getSlashContext
</Command.Group>
))}
{!hasAnyItems && (
<div className="text-gray p-4 text-[13px]">{emptyText ?? "No results"}</div>
<div className="p-4 text-[13px] text-[var(--color-command-subdued)]">
{emptyText ?? "No results"}
</div>
)}
</Command.List>
</Command>
Expand Down
4 changes: 2 additions & 2 deletions src/browser/components/KebabMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,8 @@ export const KebabMenu: React.FC<KebabMenuProps> = ({ 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 && (
Expand Down
4 changes: 2 additions & 2 deletions src/browser/components/Messages/HistoryHiddenMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ export const HistoryHiddenMessage: React.FC<HistoryHiddenMessageProps> = ({
return (
<div
className={cn(
"my-5 py-3 px-[15px] bg-white/[0.03] border-l-[3px] border-accent rounded-sm",
"text-muted text-xs font-normal text-center font-sans",
"my-5 rounded-sm border-l-[3px] border-accent bg-[var(--color-message-hidden-bg)] px-[15px] py-3",
"font-sans text-center text-xs font-normal text-muted",
className
)}
>
Expand Down
12 changes: 9 additions & 3 deletions src/browser/components/Messages/MarkdownComponents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -45,6 +47,7 @@ interface CodeBlockProps {
*/
const CodeBlock: React.FC<CodeBlockProps> = ({ code, language }) => {
const [highlightedLines, setHighlightedLines] = useState<string[] | null>(null);
const { theme: themeMode } = useTheme();

// Split code into lines, removing trailing empty line
const plainLines = code
Expand All @@ -53,6 +56,9 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ code, language }) => {

useEffect(() => {
let cancelled = false;
const shikiTheme = themeMode === "light" ? SHIKI_LIGHT_THEME : SHIKI_DARK_THEME;

setHighlightedLines(null);

async function highlight() {
try {
Expand All @@ -79,7 +85,7 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ code, language }) => {

const html = highlighter.codeToHtml(code, {
lang: shikiLang,
theme: SHIKI_THEME,
theme: shikiTheme,
});

if (!cancelled) {
Expand All @@ -100,7 +106,7 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ code, language }) => {
return () => {
cancelled = true;
};
}, [code, language]);
}, [code, language, themeMode]);

const lines = highlightedLines ?? plainLines;

Expand Down
Loading