Skip to content
Closed
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
29 changes: 24 additions & 5 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,31 @@ import type { Preview } from "@storybook/react-vite";
import "../src/browser/styles/globals.css";

const preview: Preview = {
globalTypes: {
theme: {
description: "Global theme for components",
defaultValue: "dark",
toolbar: {
title: "Theme",
icon: "circlehollow",
items: [
{ value: "light", title: "Light", icon: "sun" },
{ value: "dark", title: "Dark", icon: "moon" },
],
dynamicTitle: true,
},
},
},
decorators: [
(Story) => (
<>
<Story />
</>
),
(Story, context) => {
const theme = context.globals.theme || "dark";

return (
<div data-theme={theme} style={{ minHeight: "100vh", backgroundColor: "var(--color-background)", color: "var(--color-foreground)" }}>
<Story />
</div>
);
},
],
parameters: {
controls: {
Expand Down
19 changes: 16 additions & 3 deletions src/browser/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { CommandRegistryProvider, useCommandRegistry } from "./contexts/CommandR
import type { CommandAction } from "./contexts/CommandRegistryContext";
import { ModeProvider } from "./contexts/ModeContext";
import { ThinkingProvider } from "./contexts/ThinkingContext";
import { ThemeProvider } from "./contexts/ThemeContext";
import { CommandPalette } from "./components/CommandPalette";
import { buildCoreSources, type BuildSourcesParams } from "./utils/commands/sources";

Expand All @@ -32,6 +33,7 @@ import { getThinkingLevelKey } from "@/common/constants/storage";
import type { BranchListResult } from "@/common/types/ipc";
import { useTelemetry } from "./hooks/useTelemetry";
import { useStartWorkspaceCreation, getFirstProjectPath } from "./hooks/useStartWorkspaceCreation";
import { useTheme } from "./contexts/ThemeContext";

const THINKING_LEVELS: ThinkingLevel[] = ["off", "low", "medium", "high"];

Expand Down Expand Up @@ -60,6 +62,10 @@ function AppInner() {
// Auto-collapse sidebar on mobile by default
const isMobile = typeof window !== "undefined" && window.innerWidth <= 768;
const [sidebarCollapsed, setSidebarCollapsed] = usePersistedState("sidebarCollapsed", isMobile);

// Theme state (global, not workspace-specific)
const { toggleTheme } = useTheme();

const defaultProjectPath = getFirstProjectPath(projects);
const creationChatInputRef = useRef<ChatInputAPI | null>(null);
const creationProjectPath = !selectedWorkspace
Expand Down Expand Up @@ -402,6 +408,7 @@ function AppInner() {
onAddProject: addProjectFromPalette,
onRemoveProject: removeProjectFromPalette,
onToggleSidebar: toggleSidebarFromPalette,
onToggleTheme: toggleTheme,
onNavigateWorkspace: navigateWorkspaceFromPalette,
onOpenWorkspaceInTerminal: openWorkspaceInTerminal,
};
Expand Down Expand Up @@ -618,9 +625,15 @@ function AppInner() {

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

Expand Down
10 changes: 3 additions & 7 deletions src/browser/components/Messages/MarkdownComponents.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import type { ReactNode } from "react";
import React, { useState, useEffect } from "react";
import { Mermaid } from "./Mermaid";
import {
getShikiHighlighter,
mapToShikiLang,
SHIKI_THEME,
} from "@/browser/utils/highlighting/shikiHighlighter";
import { extractShikiLines } from "@/browser/utils/highlighting/shiki-shared";
import { getShikiHighlighter, mapToShikiLang } from "@/browser/utils/highlighting/shikiHighlighter";
import { extractShikiLines, getShikiTheme } from "@/browser/utils/highlighting/shiki-shared";
import { CopyButton } from "@/browser/components/ui/CopyButton";

interface CodeProps {
Expand Down Expand Up @@ -79,7 +75,7 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ code, language }) => {

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

if (!cancelled) {
Expand Down
53 changes: 33 additions & 20 deletions src/browser/components/Messages/Mermaid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,36 @@ import { usePersistedState } from "@/browser/hooks/usePersistedState";
const MIN_HEIGHT = 300;
const MAX_HEIGHT = 1200;

// Initialize mermaid
mermaid.initialize({
startOnLoad: false,
theme: "dark",
layout: "elk",
securityLevel: "loose",
fontFamily: "var(--font-monospace)",
darkMode: true,
elk: {
nodePlacementStrategy: "LINEAR_SEGMENTS",
mergeEdges: true,
},
wrap: true,
markdownAutoWrap: true,
flowchart: {
nodeSpacing: 60,
curve: "linear",
defaultRenderer: "elk",
},
});
/**
* Get mermaid theme configuration based on current theme
*/
function getMermaidConfig() {
const isDark =
typeof document === "undefined" || document.documentElement.dataset.theme !== "light";
const theme: "dark" | "default" = isDark ? "dark" : "default";
return {
startOnLoad: false,
theme,
layout: "elk",
securityLevel: "loose" as const,
fontFamily: "var(--font-monospace)",
darkMode: isDark,
elk: {
nodePlacementStrategy: "LINEAR_SEGMENTS" as const,
mergeEdges: true,
},
wrap: true,
markdownAutoWrap: true,
flowchart: {
nodeSpacing: 60,
curve: "linear" as const,
defaultRenderer: "elk" as const,
},
};
}

// Initialize mermaid with default dark theme
mermaid.initialize(getMermaidConfig());

// Common button styles
const getButtonStyle = (disabled = false): CSSProperties => ({
Expand Down Expand Up @@ -141,6 +151,9 @@ export const Mermaid: React.FC<{ chart: string }> = ({ chart }) => {
try {
setError(null);

// Re-initialize mermaid with current theme
mermaid.initialize(getMermaidConfig());

// Parse first to validate syntax without rendering
await mermaid.parse(chart);

Expand Down
40 changes: 40 additions & 0 deletions src/browser/contexts/ThemeContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { createContext, useContext, useEffect, type ReactNode } from "react";
import { usePersistedState } from "../hooks/usePersistedState";
import { THEME_KEY } from "@/common/constants/storage";

type Theme = "light" | "dark";

interface ThemeContextValue {
theme: Theme;
setTheme: (theme: Theme) => void;
toggleTheme: () => void;
}

const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);

export function ThemeProvider(props: { children: ReactNode }) {
const [theme, setTheme] = usePersistedState<Theme>(THEME_KEY, "dark");

// Apply theme to document root
useEffect(() => {
document.documentElement.dataset.theme = theme;
}, [theme]);

const toggleTheme = () => {
setTheme((prev) => (prev === "dark" ? "light" : "dark"));
};

return (
<ThemeContext.Provider value={{ theme, setTheme, toggleTheme }}>
{props.children}
</ThemeContext.Provider>
);
}

export function useTheme(): ThemeContextValue {
const context = useContext(ThemeContext);
if (!context) {
throw new Error("useTheme must be used within ThemeProvider");
}
return context;
}
Loading