Skip to content

Commit 5180d2b

Browse files
BunsDevclaude
andauthored
Add dedicated file view route and navigation (#124)
* Add dedicated file view page and fix file path overflow Replace the floating CodeViewerPanel with a full-page /file-view route for viewing files, matching the PR Review and Merge Conflicts page pattern. Fix file path text overflow in the Focused file context section and make the context cards clickable. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Update Bun lockfile format - Remove obsolete `configVersion` entry from `bun.lock` * Bump linting and task runner dependencies - Update oxlint and turbo versions - Refresh lockfile and generated route types --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 1f79041 commit 5180d2b

12 files changed

Lines changed: 288 additions & 24 deletions

File tree

apps/web/src/components/ChatMarkdown.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ import type { Components } from "react-markdown";
1717
import ReactMarkdown from "react-markdown";
1818
import remarkGfm from "remark-gfm";
1919
import { useAppSettings } from "../appSettings";
20-
import { useCodeViewerStore } from "../codeViewerStore";
2120
import { openFileReference } from "../fileOpen";
21+
import { useFileViewNavigation } from "~/hooks/useFileViewNavigation";
2222
import { resolveDiffThemeName, type DiffThemeName } from "../lib/diffRendering";
2323
import { fnv1a32 } from "../lib/diffRendering";
2424
import { LRUCache } from "../lib/lruCache";
@@ -240,7 +240,7 @@ function SuspenseShikiCodeBlock({
240240

241241
function ChatMarkdown({ text, cwd, isStreaming = false }: ChatMarkdownProps) {
242242
const { settings } = useAppSettings();
243-
const openFileInViewer = useCodeViewerStore((state) => state.openFile);
243+
const openFileInViewer = useFileViewNavigation();
244244
const { resolvedTheme } = useTheme();
245245
const diffThemeName = resolveDiffThemeName(resolvedTheme);
246246
const openLinksExternally = settings.openLinksExternally;

apps/web/src/components/CodeViewerPanel.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { MarkdownPreview } from "./MarkdownPreview";
1313
import { isElectron } from "~/env";
1414
import { Button } from "./ui/button";
1515

16-
function CodeViewerTabStrip(props: {
16+
export function CodeViewerTabStrip(props: {
1717
tabs: CodeViewerTab[];
1818
activeTabPath: string | null;
1919
onSelectTab: (relativePath: string) => void;
@@ -60,7 +60,7 @@ function CodeViewerTabStrip(props: {
6060
);
6161
}
6262

63-
const CodeViewerFileContent = memo(function CodeViewerFileContent(props: {
63+
export const CodeViewerFileContent = memo(function CodeViewerFileContent(props: {
6464
cwd: string;
6565
relativePath: string;
6666
resolvedTheme: "light" | "dark";

apps/web/src/components/DiffPanel.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
88
import { gitBranchesQueryOptions } from "~/lib/gitReactQuery";
99
import { checkpointDiffQueryOptions } from "~/lib/providerReactQuery";
1010
import { cn } from "~/lib/utils";
11-
import { useCodeViewerStore } from "../codeViewerStore";
11+
import { useFileViewNavigation } from "~/hooks/useFileViewNavigation";
1212
import { parseDiffRouteSearch, stripDiffSearchParams } from "../diffRouteSearch";
1313
import { useTheme } from "../hooks/useTheme";
1414
import { buildPatchCacheKey } from "../lib/diffRendering";
@@ -474,7 +474,7 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
474474
target?.scrollIntoView({ block: "nearest" });
475475
}, [selectedFilePath, renderableFiles]);
476476

477-
const openFileInCodeViewer = useCodeViewerStore((state) => state.openFile);
477+
const openFileInCodeViewer = useFileViewNavigation();
478478
const openDiffFileInCodeViewer = useCallback(
479479
(filePath: string) => {
480480
if (!activeCwd) return;

apps/web/src/components/Sidebar.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
ChevronRightIcon,
55
EyeIcon,
66
EyeOffIcon,
7+
FileCodeIcon,
78
FolderIcon,
89
GitMergeIcon,
910
GitPullRequestIcon,
@@ -375,7 +376,10 @@ export default function Sidebar() {
375376
const navigate = useNavigate();
376377
const pathname = useLocation({ select: (loc) => loc.pathname });
377378
const isOnSubPage =
378-
pathname === "/settings" || pathname === "/pr-review" || pathname === "/merge-conflicts";
379+
pathname === "/settings" ||
380+
pathname === "/pr-review" ||
381+
pathname === "/merge-conflicts" ||
382+
pathname === "/file-view";
379383
const { settings: appSettings, updateSettings } = useAppSettings();
380384
const { resolvedTheme } = useTheme();
381385
const { handleNewThread } = useHandleNewThread();
@@ -2126,6 +2130,16 @@ export default function Sidebar() {
21262130
<span className="text-xs">Merge Conflicts</span>
21272131
</SidebarMenuButton>
21282132
</SidebarMenuItem>
2133+
<SidebarMenuItem>
2134+
<SidebarMenuButton
2135+
size="sm"
2136+
className="gap-2 px-2 py-1.5 text-muted-foreground/70 hover:bg-accent hover:text-foreground"
2137+
onClick={() => void navigate({ to: "/file-view" })}
2138+
>
2139+
<FileCodeIcon className="size-3.5" />
2140+
<span className="text-xs">File View</span>
2141+
</SidebarMenuButton>
2142+
</SidebarMenuItem>
21292143
<SidebarMenuItem>
21302144
<SidebarMenuButton
21312145
size="sm"

apps/web/src/components/WorkspaceFileTree.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import {
99
TriangleAlertIcon,
1010
} from "lucide-react";
1111
import { memo, useCallback, useDeferredValue, useState } from "react";
12-
import { useCodeViewerStore } from "~/codeViewerStore";
1312
import { openInPreferredEditor } from "~/editorPreferences";
13+
import { useFileViewNavigation } from "~/hooks/useFileViewNavigation";
1414
import {
1515
projectListDirectoryQueryOptions,
1616
projectSearchEntriesQueryOptions,
@@ -48,7 +48,7 @@ export const WorkspaceFileTree = memo(function WorkspaceFileTree(props: {
4848
}));
4949
}, []);
5050

51-
const openFileInViewer = useCodeViewerStore((state) => state.openFile);
51+
const openFileInViewer = useFileViewNavigation();
5252
const filtersHaveContent = includePattern.trim().length > 0 || excludePattern.trim().length > 0;
5353
const filtersVisible = filtersOpen || filtersHaveContent;
5454

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { useNavigate } from "@tanstack/react-router";
2+
import { FileCodeIcon, XIcon } from "lucide-react";
3+
import { useCallback, useEffect } from "react";
4+
5+
import { useCodeViewerStore } from "~/codeViewerStore";
6+
import { useTheme } from "~/hooks/useTheme";
7+
import { isMacPlatform } from "~/lib/utils";
8+
import type { CodeContextSelection } from "../CodeMirrorViewer";
9+
import { CodeViewerFileContent, CodeViewerTabStrip } from "../CodeViewerPanel";
10+
import { Button } from "../ui/button";
11+
12+
export function FileViewShell(props: { initialCwd: string; initialPath: string | null }) {
13+
const { resolvedTheme } = useTheme();
14+
const navigate = useNavigate();
15+
16+
const tabs = useCodeViewerStore((state) => state.tabs);
17+
const activeTabPath = useCodeViewerStore((state) => state.activeTabPath);
18+
const setActiveTab = useCodeViewerStore((state) => state.setActiveTab);
19+
const closeTab = useCodeViewerStore((state) => state.closeTab);
20+
const closeAllTabs = useCodeViewerStore((state) => state.closeAllTabs);
21+
const openFile = useCodeViewerStore((state) => state.openFile);
22+
const setPendingContext = useCodeViewerStore((state) => state.setPendingContext);
23+
24+
// On mount, ensure the initial file is open as a tab
25+
useEffect(() => {
26+
if (props.initialPath) {
27+
openFile(props.initialCwd, props.initialPath);
28+
}
29+
// Only run on mount
30+
// eslint-disable-next-line react-hooks/exhaustive-deps
31+
}, []);
32+
33+
const activeTab = tabs.find((tab) => tab.relativePath === activeTabPath);
34+
35+
const onSelectTab = useCallback(
36+
(relativePath: string) => {
37+
setActiveTab(relativePath);
38+
const tab = tabs.find((t) => t.relativePath === relativePath);
39+
if (tab) {
40+
void navigate({
41+
to: "/file-view",
42+
search: { cwd: tab.cwd, path: relativePath },
43+
replace: true,
44+
});
45+
}
46+
},
47+
[setActiveTab, tabs, navigate],
48+
);
49+
50+
const onCloseTab = useCallback(
51+
(relativePath: string) => {
52+
closeTab(relativePath);
53+
// Check if this was the last tab (after closing, store will have tabs.length - 1)
54+
if (tabs.length <= 1) {
55+
void navigate({ to: "/" });
56+
}
57+
},
58+
[closeTab, tabs.length, navigate],
59+
);
60+
61+
const onCloseAll = useCallback(() => {
62+
closeAllTabs();
63+
void navigate({ to: "/" });
64+
}, [closeAllTabs, navigate]);
65+
66+
const onAddContext = useCallback(
67+
(ctx: CodeContextSelection) => {
68+
setPendingContext({
69+
filePath: ctx.filePath,
70+
fromLine: ctx.fromLine,
71+
toLine: ctx.toLine,
72+
});
73+
},
74+
[setPendingContext],
75+
);
76+
77+
const modKey = isMacPlatform(navigator.platform) ? "\u2318" : "Ctrl+";
78+
79+
return (
80+
<div className="flex h-full w-full flex-col">
81+
{/* Tab bar */}
82+
<div className="flex items-center justify-between gap-2 border-b border-border px-4 h-12">
83+
<CodeViewerTabStrip
84+
tabs={tabs}
85+
activeTabPath={activeTabPath}
86+
onSelectTab={onSelectTab}
87+
onCloseTab={onCloseTab}
88+
onCloseAll={onCloseAll}
89+
/>
90+
<div className="flex shrink-0 items-center gap-2">
91+
<span className="hidden text-[10px] text-muted-foreground/50 sm:inline">
92+
Select code + {modKey}L to add context
93+
</span>
94+
<Button size="icon-xs" variant="ghost" onClick={onCloseAll} aria-label="Close all tabs">
95+
<XIcon className="size-4" />
96+
</Button>
97+
</div>
98+
</div>
99+
100+
{/* Content */}
101+
<div className="flex min-h-0 flex-1 justify-center overflow-y-auto">
102+
<div className="h-full w-full max-w-5xl">
103+
{!activeTab ? (
104+
<div className="flex h-full flex-col items-center justify-center gap-2 px-5 text-center text-muted-foreground/60">
105+
<FileCodeIcon className="size-8 opacity-40" />
106+
<p className="text-xs">Click a file in the sidebar to view it here.</p>
107+
</div>
108+
) : (
109+
<CodeViewerFileContent
110+
key={activeTab.relativePath}
111+
cwd={activeTab.cwd}
112+
relativePath={activeTab.relativePath}
113+
resolvedTheme={resolvedTheme}
114+
onAddContext={onAddContext}
115+
/>
116+
)}
117+
</div>
118+
</div>
119+
</div>
120+
);
121+
}

apps/web/src/components/merge-conflicts/MergeConflictShell.tsx

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { GitResolvedPullRequest, PrConflictCandidateResolution } from "@okcode/contracts";
22
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
33
import { useDebouncedValue } from "@tanstack/react-pacer";
4+
import { useNavigate } from "@tanstack/react-router";
45
import { Schema } from "effect";
56
import {
67
AlertTriangleIcon,
@@ -242,6 +243,7 @@ function MergeConflictGuidanceRail({
242243
status: "done" | "active" | "todo" | "blocked";
243244
}>;
244245
}) {
246+
const navigateToFileView = useNavigate();
245247
return (
246248
<div className="flex min-h-0 min-w-0 flex-col bg-background/96">
247249
<div className="border-b border-border/70 px-4 py-4">
@@ -346,15 +348,51 @@ function MergeConflictGuidanceRail({
346348
title="Focused file context"
347349
/>
348350
<div className="mt-4 space-y-3 text-sm">
349-
<div className="rounded-2xl border border-border/70 bg-muted/24 p-3">
351+
<div
352+
className="cursor-pointer rounded-2xl border border-border/70 bg-muted/24 p-3 transition-colors hover:border-border hover:bg-muted/40"
353+
role="button"
354+
tabIndex={0}
355+
onClick={() =>
356+
void navigateToFileView({
357+
to: "/file-view",
358+
search: { cwd: project.cwd },
359+
})
360+
}
361+
onKeyDown={(e) => {
362+
if (e.key === "Enter" || e.key === " ") {
363+
void navigateToFileView({
364+
to: "/file-view",
365+
search: { cwd: project.cwd },
366+
});
367+
}
368+
}}
369+
>
350370
<p className="font-medium text-foreground">{projectLabel(project)}</p>
351-
<p className="mt-1 text-muted-foreground">{project.cwd}</p>
371+
<p className="mt-1 break-all text-muted-foreground">{project.cwd}</p>
352372
</div>
353-
<div className="rounded-2xl border border-border/70 bg-muted/24 p-3">
373+
<div
374+
className="cursor-pointer rounded-2xl border border-border/70 bg-muted/24 p-3 transition-colors hover:border-border hover:bg-muted/40"
375+
role="button"
376+
tabIndex={0}
377+
onClick={() =>
378+
void navigateToFileView({
379+
to: "/file-view",
380+
search: { cwd: preparedWorkspace?.cwd ?? project.cwd },
381+
})
382+
}
383+
onKeyDown={(e) => {
384+
if (e.key === "Enter" || e.key === " ") {
385+
void navigateToFileView({
386+
to: "/file-view",
387+
search: { cwd: preparedWorkspace?.cwd ?? project.cwd },
388+
});
389+
}
390+
}}
391+
>
354392
<p className="font-medium text-foreground">
355393
{workspaceModeLabel(preparedWorkspace)}
356394
</p>
357-
<p className="mt-1 text-muted-foreground">
395+
<p className="mt-1 break-all text-muted-foreground">
358396
{preparedWorkspace?.cwd ?? project.cwd}
359397
</p>
360398
</div>

apps/web/src/components/pr-review/PrWorkspace.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { resolveDiffThemeName } from "~/lib/diffRendering";
1515
import { cn } from "~/lib/utils";
1616
import { ensureNativeApi } from "~/nativeApi";
1717
import { Button } from "~/components/ui/button";
18-
import { useCodeViewerStore } from "~/codeViewerStore";
18+
import { useFileViewNavigation } from "~/hooks/useFileViewNavigation";
1919
import type { Project } from "~/types";
2020
import { PrFileCommentComposer } from "./PrFileCommentComposer";
2121
import { PrFileTabStrip } from "./PrFileTabStrip";
@@ -51,7 +51,7 @@ export function PrWorkspace({
5151
onCreateThread: (input: { path: string; line: number; body: string }) => Promise<void>;
5252
}) {
5353
const { resolvedTheme } = useTheme();
54-
const openFileInCodeViewer = useCodeViewerStore((state) => state.openFile);
54+
const openFileInCodeViewer = useFileViewNavigation();
5555
const [fileViewMode, setFileViewMode] = useLocalStorage(
5656
"okcode:pr-review:file-view-mode",
5757
"single",
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { useNavigate } from "@tanstack/react-router";
2+
import { useCallback } from "react";
3+
import { useCodeViewerStore } from "~/codeViewerStore";
4+
5+
export function useFileViewNavigation() {
6+
const navigate = useNavigate();
7+
const openFile = useCodeViewerStore((s) => s.openFile);
8+
9+
return useCallback(
10+
(cwd: string, relativePath: string) => {
11+
openFile(cwd, relativePath);
12+
void navigate({
13+
to: "/file-view",
14+
search: { cwd, path: relativePath },
15+
});
16+
},
17+
[navigate, openFile],
18+
);
19+
}

0 commit comments

Comments
 (0)