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
14 changes: 1 addition & 13 deletions desktop/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"io"
"log/slog"
"os"
"os/exec"
"path/filepath"
goruntime "runtime"
"sort"
Expand Down Expand Up @@ -2070,18 +2069,7 @@ func (a *App) RevealWorkspacePath(rel string) error {
if err != nil || !ok {
return os.ErrInvalid
}
switch goruntime.GOOS {
case "darwin":
return exec.Command("open", "-R", path).Start()
case "windows":
return exec.Command("explorer", "/select,", path).Start()
default:
dir := path
if info, err := os.Stat(path); err == nil && !info.IsDir() {
dir = filepath.Dir(path)
}
return exec.Command("xdg-open", dir).Start()
}
return revealWorkspacePath(goruntime.GOOS, path)
}

func (a *App) notice(text string) {
Expand Down
1 change: 1 addition & 0 deletions desktop/frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1036,6 +1036,7 @@ export default function App() {
onToggleMaximized={() => setWorkspacePanelMaximized((value) => !value)}
onPreviewModeChange={handleWorkspacePreviewModeChange}
onAddToChat={addToChat}
onNotice={notice}
changesRefreshKey={workspaceChangesRefreshKey}
/>
</div>
Expand Down
48 changes: 46 additions & 2 deletions desktop/frontend/src/components/WorkspacePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
Columns2,
FileText,
Folder,
FolderSearch,
GitBranch,
Maximize2,
MessageSquarePlus,
Expand All @@ -37,7 +38,8 @@ const WORKSPACE_TREE_MIN_WIDTH = 220;
const WORKSPACE_TREE_DEFAULT_WIDTH = WORKSPACE_TREE_MIN_WIDTH;
const WORKSPACE_TREE_MAX_WIDTH = 420;
const WORKSPACE_PREVIEW_MIN_WIDTH = 420;
const WORKSPACE_CONTEXT_MENU_FILE_HEIGHT = 92;
const WORKSPACE_CONTEXT_MENU_FILE_HEIGHT = 136;
const WORKSPACE_CONTEXT_MENU_FOLDER_HEIGHT = 92;
const WORKSPACE_CONTEXT_MENU_REF_HEIGHT = 48;

function clampWorkspaceTreeWidth(width: number, panelWidth?: number): number {
Expand Down Expand Up @@ -135,6 +137,12 @@ function formatBytes(n: number): string {
return `${n} B`;
}

function revealMenuKey(platform: string): "workspace.revealInFinder" | "workspace.revealInExplorer" | "workspace.revealInFileManager" {
if (platform === "darwin") return "workspace.revealInFinder";
if (platform === "windows") return "workspace.revealInExplorer";
return "workspace.revealInFileManager";
}

function isDeletedChange(row: WorkspaceChangeView): boolean {
return !!row.gitStatus && row.gitStatus.includes("D");
}
Expand All @@ -155,6 +163,7 @@ export function WorkspacePanel({
onToggleMaximized,
onPreviewModeChange,
onAddToChat,
onNotice,
changesRefreshKey,
}: {
open: boolean;
Expand All @@ -165,6 +174,7 @@ export function WorkspacePanel({
onToggleMaximized: () => void;
onPreviewModeChange?: (active: boolean) => void;
onAddToChat?: (text: string) => void;
onNotice?: (text: string, level?: "info" | "warn") => void;
changesRefreshKey?: number;
}) {
const t = useT();
Expand All @@ -187,12 +197,28 @@ export function WorkspacePanel({
const [treeVisible, setTreeVisible] = useState(true);
const [treeWidth, setTreeWidth] = useState(loadWorkspaceTreeWidth);
const [treeResizing, setTreeResizing] = useState(false);
const [platform, setPlatform] = useState("");

const loadDir = useCallback(async (dir: string) => {
const entries = await app.ListDir(dir).catch(() => []);
setEntriesByDir((prev) => ({ ...prev, [dir]: entries ?? [] }));
}, []);

useEffect(() => {
let live = true;
app
.Platform()
.then((next) => {
if (live) setPlatform(next);
})
.catch(() => {
if (live) setPlatform("");
});
return () => {
live = false;
};
}, []);

const loadChanges = useCallback(async () => {
const requestId = changesRequestRef.current + 1;
changesRequestRef.current = requestId;
Expand Down Expand Up @@ -474,6 +500,7 @@ export function WorkspacePanel({
const openTreeMenu = (event: ReactMouseEvent<HTMLElement>, path: string, isDir: boolean) => {
event.preventDefault();
event.stopPropagation();
window.getSelection()?.removeAllRanges();
setSelectionMenu(null);
setTreeMenu({ x: event.clientX, y: event.clientY, path, isDir });
};
Expand Down Expand Up @@ -508,6 +535,18 @@ export function WorkspacePanel({
}
};

const revealTreePath = async () => {
if (!treeMenu) return;
const target = treeMenu.path;
setTreeMenu(null);
try {
await app.RevealWorkspacePath(target);
} catch (err) {
const detail = err instanceof Error ? err.message : String(err);
onNotice?.(t("workspace.revealFailed", { path: target, err: detail }), "warn");
}
};

const renderChangedRows = () => {
if (loadingChanges) return <div className="workspace-empty">{t("workspace.loadingChanges")}</div>;
if (!changes) return null;
Expand Down Expand Up @@ -819,7 +858,7 @@ export function WorkspacePanel({
<FloatingMenu
x={treeMenu.x}
y={treeMenu.y}
estimatedHeight={treeMenu.isDir ? WORKSPACE_CONTEXT_MENU_REF_HEIGHT : WORKSPACE_CONTEXT_MENU_FILE_HEIGHT}
estimatedHeight={treeMenu.isDir ? WORKSPACE_CONTEXT_MENU_FOLDER_HEIGHT : WORKSPACE_CONTEXT_MENU_FILE_HEIGHT}
className="workspace-tree-menu"
>
<FloatingMenuItems
Expand All @@ -829,6 +868,11 @@ export function WorkspacePanel({
label: treeMenu.isDir ? t("workspace.addFolderReferenceToChat") : t("workspace.addFileReferenceToChat"),
onSelect: addTreeReferenceToChat,
},
{
icon: <FolderSearch size={14} />,
label: t(revealMenuKey(platform)),
onSelect: () => void revealTreePath(),
},
...(treeMenu.isDir
? []
: [
Expand Down
4 changes: 4 additions & 0 deletions desktop/frontend/src/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ export const en = {
"workspace.addFileReferenceToChat": "Add file reference",
"workspace.addFolderReferenceToChat": "Add folder reference",
"workspace.addFileContentToChat": "Add file contents",
"workspace.revealInFinder": "Reveal in Finder",
"workspace.revealInExplorer": "Show in File Explorer",
"workspace.revealInFileManager": "Show in file manager",
"workspace.revealFailed": "Could not show {path}: {err}",
"workspace.viewMode": "Workspace view",
"workspace.filesTab": "Files",
"workspace.changedTab": "Changed",
Expand Down
4 changes: 4 additions & 0 deletions desktop/frontend/src/locales/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ export const zh: Record<DictKey, string> = {
"workspace.addFileReferenceToChat": "添加文件引用",
"workspace.addFolderReferenceToChat": "添加文件夹引用",
"workspace.addFileContentToChat": "添加文件内容",
"workspace.revealInFinder": "在 Finder 中显示",
"workspace.revealInExplorer": "在资源管理器中显示",
"workspace.revealInFileManager": "在文件管理器中显示",
"workspace.revealFailed": "无法显示 {path}:{err}",
"workspace.viewMode": "工作区视图",
"workspace.filesTab": "文件",
"workspace.changedTab": "改动",
Expand Down
28 changes: 28 additions & 0 deletions desktop/reveal_workspace.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package main

import (
"os"
"os/exec"
"path/filepath"
)

func revealWorkspaceCommand(goos string, path string, isDir bool) *exec.Cmd {
switch goos {
case "darwin":
return exec.Command("open", "-R", path)
case "windows":
return exec.Command("explorer", "/select,"+path)
default:
target := path
if !isDir {
target = filepath.Dir(path)
}
return exec.Command("xdg-open", target)
}
}

func revealWorkspacePath(goos string, path string) error {
info, err := os.Stat(path)
isDir := err == nil && info.IsDir()
return revealWorkspaceCommand(goos, path, isDir).Start()
}
54 changes: 54 additions & 0 deletions desktop/workspace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"os"
"os/exec"
"path/filepath"
"reflect"
"strings"
"testing"

Expand Down Expand Up @@ -184,6 +185,59 @@ func TestWindowsOpenWorkspacePathAvoidsCmdShell(t *testing.T) {
}
}

func TestRevealWorkspaceCommandByPlatform(t *testing.T) {
tests := []struct {
name string
goos string
path string
isDir bool
wantName string
wantArgs []string
}{
{
name: "macOS reveals files with Finder selection",
goos: "darwin",
path: "/repo/src/main.go",
wantName: "open",
wantArgs: []string{"open", "-R", "/repo/src/main.go"},
},
{
name: "Windows reveals files with Explorer selection",
goos: "windows",
path: `C:\repo\src\main.go`,
wantName: "explorer",
wantArgs: []string{"explorer", `/select,C:\repo\src\main.go`},
},
{
name: "Linux reveals files by opening parent folder",
goos: "linux",
path: "/repo/src/main.go",
wantName: "xdg-open",
wantArgs: []string{"xdg-open", "/repo/src"},
},
{
name: "Linux opens folders directly",
goos: "linux",
path: "/repo/src",
isDir: true,
wantName: "xdg-open",
wantArgs: []string{"xdg-open", "/repo/src"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := revealWorkspaceCommand(tt.goos, tt.path, tt.isDir)
if filepath.Base(cmd.Path) != tt.wantName {
t.Fatalf("command path = %q, want basename %q", cmd.Path, tt.wantName)
}
if !reflect.DeepEqual(cmd.Args, tt.wantArgs) {
t.Fatalf("command args = %#v, want %#v", cmd.Args, tt.wantArgs)
}
})
}
}

func TestParseGitStatusPorcelainZ(t *testing.T) {
raw := []byte(" M changed.go\x00?? new.txt\x00R renamed.go\x00old.go\x00")
got := parseGitStatusPorcelainZ(raw)
Expand Down
Loading