mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-01 22:48:16 +00:00
fix(app): normalize Windows paths for consistent project matching
On Windows, paths can use either forward slashes (C:/Users/...) or backslashes (C:\Users\...). Deep links from external apps like GitButler use forward slashes, while paths from the file system may use backslashes. This caused duplicate project entries and broken ~ display. - Add normalizePath() to convert backslashes to forward slashes - Normalize paths in project open/close/expand/collapse/move/touch - Fix ~ home directory display by normalizing before comparison - Normalize paths at entry points (openProject, navigateToProject)
This commit is contained in:
@@ -3,7 +3,7 @@ import { batch, createEffect, createMemo, on, onCleanup, onMount, type Accessor
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { useGlobalSync } from "./global-sync"
|
||||
import { useGlobalSDK } from "./global-sdk"
|
||||
import { useServer } from "./server"
|
||||
import { useServer, normalizePath } from "./server"
|
||||
import { Project } from "@opencode-ai/sdk/v2"
|
||||
import { Persist, persisted, removePersisted } from "@/utils/persist"
|
||||
import { same } from "@/utils/same"
|
||||
@@ -415,7 +415,8 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
list,
|
||||
open(directory: string) {
|
||||
const root = rootFor(directory)
|
||||
if (server.projects.list().find((x) => x.worktree === root)) return
|
||||
const normalized = normalizePath(root)
|
||||
if (server.projects.list().find((x) => normalizePath(x.worktree) === normalized)) return
|
||||
globalSync.project.loadSessions(root)
|
||||
server.projects.open(root)
|
||||
},
|
||||
|
||||
@@ -7,6 +7,10 @@ import { Persist, persisted } from "@/utils/persist"
|
||||
|
||||
type StoredProject = { worktree: string; expanded: boolean }
|
||||
|
||||
export function normalizePath(input: string) {
|
||||
return input.replaceAll("\\", "/")
|
||||
}
|
||||
|
||||
export function normalizeServerUrl(input: string) {
|
||||
const trimmed = input.trim()
|
||||
if (!trimmed) return
|
||||
@@ -163,39 +167,44 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
||||
open(directory: string) {
|
||||
const key = origin()
|
||||
if (!key) return
|
||||
const normalized = normalizePath(directory)
|
||||
const current = store.projects[key] ?? []
|
||||
if (current.find((x) => x.worktree === directory)) return
|
||||
setStore("projects", key, [{ worktree: directory, expanded: true }, ...current])
|
||||
if (current.find((x) => normalizePath(x.worktree) === normalized)) return
|
||||
setStore("projects", key, [{ worktree: normalized, expanded: true }, ...current])
|
||||
},
|
||||
close(directory: string) {
|
||||
const key = origin()
|
||||
if (!key) return
|
||||
const normalized = normalizePath(directory)
|
||||
const current = store.projects[key] ?? []
|
||||
setStore(
|
||||
"projects",
|
||||
key,
|
||||
current.filter((x) => x.worktree !== directory),
|
||||
current.filter((x) => normalizePath(x.worktree) !== normalized),
|
||||
)
|
||||
},
|
||||
expand(directory: string) {
|
||||
const key = origin()
|
||||
if (!key) return
|
||||
const normalized = normalizePath(directory)
|
||||
const current = store.projects[key] ?? []
|
||||
const index = current.findIndex((x) => x.worktree === directory)
|
||||
const index = current.findIndex((x) => normalizePath(x.worktree) === normalized)
|
||||
if (index !== -1) setStore("projects", key, index, "expanded", true)
|
||||
},
|
||||
collapse(directory: string) {
|
||||
const key = origin()
|
||||
if (!key) return
|
||||
const normalized = normalizePath(directory)
|
||||
const current = store.projects[key] ?? []
|
||||
const index = current.findIndex((x) => x.worktree === directory)
|
||||
const index = current.findIndex((x) => normalizePath(x.worktree) === normalized)
|
||||
if (index !== -1) setStore("projects", key, index, "expanded", false)
|
||||
},
|
||||
move(directory: string, toIndex: number) {
|
||||
const key = origin()
|
||||
if (!key) return
|
||||
const normalized = normalizePath(directory)
|
||||
const current = store.projects[key] ?? []
|
||||
const fromIndex = current.findIndex((x) => x.worktree === directory)
|
||||
const fromIndex = current.findIndex((x) => normalizePath(x.worktree) === normalized)
|
||||
if (fromIndex === -1 || fromIndex === toIndex) return
|
||||
const result = [...current]
|
||||
const [item] = result.splice(fromIndex, 1)
|
||||
@@ -210,7 +219,8 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
||||
touch(directory: string) {
|
||||
const key = origin()
|
||||
if (!key) return
|
||||
setStore("lastProject", key, directory)
|
||||
const normalized = normalizePath(directory)
|
||||
setStore("lastProject", key, normalized)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { DateTime } from "luxon"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { DialogSelectDirectory } from "@/components/dialog-select-directory"
|
||||
import { DialogSelectServer } from "@/components/dialog-select-server"
|
||||
import { useServer } from "@/context/server"
|
||||
import { useServer, normalizePath } from "@/context/server"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useLanguage } from "@/context/language"
|
||||
|
||||
@@ -23,6 +23,16 @@ export default function Home() {
|
||||
const server = useServer()
|
||||
const language = useLanguage()
|
||||
const homedir = createMemo(() => sync.data.path.home)
|
||||
const displayPath = (path: string) => {
|
||||
const home = homedir()
|
||||
if (!home) return path
|
||||
const normalizedPath = normalizePath(path)
|
||||
const normalizedHome = normalizePath(home)
|
||||
if (normalizedPath.startsWith(normalizedHome)) {
|
||||
return "~" + normalizedPath.slice(normalizedHome.length)
|
||||
}
|
||||
return path
|
||||
}
|
||||
const recent = createMemo(() => {
|
||||
return sync.data.project
|
||||
.toSorted((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created))
|
||||
@@ -97,7 +107,7 @@ export default function Home() {
|
||||
class="text-14-mono text-left justify-between px-3"
|
||||
onClick={() => openProject(project.worktree)}
|
||||
>
|
||||
{project.worktree.replace(homedir(), "~")}
|
||||
{displayPath(project.worktree)}
|
||||
<div class="text-14-regular text-text-weak">
|
||||
{DateTime.fromMillis(project.time.updated ?? project.time.created).toRelative()}
|
||||
</div>
|
||||
|
||||
@@ -71,7 +71,7 @@ import { navStart } from "@/utils/perf"
|
||||
import { DialogSelectDirectory } from "@/components/dialog-select-directory"
|
||||
import { DialogEditProject } from "@/components/dialog-edit-project"
|
||||
import { Titlebar } from "@/components/titlebar"
|
||||
import { useServer } from "@/context/server"
|
||||
import { useServer, normalizePath } from "@/context/server"
|
||||
import { useLanguage, type Locale } from "@/context/language"
|
||||
|
||||
export default function Layout(props: ParentProps) {
|
||||
@@ -1111,13 +1111,14 @@ export default function Layout(props: ParentProps) {
|
||||
|
||||
function navigateToProject(directory: string | undefined) {
|
||||
if (!directory) return
|
||||
const normalized = normalizePath(directory)
|
||||
if (!layout.sidebar.opened()) {
|
||||
setState("hoverSession", undefined)
|
||||
setState("hoverProject", undefined)
|
||||
}
|
||||
server.projects.touch(directory)
|
||||
const lastSession = store.lastSession[directory]
|
||||
navigate(`/${base64Encode(directory)}${lastSession ? `/session/${lastSession}` : ""}`)
|
||||
server.projects.touch(normalized)
|
||||
const lastSession = store.lastSession[normalized]
|
||||
navigate(`/${base64Encode(normalized)}${lastSession ? `/session/${lastSession}` : ""}`)
|
||||
layout.mobileSidebar.hide()
|
||||
}
|
||||
|
||||
@@ -1132,8 +1133,9 @@ export default function Layout(props: ParentProps) {
|
||||
}
|
||||
|
||||
function openProject(directory: string, navigate = true) {
|
||||
layout.projects.open(directory)
|
||||
if (navigate) navigateToProject(directory)
|
||||
const normalized = normalizePath(directory)
|
||||
layout.projects.open(normalized)
|
||||
if (navigate) navigateToProject(normalized)
|
||||
}
|
||||
|
||||
const deepLinkEvent = "opencode:deep-link"
|
||||
@@ -2536,6 +2538,16 @@ export default function Layout(props: ParentProps) {
|
||||
return layout.sidebar.workspaces(project.worktree)()
|
||||
})
|
||||
const homedir = createMemo(() => globalSync.data.path.home)
|
||||
const displayPath = (path: string) => {
|
||||
const home = homedir()
|
||||
if (!home) return path
|
||||
const normalizedPath = normalizePath(path)
|
||||
const normalizedHome = normalizePath(home)
|
||||
if (normalizedPath.startsWith(normalizedHome)) {
|
||||
return "~" + normalizedPath.slice(normalizedHome.length)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -2570,9 +2582,7 @@ export default function Layout(props: ParentProps) {
|
||||
transform: "translate3d(52px, 0, 0)",
|
||||
}}
|
||||
>
|
||||
<span class="text-12-regular text-text-base truncate select-text">
|
||||
{p.worktree.replace(homedir(), "~")}
|
||||
</span>
|
||||
<span class="text-12-regular text-text-base truncate select-text">{displayPath(p.worktree)}</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user