Skip to content

Commit 4e2b8a5

Browse files
Honaopencode-agent[bot]
authored andcommitted
fix(app): expand Windows file tree folders
1 parent 3f9dad3 commit 4e2b8a5

3 files changed

Lines changed: 140 additions & 1 deletion

File tree

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { base64Encode } from "@opencode-ai/core/util/encode"
2+
import { expect, test } from "@playwright/test"
3+
import { mockOpenCodeServer } from "../utils/mock-server"
4+
import { expectSessionTitle } from "../utils/waits"
5+
6+
const directory = "C:/OpenCode/OpenFileExpand"
7+
const projectID = "proj_open_file_expand"
8+
const sessionID = "ses_open_file_expand"
9+
const title = "Open file expand"
10+
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
11+
12+
test.use({ viewport: { width: 1440, height: 900 } })
13+
14+
test("expands a folder whose path has a trailing Windows separator", async ({ page }) => {
15+
await mockOpenCodeServer(page, {
16+
directory,
17+
project: {
18+
id: projectID,
19+
worktree: directory,
20+
vcs: "git",
21+
name: "open-file-expand",
22+
time: { created: 1700000000000, updated: 1700000000000 },
23+
sandboxes: [],
24+
},
25+
provider: {
26+
all: [
27+
{
28+
id: "opencode",
29+
name: "OpenCode",
30+
models: { test: { id: "test", name: "Test", limit: { context: 200_000 } } },
31+
},
32+
],
33+
connected: ["opencode"],
34+
default: { providerID: "opencode", modelID: "test" },
35+
},
36+
sessions: [
37+
{
38+
id: sessionID,
39+
slug: sessionID,
40+
projectID,
41+
directory,
42+
title,
43+
version: "dev",
44+
time: { created: 1700000000000, updated: 1700000000000 },
45+
},
46+
],
47+
vcsDiff: [],
48+
fileList: (path) => {
49+
if (path === "frontend\\" || path === "frontend") {
50+
return [
51+
{
52+
name: "app.ts",
53+
path: "frontend\\app.ts",
54+
absolute: `${directory}/frontend/app.ts`,
55+
type: "file" as const,
56+
ignored: false,
57+
},
58+
]
59+
}
60+
if (path) return []
61+
return [
62+
{
63+
name: "frontend",
64+
path: "frontend\\",
65+
absolute: `${directory}/frontend`,
66+
type: "directory" as const,
67+
ignored: false,
68+
},
69+
{
70+
name: "README.md",
71+
path: "README.md",
72+
absolute: `${directory}/README.md`,
73+
type: "file" as const,
74+
ignored: false,
75+
},
76+
]
77+
},
78+
fileContent: (path) => ({ type: "text", content: `contents:${path}` }),
79+
pageMessages: () => ({ items: [] }),
80+
})
81+
82+
await page.addInitScript(
83+
({ directory, server, sessionID }) => {
84+
localStorage.setItem(
85+
"settings.v3",
86+
JSON.stringify({ general: { newLayoutDesigns: true, shouldDisplayTabsToast: false } }),
87+
)
88+
localStorage.setItem(
89+
"opencode.global.dat:server",
90+
JSON.stringify({
91+
projects: { local: [{ worktree: directory, expanded: true }] },
92+
lastProject: { local: directory },
93+
}),
94+
)
95+
localStorage.setItem(
96+
"opencode.global.dat:layout",
97+
JSON.stringify({ review: { diffStyle: "split", panelOpened: true } }),
98+
)
99+
localStorage.setItem(
100+
"opencode.global.dat:review-panel-v2",
101+
JSON.stringify({ sidebarOpened: true, sidebarWidth: 240, expandMode: "collapse" }),
102+
)
103+
localStorage.setItem(
104+
"opencode.window.browser.dat:tabs",
105+
JSON.stringify([{ type: "session", server, sessionId: sessionID }]),
106+
)
107+
},
108+
{ directory, server, sessionID },
109+
)
110+
111+
await page.goto(`/server/${base64Encode(server)}/session/${sessionID}`)
112+
await expectSessionTitle(page, title)
113+
114+
const panel = page.locator("#review-panel")
115+
await panel.getByRole("button", { name: "Open file" }).click()
116+
await expect(panel.getByRole("tab", { name: "Open file" })).toHaveAttribute("data-selected", "")
117+
118+
const sidebar = panel.locator('[data-component="session-review-v2-sidebar-root"]')
119+
await expect(sidebar).toBeVisible()
120+
121+
const frontendRow = panel.locator('[data-slot="file-tree-v2-row"][data-path="frontend"]')
122+
await expect(frontendRow).toBeVisible()
123+
await expect(frontendRow).toHaveAttribute("aria-expanded", "false")
124+
await frontendRow.click()
125+
await expect(frontendRow).toHaveAttribute("aria-expanded", "true")
126+
127+
const appRow = panel.locator('[data-slot="file-tree-v2-row"][data-path="frontend/app.ts"]')
128+
await expect(appRow).toBeVisible()
129+
await appRow.click()
130+
await expect(panel.getByRole("tab", { name: "app.ts" })).toHaveAttribute("data-selected", "")
131+
await expect(panel.getByText("contents:frontend/app.ts", { exact: true })).toBeVisible()
132+
})

packages/app/src/context/file/path.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ describe("file path helpers", () => {
2121
expect(path.normalize("c:\\repo\\src\\app.ts")).toBe("src\\app.ts")
2222
})
2323

24+
test("normalizes Windows directory separators", () => {
25+
const path = createPathHelpers(() => "C:\\repo")
26+
expect(path.normalizeDir("frontend\\")).toBe("frontend")
27+
expect(path.normalizeDir("frontend\\src\\")).toBe("frontend/src")
28+
expect(path.normalizeDir("C:\\repo\\frontend\\")).toBe("frontend")
29+
})
30+
2431
test("keeps query/hash stripping behavior stable", () => {
2532
expect(stripQueryAndHash("a/b.ts#L12?x=1")).toBe("a/b.ts")
2633
expect(stripQueryAndHash("a/b.ts?x=1#L12")).toBe("a/b.ts")

packages/app/src/context/file/path.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ export function createPathHelpers(scope: () => string) {
140140
return normalize(tabValue)
141141
}
142142

143-
const normalizeDir = (input: string) => normalize(input).replace(/\/+$/, "")
143+
const normalizeDir = (input: string) => normalize(input).replace(/\\/g, "/").replace(/\/+$/, "")
144144

145145
return {
146146
normalize,

0 commit comments

Comments
 (0)