summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/pages
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-02-08 05:02:19 -0600
committerGitHub <[email protected]>2026-02-08 05:02:19 -0600
commitd1ebe0767c264d395c8bc504c0957ccc3af90103 (patch)
tree4e10adf10def7a5ec731d1b33a6f21dbe67769f6 /packages/app/src/pages
parent19b1222cd85060f7b0999b99586e93f84821b94b (diff)
downloadopencode-d1ebe0767c264d395c8bc504c0957ccc3af90103.tar.gz
opencode-d1ebe0767c264d395c8bc504c0957ccc3af90103.zip
chore: refactoring and tests (#12629)
Diffstat (limited to 'packages/app/src/pages')
-rw-r--r--packages/app/src/pages/directory-layout.tsx5
-rw-r--r--packages/app/src/pages/layout/deep-links.ts10
-rw-r--r--packages/app/src/pages/layout/helpers.test.ts29
-rw-r--r--packages/app/src/pages/layout/helpers.ts7
-rw-r--r--packages/app/src/pages/session.tsx10
-rw-r--r--packages/app/src/pages/session/helpers.test.ts12
-rw-r--r--packages/app/src/pages/session/helpers.ts7
7 files changed, 70 insertions, 10 deletions
diff --git a/packages/app/src/pages/directory-layout.tsx b/packages/app/src/pages/directory-layout.tsx
index 2f4db8564..b2a17b96b 100644
--- a/packages/app/src/pages/directory-layout.tsx
+++ b/packages/app/src/pages/directory-layout.tsx
@@ -15,6 +15,7 @@ export default function Layout(props: ParentProps) {
const params = useParams()
const navigate = useNavigate()
const language = useLanguage()
+ let invalid = ""
const directory = createMemo(() => {
return decode64(params.dir) ?? ""
})
@@ -22,12 +23,14 @@ export default function Layout(props: ParentProps) {
createEffect(() => {
if (!params.dir) return
if (directory()) return
+ if (invalid === params.dir) return
+ invalid = params.dir
showToast({
variant: "error",
title: language.t("common.requestFailed"),
description: language.t("directory.error.invalidUrl"),
})
- navigate("/")
+ navigate("/", { replace: true })
})
return (
<Show when={directory()}>
diff --git a/packages/app/src/pages/layout/deep-links.ts b/packages/app/src/pages/layout/deep-links.ts
index 772e6ece6..7bdb002a3 100644
--- a/packages/app/src/pages/layout/deep-links.ts
+++ b/packages/app/src/pages/layout/deep-links.ts
@@ -2,7 +2,15 @@ export const deepLinkEvent = "opencode:deep-link"
export const parseDeepLink = (input: string) => {
if (!input.startsWith("opencode://")) return
- const url = new URL(input)
+ if (typeof URL.canParse === "function" && !URL.canParse(input)) return
+ const url = (() => {
+ try {
+ return new URL(input)
+ } catch {
+ return undefined
+ }
+ })()
+ if (!url) return
if (url.hostname !== "open-project") return
const directory = url.searchParams.get("directory")
if (!directory) return
diff --git a/packages/app/src/pages/layout/helpers.test.ts b/packages/app/src/pages/layout/helpers.test.ts
index 8a8ea78c7..83d8f4748 100644
--- a/packages/app/src/pages/layout/helpers.test.ts
+++ b/packages/app/src/pages/layout/helpers.test.ts
@@ -12,6 +12,27 @@ describe("layout deep links", () => {
expect(parseDeepLink("https://example.com")).toBeUndefined()
})
+ test("ignores malformed deep links safely", () => {
+ expect(() => parseDeepLink("opencode://open-project/%E0%A4%A%")).not.toThrow()
+ expect(parseDeepLink("opencode://open-project/%E0%A4%A%")).toBeUndefined()
+ })
+
+ test("parses links when URL.canParse is unavailable", () => {
+ const original = Object.getOwnPropertyDescriptor(URL, "canParse")
+ Object.defineProperty(URL, "canParse", { configurable: true, value: undefined })
+ try {
+ expect(parseDeepLink("opencode://open-project?directory=/tmp/demo")).toBe("/tmp/demo")
+ } finally {
+ if (original) Object.defineProperty(URL, "canParse", original)
+ if (!original) Reflect.deleteProperty(URL, "canParse")
+ }
+ })
+
+ test("ignores open-project deep links without directory", () => {
+ expect(parseDeepLink("opencode://open-project")).toBeUndefined()
+ expect(parseDeepLink("opencode://open-project?directory=")).toBeUndefined()
+ })
+
test("collects only valid open-project directories", () => {
const result = collectOpenProjectDeepLinks([
"opencode://open-project?directory=/a",
@@ -39,6 +60,14 @@ describe("layout workspace helpers", () => {
expect(workspaceKey("C:\\tmp\\demo\\\\")).toBe("C:\\tmp\\demo")
})
+ test("preserves posix and drive roots in workspace key", () => {
+ expect(workspaceKey("/")).toBe("/")
+ expect(workspaceKey("///")).toBe("/")
+ expect(workspaceKey("C:\\")).toBe("C:\\")
+ expect(workspaceKey("C:\\\\\\")).toBe("C:\\")
+ expect(workspaceKey("C:///")).toBe("C:/")
+ })
+
test("keeps local first while preserving known order", () => {
const result = syncWorkspaceOrder("/root", ["/root", "/b", "/c"], ["/root", "/c", "/a", "/b"])
expect(result).toEqual(["/root", "/c", "/b"])
diff --git a/packages/app/src/pages/layout/helpers.ts b/packages/app/src/pages/layout/helpers.ts
index 4d144f34e..6ecccb95c 100644
--- a/packages/app/src/pages/layout/helpers.ts
+++ b/packages/app/src/pages/layout/helpers.ts
@@ -1,7 +1,12 @@
import { getFilename } from "@opencode-ai/util/path"
import { type Session } from "@opencode-ai/sdk/v2/client"
-export const workspaceKey = (directory: string) => directory.replace(/[\\/]+$/, "")
+export const workspaceKey = (directory: string) => {
+ const drive = directory.match(/^([A-Za-z]:)[\\/]+$/)
+ if (drive) return `${drive[1]}${directory.includes("\\") ? "\\" : "/"}`
+ if (/^[\\/]+$/.test(directory)) return directory.includes("\\") ? "\\" : "/"
+ return directory.replace(/[\\/]+$/, "")
+}
export function sortSessions(now: number) {
const oneMinuteAgo = now - 60 * 1000
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx
index 98f8bdbcd..7678ea6a8 100644
--- a/packages/app/src/pages/session.tsx
+++ b/packages/app/src/pages/session.tsx
@@ -40,7 +40,7 @@ import { showToast } from "@opencode-ai/ui/toast"
import { SessionHeader, SessionContextTab, SortableTab, FileVisual, NewSessionView } from "@/components/session"
import { navMark, navParams } from "@/utils/perf"
import { same } from "@/utils/same"
-import { createOpenReviewFile, focusTerminalById } from "@/pages/session/helpers"
+import { createOpenReviewFile, focusTerminalById, getTabReorderIndex } from "@/pages/session/helpers"
import { createScrollSpy } from "@/pages/session/scroll-spy"
import { createFileTabListSync } from "@/pages/session/file-tab-scroll"
import { FileTabContent } from "@/pages/session/file-tabs"
@@ -844,11 +844,9 @@ export default function Page() {
const { draggable, droppable } = event
if (draggable && droppable) {
const currentTabs = tabs().all()
- const fromIndex = currentTabs?.indexOf(draggable.id.toString())
- const toIndex = currentTabs?.indexOf(droppable.id.toString())
- if (fromIndex !== toIndex && toIndex !== undefined) {
- tabs().move(draggable.id.toString(), toIndex)
- }
+ const toIndex = getTabReorderIndex(currentTabs, draggable.id.toString(), droppable.id.toString())
+ if (toIndex === undefined) return
+ tabs().move(draggable.id.toString(), toIndex)
}
}
diff --git a/packages/app/src/pages/session/helpers.test.ts b/packages/app/src/pages/session/helpers.test.ts
index 0afc7eb6a..d877d5b2e 100644
--- a/packages/app/src/pages/session/helpers.test.ts
+++ b/packages/app/src/pages/session/helpers.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
-import { combineCommandSections, createOpenReviewFile, focusTerminalById } from "./helpers"
+import { combineCommandSections, createOpenReviewFile, focusTerminalById, getTabReorderIndex } from "./helpers"
describe("createOpenReviewFile", () => {
test("opens and loads selected review file", () => {
@@ -59,3 +59,13 @@ describe("combineCommandSections", () => {
expect(result.map((item) => item.id)).toEqual(["a", "b", "c"])
})
})
+
+describe("getTabReorderIndex", () => {
+ test("returns target index for valid drag reorder", () => {
+ expect(getTabReorderIndex(["a", "b", "c"], "a", "c")).toBe(2)
+ })
+
+ test("returns undefined for unknown droppable id", () => {
+ expect(getTabReorderIndex(["a", "b", "c"], "a", "missing")).toBeUndefined()
+ })
+})
diff --git a/packages/app/src/pages/session/helpers.ts b/packages/app/src/pages/session/helpers.ts
index d9ce90793..dcf2c8784 100644
--- a/packages/app/src/pages/session/helpers.ts
+++ b/packages/app/src/pages/session/helpers.ts
@@ -36,3 +36,10 @@ export const createOpenReviewFile = (input: {
export const combineCommandSections = (sections: readonly (readonly CommandOption[])[]) => {
return sections.flatMap((section) => section)
}
+
+export const getTabReorderIndex = (tabs: readonly string[], from: string, to: string) => {
+ const fromIndex = tabs.indexOf(from)
+ const toIndex = tabs.indexOf(to)
+ if (fromIndex === -1 || toIndex === -1 || fromIndex === toIndex) return undefined
+ return toIndex
+}