diff --git a/ui/src/components/MarkdownBody.tsx b/ui/src/components/MarkdownBody.tsx
index f6b73872..3b4addbc 100644
--- a/ui/src/components/MarkdownBody.tsx
+++ b/ui/src/components/MarkdownBody.tsx
@@ -1,6 +1,7 @@
import Markdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { cn } from "../lib/utils";
+import { useTheme } from "../context/ThemeContext";
interface MarkdownBodyProps {
children: string;
@@ -8,10 +9,12 @@ interface MarkdownBodyProps {
}
export function MarkdownBody({ children, className }: MarkdownBodyProps) {
+ const { theme } = useTheme();
return (
diff --git a/ui/src/components/SidebarAgents.tsx b/ui/src/components/SidebarAgents.tsx
index bd585961..8687add1 100644
--- a/ui/src/components/SidebarAgents.tsx
+++ b/ui/src/components/SidebarAgents.tsx
@@ -118,7 +118,7 @@ export function SidebarAgents() {
-
+
{runCount} live
diff --git a/ui/src/components/SidebarNavItem.tsx b/ui/src/components/SidebarNavItem.tsx
index 4749f749..cd42c86c 100644
--- a/ui/src/components/SidebarNavItem.tsx
+++ b/ui/src/components/SidebarNavItem.tsx
@@ -8,6 +8,7 @@ interface SidebarNavItemProps {
label: string;
icon: LucideIcon;
end?: boolean;
+ className?: string;
badge?: number;
badgeTone?: "default" | "danger";
alert?: boolean;
@@ -19,6 +20,7 @@ export function SidebarNavItem({
label,
icon: Icon,
end,
+ className,
badge,
badgeTone = "default",
alert = false,
@@ -36,7 +38,8 @@ export function SidebarNavItem({
"flex items-center gap-2.5 px-3 py-2 text-[13px] font-medium transition-colors",
isActive
? "bg-accent text-foreground"
- : "text-foreground/80 hover:bg-accent/50 hover:text-foreground"
+ : "text-foreground/80 hover:bg-accent/50 hover:text-foreground",
+ className,
)
}
>
@@ -53,7 +56,7 @@ export function SidebarNavItem({
-
{liveCount} live
+
{liveCount} live
)}
{badge != null && badge > 0 && (
diff --git a/ui/src/context/ThemeContext.tsx b/ui/src/context/ThemeContext.tsx
new file mode 100644
index 00000000..81c0b8ad
--- /dev/null
+++ b/ui/src/context/ThemeContext.tsx
@@ -0,0 +1,83 @@
+import {
+ createContext,
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useState,
+ type ReactNode,
+} from "react";
+
+type Theme = "light" | "dark";
+
+interface ThemeContextValue {
+ theme: Theme;
+ setTheme: (theme: Theme) => void;
+ toggleTheme: () => void;
+}
+
+const THEME_STORAGE_KEY = "paperclip.theme";
+const DARK_THEME_COLOR = "#18181b";
+const LIGHT_THEME_COLOR = "#ffffff";
+const ThemeContext = createContext
(undefined);
+
+function resolveThemeFromDocument(): Theme {
+ if (typeof document === "undefined") return "dark";
+ return document.documentElement.classList.contains("dark") ? "dark" : "light";
+}
+
+function applyTheme(theme: Theme) {
+ if (typeof document === "undefined") return;
+ const isDark = theme === "dark";
+ const root = document.documentElement;
+ root.classList.toggle("dark", isDark);
+ root.style.colorScheme = isDark ? "dark" : "light";
+ const themeColorMeta = document.querySelector('meta[name="theme-color"]');
+ if (themeColorMeta instanceof HTMLMetaElement) {
+ themeColorMeta.setAttribute("content", isDark ? DARK_THEME_COLOR : LIGHT_THEME_COLOR);
+ }
+}
+
+export function ThemeProvider({ children }: { children: ReactNode }) {
+ const [theme, setThemeState] = useState(() => resolveThemeFromDocument());
+
+ const setTheme = useCallback((nextTheme: Theme) => {
+ setThemeState(nextTheme);
+ }, []);
+
+ const toggleTheme = useCallback(() => {
+ setThemeState((current) => (current === "dark" ? "light" : "dark"));
+ }, []);
+
+ useEffect(() => {
+ applyTheme(theme);
+ try {
+ localStorage.setItem(THEME_STORAGE_KEY, theme);
+ } catch {
+ // Ignore local storage write failures in restricted environments.
+ }
+ }, [theme]);
+
+ const value = useMemo(
+ () => ({
+ theme,
+ setTheme,
+ toggleTheme,
+ }),
+ [theme, setTheme, toggleTheme],
+ );
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useTheme() {
+ const context = useContext(ThemeContext);
+ if (!context) {
+ throw new Error("useTheme must be used within ThemeProvider");
+ }
+ return context;
+}
diff --git a/ui/src/lib/status-colors.ts b/ui/src/lib/status-colors.ts
index 2f3b9c75..beee561a 100644
--- a/ui/src/lib/status-colors.ts
+++ b/ui/src/lib/status-colors.ts
@@ -12,12 +12,12 @@
/** StatusIcon circle: text + border classes */
export const issueStatusIcon: Record = {
backlog: "text-muted-foreground border-muted-foreground",
- todo: "text-blue-400 border-blue-400",
- in_progress: "text-yellow-400 border-yellow-400",
- in_review: "text-violet-400 border-violet-400",
- done: "text-green-400 border-green-400",
+ todo: "text-blue-600 border-blue-600 dark:text-blue-400 dark:border-blue-400",
+ in_progress: "text-yellow-600 border-yellow-600 dark:text-yellow-400 dark:border-yellow-400",
+ in_review: "text-violet-600 border-violet-600 dark:text-violet-400 dark:border-violet-400",
+ done: "text-green-600 border-green-600 dark:text-green-400 dark:border-green-400",
cancelled: "text-neutral-500 border-neutral-500",
- blocked: "text-red-400 border-red-400",
+ blocked: "text-red-600 border-red-600 dark:text-red-400 dark:border-red-400",
};
export const issueStatusIconDefault = "text-muted-foreground border-muted-foreground";
@@ -25,12 +25,12 @@ export const issueStatusIconDefault = "text-muted-foreground border-muted-foregr
/** Text-only color for issue statuses (dropdowns, labels) */
export const issueStatusText: Record = {
backlog: "text-muted-foreground",
- todo: "text-blue-400",
- in_progress: "text-yellow-400",
- in_review: "text-violet-400",
- done: "text-green-400",
+ todo: "text-blue-600 dark:text-blue-400",
+ in_progress: "text-yellow-600 dark:text-yellow-400",
+ in_review: "text-violet-600 dark:text-violet-400",
+ done: "text-green-600 dark:text-green-400",
cancelled: "text-neutral-500",
- blocked: "text-red-400",
+ blocked: "text-red-600 dark:text-red-400",
};
export const issueStatusTextDefault = "text-muted-foreground";
@@ -41,42 +41,42 @@ export const issueStatusTextDefault = "text-muted-foreground";
export const statusBadge: Record = {
// Agent statuses
- active: "bg-green-900/50 text-green-300",
- running: "bg-cyan-900/50 text-cyan-300",
- paused: "bg-orange-900/50 text-orange-300",
- idle: "bg-yellow-900/50 text-yellow-300",
- archived: "bg-neutral-800 text-neutral-400",
+ active: "bg-green-100 text-green-700 dark:bg-green-900/50 dark:text-green-300",
+ running: "bg-cyan-100 text-cyan-700 dark:bg-cyan-900/50 dark:text-cyan-300",
+ paused: "bg-orange-100 text-orange-700 dark:bg-orange-900/50 dark:text-orange-300",
+ idle: "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/50 dark:text-yellow-300",
+ archived: "bg-muted text-muted-foreground",
// Goal statuses
- planned: "bg-neutral-800 text-neutral-400",
- achieved: "bg-green-900/50 text-green-300",
- completed: "bg-green-900/50 text-green-300",
+ planned: "bg-muted text-muted-foreground",
+ achieved: "bg-green-100 text-green-700 dark:bg-green-900/50 dark:text-green-300",
+ completed: "bg-green-100 text-green-700 dark:bg-green-900/50 dark:text-green-300",
// Run statuses
- failed: "bg-red-900/50 text-red-300",
- timed_out: "bg-orange-900/50 text-orange-300",
- succeeded: "bg-green-900/50 text-green-300",
- error: "bg-red-900/50 text-red-300",
- terminated: "bg-red-900/50 text-red-300",
- pending: "bg-yellow-900/50 text-yellow-300",
+ failed: "bg-red-100 text-red-700 dark:bg-red-900/50 dark:text-red-300",
+ timed_out: "bg-orange-100 text-orange-700 dark:bg-orange-900/50 dark:text-orange-300",
+ succeeded: "bg-green-100 text-green-700 dark:bg-green-900/50 dark:text-green-300",
+ error: "bg-red-100 text-red-700 dark:bg-red-900/50 dark:text-red-300",
+ terminated: "bg-red-100 text-red-700 dark:bg-red-900/50 dark:text-red-300",
+ pending: "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/50 dark:text-yellow-300",
// Approval statuses
- pending_approval: "bg-amber-900/50 text-amber-300",
- revision_requested: "bg-amber-900/50 text-amber-300",
- approved: "bg-green-900/50 text-green-300",
- rejected: "bg-red-900/50 text-red-300",
+ pending_approval: "bg-amber-100 text-amber-700 dark:bg-amber-900/50 dark:text-amber-300",
+ revision_requested: "bg-amber-100 text-amber-700 dark:bg-amber-900/50 dark:text-amber-300",
+ approved: "bg-green-100 text-green-700 dark:bg-green-900/50 dark:text-green-300",
+ rejected: "bg-red-100 text-red-700 dark:bg-red-900/50 dark:text-red-300",
// Issue statuses — consistent hues with issueStatusIcon above
- backlog: "bg-neutral-800 text-neutral-400",
- todo: "bg-blue-900/50 text-blue-300",
- in_progress: "bg-yellow-900/50 text-yellow-300",
- in_review: "bg-violet-900/50 text-violet-300",
- blocked: "bg-red-900/50 text-red-300",
- done: "bg-green-900/50 text-green-300",
- cancelled: "bg-neutral-800 text-neutral-500",
+ backlog: "bg-muted text-muted-foreground",
+ todo: "bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300",
+ in_progress: "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/50 dark:text-yellow-300",
+ in_review: "bg-violet-100 text-violet-700 dark:bg-violet-900/50 dark:text-violet-300",
+ blocked: "bg-red-100 text-red-700 dark:bg-red-900/50 dark:text-red-300",
+ done: "bg-green-100 text-green-700 dark:bg-green-900/50 dark:text-green-300",
+ cancelled: "bg-muted text-muted-foreground",
};
-export const statusBadgeDefault = "bg-neutral-800 text-neutral-400";
+export const statusBadgeDefault = "bg-muted text-muted-foreground";
// ---------------------------------------------------------------------------
// Agent status dot — solid background for small indicator dots
@@ -99,10 +99,10 @@ export const agentStatusDotDefault = "bg-neutral-400";
// ---------------------------------------------------------------------------
export const priorityColor: Record = {
- critical: "text-red-400",
- high: "text-orange-400",
- medium: "text-yellow-400",
- low: "text-blue-400",
+ critical: "text-red-600 dark:text-red-400",
+ high: "text-orange-600 dark:text-orange-400",
+ medium: "text-yellow-600 dark:text-yellow-400",
+ low: "text-blue-600 dark:text-blue-400",
};
-export const priorityColorDefault = "text-yellow-400";
+export const priorityColorDefault = "text-yellow-600 dark:text-yellow-400";
diff --git a/ui/src/main.tsx b/ui/src/main.tsx
index 58aec541..8927cf86 100644
--- a/ui/src/main.tsx
+++ b/ui/src/main.tsx
@@ -10,6 +10,7 @@ import { PanelProvider } from "./context/PanelContext";
import { SidebarProvider } from "./context/SidebarContext";
import { DialogProvider } from "./context/DialogContext";
import { ToastProvider } from "./context/ToastContext";
+import { ThemeProvider } from "./context/ThemeContext";
import { TooltipProvider } from "@/components/ui/tooltip";
import "@mdxeditor/editor/style.css";
import "./index.css";
@@ -26,25 +27,27 @@ const queryClient = new QueryClient({
createRoot(document.getElementById("root")!).render(
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
diff --git a/ui/src/pages/ApprovalDetail.tsx b/ui/src/pages/ApprovalDetail.tsx
index 2b8e8ac1..49420373 100644
--- a/ui/src/pages/ApprovalDetail.tsx
+++ b/ui/src/pages/ApprovalDetail.tsx
@@ -165,16 +165,16 @@ export function ApprovalDetail() {
return (
{showApprovedBanner && (
-
+
-
-
+
+
-
Approval confirmed
-
+
Approval confirmed
+
Requesting agent was notified to review this approval and linked issues.
@@ -182,7 +182,7 @@ export function ApprovalDetail() {