diff --git a/apps/server/src/routes/agents/lifecycle-routes.ts b/apps/server/src/routes/agents/lifecycle-routes.ts index d9d54f7b..368268c5 100644 --- a/apps/server/src/routes/agents/lifecycle-routes.ts +++ b/apps/server/src/routes/agents/lifecycle-routes.ts @@ -332,7 +332,9 @@ export async function registerAgentLifecycleRoutes( (agent.worktreePath || gitContextWorktreePath ? "main" : null); try { - const result = await getAgentDiff(worktreePath, baseRef); + const result = await getAgentDiff(worktreePath, baseRef, undefined, { + ignoreWhitespace: true, + }); if (!result) { return { baseRef: null, files: [] }; } @@ -377,7 +379,13 @@ export async function registerAgentLifecycleRoutes( (agent.worktreePath || gitContextWorktreePath ? "main" : null); try { - const result = await getAgentFileDiff(worktreePath, baseRef, query.path); + const result = await getAgentFileDiff( + worktreePath, + baseRef, + query.path, + undefined, + { ignoreWhitespace: true } + ); if (!result) { return reply.code(404).send({ error: "File not found in diff." }); } diff --git a/apps/server/src/shared/git/agent-diff.ts b/apps/server/src/shared/git/agent-diff.ts index f8270c0b..12422ca8 100644 --- a/apps/server/src/shared/git/agent-diff.ts +++ b/apps/server/src/shared/git/agent-diff.ts @@ -187,7 +187,8 @@ function buildUntrackedDiff(filePath: string, content: string): string { export async function getAgentDiff( worktreePath: string, baseRef: string | null, - run: CommandRunner = runCommand + run: CommandRunner = runCommand, + options?: { ignoreWhitespace?: boolean } ): Promise { const resolvedBase = await resolveBaseRef(worktreePath, baseRef, { runCommand: run, @@ -204,19 +205,36 @@ export async function getAgentDiff( } const mergeBaseSha = mergeBaseResult.stdout.trim(); + const wsFlag = options?.ignoreWhitespace ? ["-w"] : []; const [numstatResult, statusResult, diffResult, untrackedResult] = await Promise.all([ - run("git", ["-C", worktreePath, "diff", mergeBaseSha, "--numstat"], { - allowedExitCodes: [0], - timeoutMs: GIT_TIMEOUT_MS, - }), - run("git", ["-C", worktreePath, "diff", mergeBaseSha, "--name-status"], { - allowedExitCodes: [0], - timeoutMs: GIT_TIMEOUT_MS, - }), run( "git", - ["-C", worktreePath, "diff", mergeBaseSha, "-U3", "--no-color"], + ["-C", worktreePath, "diff", mergeBaseSha, "--numstat", ...wsFlag], + { + allowedExitCodes: [0], + timeoutMs: GIT_TIMEOUT_MS, + } + ), + run( + "git", + ["-C", worktreePath, "diff", mergeBaseSha, "--name-status", ...wsFlag], + { + allowedExitCodes: [0], + timeoutMs: GIT_TIMEOUT_MS, + } + ), + run( + "git", + [ + "-C", + worktreePath, + "diff", + mergeBaseSha, + "-U3", + "--no-color", + ...wsFlag, + ], { allowedExitCodes: [0], timeoutMs: GIT_TIMEOUT_MS, @@ -298,7 +316,8 @@ export async function getAgentFileDiff( worktreePath: string, baseRef: string | null, filePath: string, - run: CommandRunner = runCommand + run: CommandRunner = runCommand, + options?: { ignoreWhitespace?: boolean } ): Promise { const resolvedBase = await resolveBaseRef(worktreePath, baseRef, { runCommand: run, @@ -315,10 +334,20 @@ export async function getAgentFileDiff( } const mergeBaseSha = mergeBaseResult.stdout.trim(); + const wsFlag2 = options?.ignoreWhitespace ? ["-w"] : []; const [numstatResult, statusResult, diffResult] = await Promise.all([ run( "git", - ["-C", worktreePath, "diff", mergeBaseSha, "--numstat", "--", filePath], + [ + "-C", + worktreePath, + "diff", + mergeBaseSha, + "--numstat", + ...wsFlag2, + "--", + filePath, + ], { allowedExitCodes: [0], timeoutMs: GIT_TIMEOUT_MS } ), run( @@ -329,6 +358,7 @@ export async function getAgentFileDiff( "diff", mergeBaseSha, "--name-status", + ...wsFlag2, "--", filePath, ], @@ -343,6 +373,7 @@ export async function getAgentFileDiff( mergeBaseSha, "-U3", "--no-color", + ...wsFlag2, "--", filePath, ], diff --git a/apps/web/index.html b/apps/web/index.html index d09e028c..e78fc993 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -68,10 +68,15 @@ midnight: { background: "#000000", foreground: "#f1f1f4" }, mytra: { background: "#030303", foreground: "#dee2e7" }, }; + var lightThemes = { light: 1, "solarized-light": 1, daylight: 1 }; var selected = palette[theme] || palette.default; if (theme && theme !== "default") { document.documentElement.setAttribute("data-theme", theme); } + document.documentElement.setAttribute( + "data-theme-mode", + lightThemes[theme] ? "light" : "dark" + ); document.documentElement.style.backgroundColor = selected.background; document.body.style.backgroundColor = selected.background; document.body.style.color = selected.foreground; diff --git a/apps/web/package.json b/apps/web/package.json index f9b04090..95f1299e 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -31,6 +31,7 @@ "cmdk": "^1.1.1", "cronstrue": "^3.14.0", "date-fns": "^4.1.0", + "diff-match-patch": "^1.0.5", "framer-motion": "^12.35.2", "highlight.js": "^11.11.1", "jotai": "^2.19.0", @@ -55,6 +56,7 @@ "@eslint/js": "^9.39.4", "@tailwindcss/typography": "^0.5.19", "@testing-library/react": "^16.3.2", + "@types/diff-match-patch": "^1.0.36", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@vitejs/plugin-react": "^4.7.0", diff --git a/apps/web/src/components/app/agents-view.tsx b/apps/web/src/components/app/agents-view.tsx index b71eb5e9..f7a4a444 100644 --- a/apps/web/src/components/app/agents-view.tsx +++ b/apps/web/src/components/app/agents-view.tsx @@ -487,32 +487,31 @@ export function AgentsView({ )} onTransitionEnd={handleFeedbackTransitionEnd} > -
-
- {!leftPanelOpen ? ( - - ) : null} - - - -
-
-
+
+
+
+ {!leftPanelOpen ? ( + + ) : null} + + + +
+
{focusedAgent?.name ? ( <> ) : null}
- {hasActiveAgent && (!mediaPanelOpen || isMobile) ? ( -
+
+ {hasActiveAgent && (!mediaPanelOpen || isMobile) ? ( -
- ) : null} + ) : null} +
{connState === "reconnecting" ? ( -
+
) : null} +
+
{!isMobile ? ( -
+
) : null} diff --git a/apps/web/src/components/app/changes-tab.tsx b/apps/web/src/components/app/changes-tab.tsx index f4673d57..2e1ccc60 100644 --- a/apps/web/src/components/app/changes-tab.tsx +++ b/apps/web/src/components/app/changes-tab.tsx @@ -8,11 +8,52 @@ import { FilePlus, FileText, Loader2, + PanelLeftClose, + PanelLeftOpen, } from "lucide-react"; import { AnimatePresence, motion } from "framer-motion"; -import { Diff, Hunk, parseDiff, tokenize } from "react-diff-view"; +import { Diff, Hunk, markEdits, parseDiff, tokenize } from "react-diff-view"; import "react-diff-view/style/index.css"; import { refractor as baseRefractor } from "refractor"; +import jsx from "refractor/jsx"; +import tsx from "refractor/tsx"; +import scss from "refractor/scss"; +import toml from "refractor/toml"; +import diff from "refractor/diff"; +import docker from "refractor/docker"; +import graphql from "refractor/graphql"; +import elixir from "refractor/elixir"; +import haskell from "refractor/haskell"; +import lua from "refractor/lua"; +import php from "refractor/php"; +import scala from "refractor/scala"; +import dart from "refractor/dart"; +import r from "refractor/r"; +import perl from "refractor/perl"; +import zig from "refractor/zig"; +import nim from "refractor/nim"; +import objectivec from "refractor/objectivec"; +import shell from "refractor/shell-session"; + +baseRefractor.register(jsx); +baseRefractor.register(tsx); +baseRefractor.register(scss); +baseRefractor.register(toml); +baseRefractor.register(diff); +baseRefractor.register(docker); +baseRefractor.register(graphql); +baseRefractor.register(elixir); +baseRefractor.register(haskell); +baseRefractor.register(lua); +baseRefractor.register(php); +baseRefractor.register(scala); +baseRefractor.register(dart); +baseRefractor.register(r); +baseRefractor.register(perl); +baseRefractor.register(zig); +baseRefractor.register(nim); +baseRefractor.register(objectivec); +baseRefractor.register(shell); const refractorAdapter = { highlight(code: string, language: string) { @@ -44,6 +85,7 @@ export const ChangesTab = memo(function ChangesTab({ const { data, isLoading } = useAgentDiff(agentId, active); const [collapsedFiles, setCollapsedFiles] = useState>(new Set()); const [selectedFile, setSelectedFile] = useState(null); + const [showFileTree, setShowFileTree] = useState(true); const fileRefs = useRef>(new Map()); const files = useMemo( @@ -87,15 +129,20 @@ export const ChangesTab = memo(function ChangesTab({ return (
- + {showFileTree && ( + setShowFileTree(false)} + /> + )} setShowFileTree(true)} onToggleCollapse={(path) => { setCollapsedFiles((prev) => { const next = new Set(prev); @@ -140,6 +187,7 @@ type FileTreeProps = { files: DiffFile[]; selectedFile: string | null; onSelectFile: (path: string) => void; + onClose: () => void; }; type TreeNode = { @@ -198,6 +246,7 @@ function FileTree({ files, selectedFile, onSelectFile, + onClose, }: FileTreeProps): JSX.Element { const [collapsedDirs, setCollapsedDirs] = useState>(new Set()); const tree = useMemo(() => buildTree(files), [files]); @@ -216,8 +265,18 @@ function FileTree({ return (
-
- {files.length} file{files.length !== 1 ? "s" : ""} changed +
+ + {files.length} file{files.length !== 1 ? "s" : ""} changed + +
{(tree.file ? [tree] : [...tree.children.values()]).map((child) => ( @@ -328,6 +387,8 @@ type DiffPaneProps = { agentId: string | null; files: DiffFile[]; collapsedFiles: Set; + showFileTreeToggle: boolean; + onShowFileTree: () => void; onToggleCollapse: (path: string) => void; fileRefs: React.RefObject | null>; }; @@ -336,27 +397,43 @@ function DiffPane({ agentId, files, collapsedFiles, + showFileTreeToggle, + onShowFileTree, onToggleCollapse, fileRefs, }: DiffPaneProps): JSX.Element { return ( -
- {files.map((file) => ( - onToggleCollapse(file.path)} - setRef={(el) => { - if (el) { - fileRefs.current?.set(file.path, el); - } else { - fileRefs.current?.delete(file.path); - } - }} - /> - ))} +
+ {showFileTreeToggle && ( +
+ +
+ )} +
+ {files.map((file) => ( + onToggleCollapse(file.path)} + setRef={(el) => { + if (el) { + fileRefs.current?.set(file.path, el); + } else { + fileRefs.current?.delete(file.path); + } + }} + /> + ))} +
); } @@ -491,14 +568,18 @@ const EXT_TO_LANGUAGE: Record = { jsx: "jsx", json: "json", css: "css", - scss: "css", + scss: "scss", html: "markup", + xml: "markup", + svg: "markup", md: "markdown", yaml: "yaml", yml: "yaml", + toml: "toml", sql: "sql", sh: "bash", bash: "bash", + zsh: "bash", py: "python", rs: "rust", go: "go", @@ -510,10 +591,37 @@ const EXT_TO_LANGUAGE: Record = { cpp: "cpp", h: "c", hpp: "cpp", + diff: "diff", + patch: "diff", + graphql: "graphql", + gql: "graphql", + dockerfile: "docker", + ex: "elixir", + exs: "elixir", + hs: "haskell", + lua: "lua", + php: "php", + scala: "scala", + dart: "dart", + r: "r", + pl: "perl", + pm: "perl", + zig: "zig", + nim: "nim", + m: "objectivec", +}; + +const FILENAME_TO_LANGUAGE: Record = { + Dockerfile: "docker", + Makefile: "makefile", }; function languageFromPath(filePath: string): string | null { - const ext = filePath.split(".").pop()?.toLowerCase(); + const basename = filePath.split("/").pop() ?? ""; + const byName = FILENAME_TO_LANGUAGE[basename]; + if (byName && refractorAdapter.registered(byName)) return byName; + + const ext = basename.split(".").pop()?.toLowerCase(); if (!ext) return null; const lang = EXT_TO_LANGUAGE[ext]; if (!lang) return null; @@ -543,13 +651,17 @@ const UnifiedDiffView = memo(function UnifiedDiffView({ const tokens = useMemo(() => { if (parsed.length === 0) return undefined; const file = parsed[0]!; - if (!language) return undefined; + const enhancers = [markEdits(file.hunks)]; try { - return tokenize(file.hunks, { - highlight: true, - refractor: refractorAdapter, - language, - }); + if (language) { + return tokenize(file.hunks, { + highlight: true, + refractor: refractorAdapter, + language, + enhancers, + }); + } + return tokenize(file.hunks, { enhancers }); } catch { return undefined; } diff --git a/apps/web/src/components/app/design-lab.tsx b/apps/web/src/components/app/design-lab.tsx index 6afec624..71b30f7d 100644 --- a/apps/web/src/components/app/design-lab.tsx +++ b/apps/web/src/components/app/design-lab.tsx @@ -1,10 +1,135 @@ -import { useEffect, useState } from "react"; +import { useMemo } from "react"; import { THEMES, useTheme, type ThemeId } from "@/hooks/use-theme"; -import { PathInput } from "@/components/app/path-input"; +import { Diff, Hunk, markEdits, parseDiff, tokenize } from "react-diff-view"; +import "react-diff-view/style/index.css"; +import { refractor } from "refractor"; +import tsx from "refractor/tsx"; +import javascript from "refractor/javascript"; -// Canvas for one-off component experiments. Add sections below as needed and -// remove them once the work ships — this page intentionally stays empty -// between projects so it's always ready for the next variant exploration. +refractor.register(tsx); +refractor.register(javascript); + +const refractorAdapter = { + highlight(code: string, language: string) { + return refractor.highlight(code, language).children; + }, + registered(language: string) { + return refractor.registered(language); + }, +}; + +const SAMPLE_DIFF = `diff --git a/apps/web/src/components/app/agents-view.tsx b/apps/web/src/components/app/agents-view.tsx +--- a/apps/web/src/components/app/agents-view.tsx ++++ b/apps/web/src/components/app/agents-view.tsx +@@ -1,11 +1,5 @@ + import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +-import { +- Routes, +- Route, +- useMatch, +- useNavigate, +- useParams, +-} from "react-router-dom"; ++import { Routes, Route, useParams } from "react-router-dom"; + import { PanelLeftOpen, PanelRightOpen } from "lucide-react"; + + import { ChangesTab } from "@/components/app/changes-tab"; +@@ -23,12 +17,9 @@ + import { CreateAgentDialog } from "@/components/app/create-agent-dialog"; + import { DeleteAgentDialog } from "@/components/app/delete-agent-dialog"; + import { +- type FeedbackDetailState, +- FeedbackDetailPanel, +- MobileFeedbackSheet, +- MobileReviewSummarySheet, +- ReviewSummaryPanel, +-} from "@/components/app/feedback-panel"; ++ DesktopFeedbackDetail, ++ MobileFeedbackDetail, ++} from "@/components/app/agents-view-feedback-detail"; + import { MediaLightbox } from "@/components/app/media-lightbox"; + import { cn } from "@/lib/utils"; + import { useAgentActions } from "@/hooks/use-agent-actions"; +@@ -52,13 +43,7 @@ + }: AgentsViewProps): JSX.Element { +- const navigate = useNavigate(); + const { agentId: routeAgentId } = useParams(); +- const feedbackMatch = useMatch("/agents/:agentId/feedback/:itemId"); +- const reviewMatch = useMatch("/agents/:agentId/review/:summaryAgentId"); +- const changesMatch = useMatch("/agents/:agentId/changes"); +- const itemId = feedbackMatch?.params.itemId; +- const summaryAgentId = reviewMatch?.params.summaryAgentId; + + const [sharedConnectedAgentId, setSharedConnectedAgentId] = useState< + string | null +@@ -80,6 +65,22 @@ + routeAgentId ?? null + ); + ++ const { ++ changesMatch, ++ feedbackDetail, ++ feedbackDetailRendered, ++ handleFeedbackTransitionEnd, ++ closeFeedbackDetail, ++ openFeedbackDetail, ++ navigateFeedbackItem, ++ onTabChange, ++ } = useAgentsViewRouting({ ++ routeAgentId, ++ agents, ++ agentsLoaded, ++ validatedSelectedAgentId, ++ }); ++ + const [createOpen, setCreateOpen] = useState(false); + const [requestedCreateType, setRequestedCreateType] = + useState(null); +`; + +function DiffPreview() { + const parsed = useMemo(() => parseDiff(SAMPLE_DIFF), []); + const file = parsed[0]; + + const tokens = useMemo(() => { + if (!file) return undefined; + const enhancers = [markEdits(file.hunks)]; + try { + return tokenize(file.hunks, { + highlight: true, + refractor: refractorAdapter, + language: "tsx", + enhancers, + }); + } catch { + return undefined; + } + }, [file]); + + if (!file) return null; + + return ( +
+
+ + src/components/app/agents-view.tsx + + +22 + -18 +
+ + {(hunks) => + hunks.map((hunk) => ) + } + +
+ ); +} function ThemePicker({ theme, @@ -47,28 +172,9 @@ function ThemePicker({ export function DesignLab() { const { theme, setTheme } = useTheme(); - const [cwd, setCwd] = useState("~/dev/apps/dispatch"); - - const projectHistory = [ - "~/dev/apps/dispatch", - "~/dev/apps/dispatch-ios", - "~/dev/tools/codex", - "~/work/customer-portal", - "~/experiments/agent-sandbox", - ]; - - useEffect(() => { - const style = document.createElement("style"); - style.textContent = - "html, body, #root { overflow: auto !important; height: auto !important; }"; - document.head.appendChild(style); - return () => { - style.remove(); - }; - }, []); return ( -
+

@@ -84,70 +190,54 @@ export function DesignLab() {

-
-
-
-

- Working Directory Input -

-

- Shared input used by agents, templates, and jobs. -

+
+
+

Diff View

+

+ Unified diff with syntax highlighting and intra-line edits. Switch + themes above to compare diff colors. +

+
+ +
+
+ --diff-add +
+
- - undefined} - data-testid="design-lab-path-input" - historyItemTestId="design-lab-path-option" - /> -
- -
-

- Interaction Targets -

-
-
    -
  • - Tab accepts the inline completion without adding a slash. -
  • -
  • Arrow up/down moves through project rows.
  • -
  • Enter or Space selects the highlighted project.
  • -
  • - Frequently used projects sort above recent-but-rare ones. -
  • -
  • Known projects can render as compact names with icons.
  • -
+
+ --diff-delete +
+
-
-
+
+ --status-working +
+
+
+ --status-blocked +
+
+
+
); diff --git a/apps/web/src/hooks/use-theme.ts b/apps/web/src/hooks/use-theme.ts index 3818a038..df5085a2 100644 --- a/apps/web/src/hooks/use-theme.ts +++ b/apps/web/src/hooks/use-theme.ts @@ -396,6 +396,8 @@ function applyTheme(themeId: ThemeId): void { } else { root.setAttribute("data-theme", themeId); } + const mode = THEMES.find((t) => t.id === themeId)?.mode ?? "dark"; + root.setAttribute("data-theme-mode", mode); // The pre-paint script in index.html sets inline body bg/color and the // theme-color meta to avoid FOUC. After data-theme changes we re-derive // those from the live CSS tokens so the shell tracks the selected theme diff --git a/apps/web/src/index.css b/apps/web/src/index.css index a4b6056b..b4373a89 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -40,6 +40,12 @@ --chart-5: 158 64% 42%; /* deep teal — model bars */ --chart-6: 45 96% 54%; /* deep amber — project bars */ + /* Diff colors — GitHub Dark */ + --diff-bg: 215 15% 16%; + --diff-fg: 210 67% 96%; + --diff-add: 137 40% 30%; + --diff-delete: 2 50% 40%; + /* Surface & terminal */ --surface: 50 15% 5%; --log-stream-bg: 48 10% 10%; @@ -201,6 +207,12 @@ --chart-5: 217 80% 45%; --chart-6: 142 60% 30%; + /* Diff colors — GitHub Light */ + --diff-bg: 0 0% 100%; + --diff-fg: 216 14% 14%; + --diff-add: 137 50% 55%; + --diff-delete: 0 80% 60%; + --surface: 207 22% 90%; --log-stream-bg: 210 29% 97%; --log-stream-foreground: 153 63% 26%; @@ -401,6 +413,12 @@ --chart-5: 205 70% 38%; --chart-6: 68 80% 25%; + /* Diff colors — GitHub Light */ + --diff-bg: 0 0% 100%; + --diff-fg: 216 14% 14%; + --diff-add: 137 50% 55%; + --diff-delete: 0 80% 60%; + --surface: 46 42% 88%; --log-stream-bg: 46 42% 88%; --log-stream-foreground: 196 13% 35%; @@ -481,6 +499,12 @@ --chart-5: 217 80% 30%; --chart-6: 30 80% 28%; + /* Diff colors — GitHub Light */ + --diff-bg: 0 0% 100%; + --diff-fg: 216 14% 14%; + --diff-add: 137 50% 55%; + --diff-delete: 0 80% 60%; + --surface: 220 14% 96%; --log-stream-bg: 0 0% 100%; --log-stream-foreground: 153 80% 22%; @@ -1162,21 +1186,25 @@ html[data-hidden] .animate-sidebar-nav-pulse { animation-duration: 300ms !important; } -/* react-diff-view dark theme overrides */ +/* react-diff-view theme overrides */ .changes-diff-view .diff { font-family: var(--font-terminal); font-size: 12px; line-height: 1.5; - color: hsl(var(--foreground)); + background: hsl(var(--diff-bg)); + color: hsl(var(--diff-fg)); border: none; + table-layout: auto; + width: max-content; + min-width: 100%; } .changes-diff-view .diff-gutter-col { width: 50px; } .changes-diff-view .diff-gutter { - color: hsl(var(--muted-foreground)); - background: hsl(var(--muted) / 0.3); - border-right: 1px solid hsl(var(--border) / 0.4); + color: hsl(var(--diff-fg) / 0.5); + background: hsl(var(--diff-bg)); + border-right: 1px solid hsl(var(--diff-fg) / 0.1); padding: 0 8px; text-align: right; cursor: default; @@ -1188,78 +1216,155 @@ html[data-hidden] .animate-sidebar-nav-pulse { white-space: pre; } .changes-diff-view .diff-code-insert { - background: hsl(158 64% 52% / 0.08); + background: hsl(var(--diff-add) / 0.22); } .changes-diff-view .diff-code-delete { - background: hsl(0 84% 60% / 0.08); + background: hsl(var(--diff-delete) / 0.22); +} +.changes-diff-view .diff-code-insert .diff-code-edit { + background: hsl(var(--diff-add) / 0.5); + border-radius: 2px; + padding: 1px 0; +} +.changes-diff-view .diff-code-delete .diff-code-edit { + background: hsl(var(--diff-delete) / 0.5); + border-radius: 2px; + padding: 1px 0; } .changes-diff-view .diff-gutter-insert { - background: hsl(158 64% 52% / 0.12); - color: hsl(var(--status-working)); + background: hsl(var(--diff-add) / 0.28); + color: hsl(var(--diff-fg)); } .changes-diff-view .diff-gutter-delete { - background: hsl(0 84% 60% / 0.12); - color: hsl(var(--status-blocked)); + background: hsl(var(--diff-delete) / 0.28); + color: hsl(var(--diff-fg)); } .changes-diff-view .diff-hunk-header { - background: hsl(var(--muted) / 0.5); - color: hsl(var(--muted-foreground)); + background: hsl(var(--diff-fg) / 0.08); + color: hsl(var(--diff-fg) / 0.5); } .changes-diff-view .diff-hunk-header-gutter { - background: hsl(var(--muted) / 0.5); + background: hsl(var(--diff-fg) / 0.08); } .changes-diff-view .diff-hunk-header-content { padding: 4px 12px; } -/* Syntax highlighting tokens (refractor/Prism) */ +/* Syntax highlighting — dark mode (GitHub Dark) */ .changes-diff-view .token.comment, .changes-diff-view .token.prolog, .changes-diff-view .token.doctype, .changes-diff-view .token.cdata { - color: #6a737d; + color: #8b949e; } .changes-diff-view .token.punctuation { - color: #b3b9c5; + color: #f0f6fc; +} +.changes-diff-view .token.tag { + color: #f0f6fc; } .changes-diff-view .token.property, -.changes-diff-view .token.tag, .changes-diff-view .token.constant, .changes-diff-view .token.symbol, .changes-diff-view .token.deleted { - color: #f97583; + color: #79c0ff; } .changes-diff-view .token.boolean, .changes-diff-view .token.number { - color: #79b8ff; + color: #79c0ff; } -.changes-diff-view .token.selector, -.changes-diff-view .token.attr-name, .changes-diff-view .token.string, .changes-diff-view .token.char, -.changes-diff-view .token.builtin, .changes-diff-view .token.inserted { - color: #9ecbff; + color: #a5d6ff; +} +.changes-diff-view .token.selector, +.changes-diff-view .token.attr-name, +.changes-diff-view .token.builtin { + color: #ffa657; } .changes-diff-view .token.operator, .changes-diff-view .token.entity, .changes-diff-view .token.url { - color: #b3b9c5; + color: #79c0ff; } .changes-diff-view .token.atrule, .changes-diff-view .token.attr-value, .changes-diff-view .token.keyword { - color: #f97583; + color: #ff7b72; +} +.changes-diff-view .token.function { + color: #d2a8ff; } -.changes-diff-view .token.function, .changes-diff-view .token.class-name { - color: #b392f0; + color: #ffa657; } .changes-diff-view .token.regex, .changes-diff-view .token.important, .changes-diff-view .token.variable { - color: #ffab70; + color: #ffa657; } .changes-diff-view .token.template-string .token.interpolation { - color: #ffab70; + color: #ff7b72; +} + +/* Syntax highlighting — light mode (GitHub Light) */ +[data-theme-mode="light"] .changes-diff-view .token.comment, +[data-theme-mode="light"] .changes-diff-view .token.prolog, +[data-theme-mode="light"] .changes-diff-view .token.doctype, +[data-theme-mode="light"] .changes-diff-view .token.cdata { + color: #59636e; +} +[data-theme-mode="light"] .changes-diff-view .token.punctuation { + color: #1f2328; +} +[data-theme-mode="light"] .changes-diff-view .token.tag { + color: #1f2328; +} +[data-theme-mode="light"] .changes-diff-view .token.property, +[data-theme-mode="light"] .changes-diff-view .token.constant, +[data-theme-mode="light"] .changes-diff-view .token.symbol, +[data-theme-mode="light"] .changes-diff-view .token.deleted { + color: #0550ae; +} +[data-theme-mode="light"] .changes-diff-view .token.boolean, +[data-theme-mode="light"] .changes-diff-view .token.number { + color: #0550ae; +} +[data-theme-mode="light"] .changes-diff-view .token.string, +[data-theme-mode="light"] .changes-diff-view .token.char, +[data-theme-mode="light"] .changes-diff-view .token.inserted { + color: #0a3069; +} +[data-theme-mode="light"] .changes-diff-view .token.selector, +[data-theme-mode="light"] .changes-diff-view .token.attr-name, +[data-theme-mode="light"] .changes-diff-view .token.builtin { + color: #953800; +} +[data-theme-mode="light"] .changes-diff-view .token.operator, +[data-theme-mode="light"] .changes-diff-view .token.entity, +[data-theme-mode="light"] .changes-diff-view .token.url { + color: #0550ae; +} +[data-theme-mode="light"] .changes-diff-view .token.atrule, +[data-theme-mode="light"] .changes-diff-view .token.attr-value, +[data-theme-mode="light"] .changes-diff-view .token.keyword { + color: #cf222e; +} +[data-theme-mode="light"] .changes-diff-view .token.function { + color: #6639ba; +} +[data-theme-mode="light"] .changes-diff-view .token.class-name { + color: #953800; +} +[data-theme-mode="light"] .changes-diff-view .token.regex, +[data-theme-mode="light"] .changes-diff-view .token.important, +[data-theme-mode="light"] .changes-diff-view .token.variable { + color: #953800; +} +[data-theme-mode="light"] + .changes-diff-view + .token.template-string + .token.interpolation { + color: #cf222e; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a293c782..6d489746 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -152,6 +152,9 @@ importers: date-fns: specifier: ^4.1.0 version: 4.1.0 + diff-match-patch: + specifier: ^1.0.5 + version: 1.0.5 framer-motion: specifier: ^12.35.2 version: 12.38.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -219,6 +222,9 @@ importers: "@testing-library/react": specifier: ^16.3.2 version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + "@types/diff-match-patch": + specifier: ^1.0.36 + version: 1.0.36 "@types/react": specifier: ^18.3.12 version: 18.3.28 @@ -3574,6 +3580,12 @@ packages: integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==, } + "@types/diff-match-patch@1.0.36": + resolution: + { + integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==, + } + "@types/estree-jsx@1.0.5": resolution: { @@ -11855,6 +11867,8 @@ snapshots: "@types/deep-eql@4.0.2": {} + "@types/diff-match-patch@1.0.36": {} + "@types/estree-jsx@1.0.5": dependencies: "@types/estree": 1.0.8