summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src
diff options
context:
space:
mode:
authorErik Demaine <[email protected]>2026-02-22 18:49:05 -0500
committerGitHub <[email protected]>2026-02-23 09:49:05 +1000
commita74fedd23beecc70cb7cf7f07c6a14186787c960 (patch)
tree42800bd7b9e4e3b8d100bf99cbab92774e6b881a /packages/app/src
parenteb64ce08b8a862c27b7dab58c3a8d2ea1981655f (diff)
downloadopencode-a74fedd23beecc70cb7cf7f07c6a14186787c960.tar.gz
opencode-a74fedd23beecc70cb7cf7f07c6a14186787c960.zip
fix(desktop): change detection on Windows, especially Cygwin (#13659)
Co-authored-by: LukeParkerDev <[email protected]>
Diffstat (limited to 'packages/app/src')
-rw-r--r--packages/app/src/context/file/path.test.ts8
-rw-r--r--packages/app/src/context/file/path.ts24
-rw-r--r--packages/app/src/i18n/en.ts1
-rw-r--r--packages/app/src/pages/session.tsx7
4 files changed, 29 insertions, 11 deletions
diff --git a/packages/app/src/context/file/path.test.ts b/packages/app/src/context/file/path.test.ts
index f2a3c44b6..7eb5e8b2a 100644
--- a/packages/app/src/context/file/path.test.ts
+++ b/packages/app/src/context/file/path.test.ts
@@ -13,6 +13,14 @@ describe("file path helpers", () => {
expect(path.pathFromTab("other://src/app.ts")).toBeUndefined()
})
+ test("normalizes Windows absolute paths with mixed separators", () => {
+ const path = createPathHelpers(() => "C:\\repo")
+ expect(path.normalize("C:\\repo\\src\\app.ts")).toBe("src/app.ts")
+ expect(path.normalize("C:/repo/src/app.ts")).toBe("src/app.ts")
+ expect(path.normalize("file://C:/repo/src/app.ts")).toBe("src/app.ts")
+ expect(path.normalize("c:\\repo\\src\\app.ts")).toBe("src/app.ts")
+ })
+
test("keeps query/hash stripping behavior stable", () => {
expect(stripQueryAndHash("a/b.ts#L12?x=1")).toBe("a/b.ts")
expect(stripQueryAndHash("a/b.ts?x=1#L12")).toBe("a/b.ts")
diff --git a/packages/app/src/context/file/path.ts b/packages/app/src/context/file/path.ts
index 859fdc040..6be7588f9 100644
--- a/packages/app/src/context/file/path.ts
+++ b/packages/app/src/context/file/path.ts
@@ -103,16 +103,20 @@ export function encodeFilePath(filepath: string): string {
export function createPathHelpers(scope: () => string) {
const normalize = (input: string) => {
- const root = scope()
- const prefix = root.endsWith("/") ? root : root + "/"
-
- let path = unquoteGitPath(decodeFilePath(stripQueryAndHash(stripFileProtocol(input))))
-
- if (path.startsWith(prefix)) {
- path = path.slice(prefix.length)
- }
-
- if (path.startsWith(root)) {
+ const root = scope().replace(/\\/g, "/")
+
+ let path = unquoteGitPath(decodeFilePath(stripQueryAndHash(stripFileProtocol(input)))).replace(/\\/g, "/")
+
+ // Remove initial root prefix, if it's a complete match or followed by /
+ // (don't want /foo/bar to root of /f).
+ // For Windows paths, also check for case-insensitive match.
+ const windows = /^[A-Za-z]:/.test(root)
+ const canonRoot = windows ? root.toLowerCase() : root
+ const canonPath = windows ? path.toLowerCase() : path
+ if (canonPath.startsWith(canonRoot) &&
+ (canonRoot.endsWith("/") || canonPath === canonRoot ||
+ canonPath.startsWith(canonRoot + "/"))) {
+ // If we match canonRoot + "/", the slash will be removed below.
path = path.slice(root.length)
}
diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts
index 0fa3777dd..992509fcf 100644
--- a/packages/app/src/i18n/en.ts
+++ b/packages/app/src/i18n/en.ts
@@ -495,6 +495,7 @@ export const dict = {
"session.review.change.other": "Changes",
"session.review.loadingChanges": "Loading changes...",
"session.review.empty": "No changes in this session yet",
+ "session.review.noVcs": "No git VCS detected, so session changes will not be detected",
"session.review.noChanges": "No changes",
"session.files.selectToOpen": "Select a file to open",
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx
index a3f4b7164..e0ef92682 100644
--- a/packages/app/src/pages/session.tsx
+++ b/packages/app/src/pages/session.tsx
@@ -274,6 +274,11 @@ export default function Page() {
if (!hasReview()) return true
return sync.data.session_diff[id] !== undefined
})
+ const reviewEmptyKey = createMemo(() => {
+ const project = sync.project
+ if (!project || project.vcs) return "session.review.empty"
+ return "session.review.noVcs"
+ })
let inputRef!: HTMLDivElement
let promptDock: HTMLDivElement | undefined
@@ -531,7 +536,7 @@ export default function Page() {
) : (
<div class={input.emptyClass}>
<Mark class="w-14 opacity-10" />
- <div class="text-14-regular text-text-weak max-w-56">{language.t("session.review.empty")}</div>
+ <div class="text-14-regular text-text-weak max-w-56">{language.t(reviewEmptyKey())}</div>
</div>
)
}