diff --git a/scripts/generate_prism_css.ts b/scripts/generate_prism_css.ts index d95ea9366..9ac077d33 100755 --- a/scripts/generate_prism_css.ts +++ b/scripts/generate_prism_css.ts @@ -25,8 +25,8 @@ for (const [key, value] of Object.entries(vscDarkPlus as Record } // Convert CSS properties object to CSS string -function cssPropertiesToString(props: CSSProperties): string { - return Object.entries(props) +function cssPropertiesToString(props: CSSProperties, selector: string): string { + const entries = Object.entries(props) .filter(([key]) => { // Skip font-family and font-size - we want to inherit these return key !== "fontFamily" && key !== "fontSize"; @@ -35,8 +35,14 @@ function cssPropertiesToString(props: CSSProperties): string { // Convert camelCase to kebab-case const cssKey = key.replace(/([A-Z])/g, "-$1").toLowerCase(); return ` ${cssKey}: ${value};`; - }) - .join("\n"); + }); + + // Add background: transparent to pre/code elements to prevent double backgrounds + if (selector.startsWith("pre") || selector.startsWith("code")) { + entries.push(" background: transparent;"); + } + + return entries.join("\n"); } // Generate CSS content @@ -55,7 +61,7 @@ function generateCSS(): string { ]; for (const [selector, props] of Object.entries(syntaxStyleNoBackgrounds)) { - const cssRules = cssPropertiesToString(props); + const cssRules = cssPropertiesToString(props, selector); if (cssRules.trim().length > 0) { // Handle selectors that need .token prefix let cssSelector = selector; diff --git a/src/components/Messages/AssistantMessage.tsx b/src/components/Messages/AssistantMessage.tsx index 831f382de..8e3f9af0d 100644 --- a/src/components/Messages/AssistantMessage.tsx +++ b/src/components/Messages/AssistantMessage.tsx @@ -21,7 +21,7 @@ const RawContent = styled.pre` word-break: break-word; margin: 0; padding: 8px; - background: rgba(0, 0, 0, 0.2); + background: var(--color-code-bg); border-radius: 3px; `; diff --git a/src/components/Messages/MarkdownComponents.tsx b/src/components/Messages/MarkdownComponents.tsx index e38ab9490..af5079bd2 100644 --- a/src/components/Messages/MarkdownComponents.tsx +++ b/src/components/Messages/MarkdownComponents.tsx @@ -38,7 +38,7 @@ export const markdownComponents = { padding: "0.25em 0.5em", border: "1px solid rgba(255, 255, 255, 0.1)", borderRadius: "4px", - background: "rgba(0, 0, 0, 0.2)", + background: "var(--color-code-bg)", }} > {children} diff --git a/src/components/Messages/Mermaid.tsx b/src/components/Messages/Mermaid.tsx index 7b418eb57..39ffc2d19 100644 --- a/src/components/Messages/Mermaid.tsx +++ b/src/components/Messages/Mermaid.tsx @@ -165,7 +165,7 @@ export const Mermaid: React.FC<{ chart: string }> = ({ chart }) => {
= ({ chart }) => { style={{ position: "relative", margin: "1em 0", - background: "rgba(0, 0, 0, 0.2)", + background: "var(--color-code-bg)", borderRadius: "4px", padding: "16px", }} @@ -243,7 +243,7 @@ export const Mermaid: React.FC<{ chart: string }> = ({ chart }) => { ref={modalContainerRef} className="mermaid-container mermaid-modal" style={{ - background: "rgba(0, 0, 0, 0.2)", + background: "var(--color-code-bg)", padding: "24px", borderRadius: "8px", minWidth: "80vw", diff --git a/src/components/RightSidebar/CodeReview/FileTree.tsx b/src/components/RightSidebar/CodeReview/FileTree.tsx index 89f07f03c..bec1c2435 100644 --- a/src/components/RightSidebar/CodeReview/FileTree.tsx +++ b/src/components/RightSidebar/CodeReview/FileTree.tsx @@ -2,9 +2,11 @@ * FileTree - Displays file hierarchy with diff statistics */ -import React, { useState } from "react"; +import React from "react"; import styled from "@emotion/styled"; import type { FileTreeNode } from "@/utils/git/numstatParser"; +import { usePersistedState } from "@/hooks/usePersistedState"; +import { getFileTreeExpandStateKey } from "@/constants/storage"; const TreeContainer = styled.div` flex: 1; @@ -194,8 +196,30 @@ const TreeNodeContent: React.FC<{ onSelectFile: (path: string | null) => void; commonPrefix: string | null; getFileReadStatus?: (filePath: string) => { total: number; read: number } | null; -}> = ({ node, depth, selectedPath, onSelectFile, commonPrefix, getFileReadStatus }) => { - const [isOpen, setIsOpen] = useState(depth < 2); // Auto-expand first 2 levels + expandStateMap: Record; + setExpandStateMap: ( + value: Record | ((prev: Record) => Record) + ) => void; +}> = ({ + node, + depth, + selectedPath, + onSelectFile, + commonPrefix, + getFileReadStatus, + expandStateMap, + setExpandStateMap, +}) => { + // Check if user has manually set expand state for this directory + const hasManualState = node.path in expandStateMap; + const isOpen = hasManualState ? expandStateMap[node.path] : depth < 2; // Default: auto-expand first 2 levels + + const setIsOpen = (open: boolean) => { + setExpandStateMap((prev) => ({ + ...prev, + [node.path]: open, + })); + }; const handleClick = (e: React.MouseEvent) => { if (node.isDirectory) { @@ -295,6 +319,8 @@ const TreeNodeContent: React.FC<{ onSelectFile={onSelectFile} commonPrefix={commonPrefix} getFileReadStatus={getFileReadStatus} + expandStateMap={expandStateMap} + setExpandStateMap={setExpandStateMap} /> ))} @@ -308,6 +334,7 @@ interface FileTreeExternalProps { isLoading?: boolean; commonPrefix?: string | null; getFileReadStatus?: (filePath: string) => { total: number; read: number } | null; + workspaceId: string; } export const FileTree: React.FC = ({ @@ -317,7 +344,15 @@ export const FileTree: React.FC = ({ isLoading = false, commonPrefix = null, getFileReadStatus, + workspaceId, }) => { + // Use persisted state for expand/collapse per workspace (lifted to parent to avoid O(n) re-renders) + const [expandStateMap, setExpandStateMap] = usePersistedState>( + getFileTreeExpandStateKey(workspaceId), + {}, + { listener: true } + ); + // Find the node at the common prefix path to start rendering from const startNode = React.useMemo(() => { if (!commonPrefix || !root) return root; @@ -355,6 +390,8 @@ export const FileTree: React.FC = ({ onSelectFile={onSelectFile} commonPrefix={commonPrefix} getFileReadStatus={getFileReadStatus} + expandStateMap={expandStateMap} + setExpandStateMap={setExpandStateMap} /> )) ) : ( diff --git a/src/components/RightSidebar/CodeReview/HunkViewer.tsx b/src/components/RightSidebar/CodeReview/HunkViewer.tsx index d1f1c6774..85e48b59c 100644 --- a/src/components/RightSidebar/CodeReview/HunkViewer.tsx +++ b/src/components/RightSidebar/CodeReview/HunkViewer.tsx @@ -107,7 +107,7 @@ const HunkContent = styled.div` font-size: 11px; line-height: 1.4; overflow-x: auto; - background: rgba(0, 0, 0, 0.2); + background: var(--color-code-bg); /* CSS Grid ensures all diff lines span the same width (width of longest line) */ display: grid; diff --git a/src/components/RightSidebar/CodeReview/ReviewPanel.tsx b/src/components/RightSidebar/CodeReview/ReviewPanel.tsx index d02bb2f0e..01c62a578 100644 --- a/src/components/RightSidebar/CodeReview/ReviewPanel.tsx +++ b/src/components/RightSidebar/CodeReview/ReviewPanel.tsx @@ -735,6 +735,7 @@ export const ReviewPanel: React.FC = ({ isLoading={isLoadingTree} commonPrefix={commonPrefix} getFileReadStatus={getFileReadStatus} + workspaceId={workspaceId} /> )} diff --git a/src/components/shared/DiffRenderer.tsx b/src/components/shared/DiffRenderer.tsx index db5e89a1f..9f7e0134f 100644 --- a/src/components/shared/DiffRenderer.tsx +++ b/src/components/shared/DiffRenderer.tsx @@ -111,7 +111,7 @@ export const DiffIndicator = styled.span<{ type: DiffLineType }>` export const DiffContainer = styled.div<{ fontSize?: string; maxHeight?: string }>` margin: 0; padding: 6px 0; - background: rgba(0, 0, 0, 0.2); + background: var(--color-code-bg); border-radius: 3px; font-size: ${({ fontSize }) => fontSize ?? "12px"}; line-height: 1.4; diff --git a/src/components/tools/BashToolCall.tsx b/src/components/tools/BashToolCall.tsx index 265b2c26d..901d0235f 100644 --- a/src/components/tools/BashToolCall.tsx +++ b/src/components/tools/BashToolCall.tsx @@ -30,7 +30,7 @@ const ScriptPreview = styled.span` const OutputBlock = styled.pre` margin: 0; padding: 6px 8px; - background: rgba(0, 0, 0, 0.2); + background: var(--color-code-bg); border-radius: 3px; border-left: 2px solid #4caf50; font-size: 11px; diff --git a/src/components/tools/FileReadToolCall.tsx b/src/components/tools/FileReadToolCall.tsx index 53be4ce1e..3e3a99cde 100644 --- a/src/components/tools/FileReadToolCall.tsx +++ b/src/components/tools/FileReadToolCall.tsx @@ -35,7 +35,7 @@ const MetadataText = styled.span` const ContentBlock = styled.div` margin: 0; padding: 6px 8px; - background: rgba(0, 0, 0, 0.2); + background: var(--color-code-bg); border-radius: 3px; font-size: 11px; line-height: 1.4; @@ -79,7 +79,7 @@ const FileInfoRow = styled.div` flex-wrap: wrap; gap: 16px; padding: 6px 8px; - background: rgba(0, 0, 0, 0.2); + background: var(--color-code-bg); border-radius: 3px; font-size: 11px; line-height: 1.4; diff --git a/src/components/tools/ProposePlanToolCall.tsx b/src/components/tools/ProposePlanToolCall.tsx index 57b3fc89a..c2eb6082f 100644 --- a/src/components/tools/ProposePlanToolCall.tsx +++ b/src/components/tools/ProposePlanToolCall.tsx @@ -96,7 +96,7 @@ const RawContent = styled.pre` word-break: break-word; margin: 0; padding: 8px; - background: rgba(0, 0, 0, 0.2); + background: var(--color-code-bg); border-radius: 3px; `; diff --git a/src/components/tools/shared/ToolPrimitives.tsx b/src/components/tools/shared/ToolPrimitives.tsx index bea2793ea..deda8d39d 100644 --- a/src/components/tools/shared/ToolPrimitives.tsx +++ b/src/components/tools/shared/ToolPrimitives.tsx @@ -95,7 +95,7 @@ export const DetailLabel = styled.div` export const DetailContent = styled.pre` margin: 0; padding: 6px 8px; - background: rgba(0, 0, 0, 0.2); + background: var(--color-code-bg); border-radius: 3px; font-size: 11px; line-height: 1.4; diff --git a/src/constants/storage.ts b/src/constants/storage.ts index 7873f7eb7..2173f6b22 100644 --- a/src/constants/storage.ts +++ b/src/constants/storage.ts @@ -93,6 +93,15 @@ export function getReviewExpandStateKey(workspaceId: string): string { return `reviewExpandState:${workspaceId}`; } +/** + * Get the localStorage key for FileTree expand/collapse state in Review tab + * Stores directory expand/collapse preferences per workspace + * Format: "fileTreeExpandState:{workspaceId}" + */ +export function getFileTreeExpandStateKey(workspaceId: string): string { + return `fileTreeExpandState:${workspaceId}`; +} + /** * List of workspace-scoped key functions that should be copied on fork and deleted on removal * Note: Excludes ephemeral keys like getCompactContinueMessageKey @@ -105,6 +114,7 @@ const PERSISTENT_WORKSPACE_KEY_FUNCTIONS: Array<(workspaceId: string) => string> getAutoRetryKey, getRetryStateKey, getReviewExpandStateKey, + getFileTreeExpandStateKey, ]; /** @@ -117,7 +127,7 @@ const EPHEMERAL_WORKSPACE_KEY_FUNCTIONS: Array<(workspaceId: string) => string> /** * Copy all workspace-specific localStorage keys from source to destination workspace - * This includes: model, input, mode, thinking level, auto-retry, retry state, review expand state + * This includes: model, input, mode, thinking level, auto-retry, retry state, review expand state, file tree expand state */ export function copyWorkspaceStorage(sourceWorkspaceId: string, destWorkspaceId: string): void { for (const getKey of PERSISTENT_WORKSPACE_KEY_FUNCTIONS) { diff --git a/src/styles/colors.tsx b/src/styles/colors.tsx index 5e45bdfba..2951f8cff 100644 --- a/src/styles/colors.tsx +++ b/src/styles/colors.tsx @@ -67,6 +67,11 @@ export const GlobalColors = () => ( --color-text: hsl(0 0% 83%); --color-text-secondary: hsl(0 0% 42%); + /* Code Block Background */ + --color-code-bg: hsl( + 0deg 6.43% 8.04% + ); /* Slightly darker than main background for code/diff containers */ + /* Button Colors */ --color-button-bg: hsl(0 0% 24%); --color-button-text: hsl(0 0% 80%); diff --git a/src/styles/prism-syntax.css b/src/styles/prism-syntax.css index ee31f9fbe..a0ebbf57d 100644 --- a/src/styles/prism-syntax.css +++ b/src/styles/prism-syntax.css @@ -25,8 +25,9 @@ pre[class*="language-"] { ms-hyphens: none; hyphens: none; padding: 1em; - margin: .5em 0; + margin: 0.5em 0; overflow: auto; + background: transparent; } code[class*="language-"] { @@ -64,17 +65,17 @@ code[class*="language-"] *::selection { } :not(pre) > code[class*="language-"] { - padding: .1em .3em; - border-radius: .3em; + padding: 0.1em 0.3em; + border-radius: 0.3em; color: #db4c69; } .namespace { - -opacity: .7; + -opacity: 0.7; } doctype.doctype-tag { - color: #569CD6; + color: #569cd6; } doctype.name { @@ -170,7 +171,7 @@ doctype.name { } operator.arrow { - color: #569CD6; + color: #569cd6; } .token.atrule { @@ -194,7 +195,7 @@ atrule.url.punctuation { } .token.keyword { - color: #569CD6; + color: #569cd6; } keyword.module {