Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
96ddad1
fix: prevent Windows reserved device names from being added to direct…
Feb 1, 2026
20cb1f5
fix: 为method()函数添加超时保护,修复Windows CLI启动挂起问题(Issue #11657)
Feb 2, 2026
4666b81
fix: 修复Windows CLI启动挂起问题(Issue #11657)
Feb 2, 2026
1e15520
修复Windows上深度链接重复创建项目问题 (Issue #11666)
Feb 2, 2026
f57531c
优化: 重构路径规范化逻辑,消除代码重复
Feb 2, 2026
28f36d7
fix: remove deprecated copyToClipboardOSC52 call
Feb 2, 2026
f0847ce
修复Windows上深度链接重复创建项目问题 (Issue #11666)
Feb 2, 2026
dcd9534
优化: 重构路径规范化逻辑,消除代码重复
Feb 2, 2026
db803aa
fix: 移除app.tsx中不支持的autoFocus属性
Feb 4, 2026
39795c5
Merge branch 'dev' into dev
01luyicheng Feb 4, 2026
f64644d
fix(desktop): removed compression from rpm bundle to save 15m in CI (…
goniz Feb 4, 2026
f870d8a
chore: generate
opencode-agent[bot] Feb 4, 2026
a63681a
fix(app): opened tabs follow created session
adamdotdevin Feb 4, 2026
a0850a1
wip(app): session options
adamdotdevin Feb 4, 2026
3349cf3
fix(app): move session options to the session page
adamdotdevin Feb 4, 2026
5f69c58
fix(app): file tree not staying in sync across projects/sessions
adamdotdevin Feb 4, 2026
4b32b59
ci: remove source-based AUR package from publish script
thdxr Feb 4, 2026
19510ca
fix(app): don't show scroll-to-bottom unecessarily
adamdotdevin Feb 4, 2026
582f7aa
test(app): fix dated e2e tests
adamdotdevin Feb 4, 2026
fbfd973
fix(core): session errors when attachment file not found
adamdotdevin Feb 4, 2026
a42e6b5
test(app): fix e2e test action
adamdotdevin Feb 4, 2026
dc77a5a
fix(ui): review comments z-index stacking
adamdotdevin Feb 4, 2026
853207e
fix(app): terminal hyperlink clicks
adamdotdevin Feb 4, 2026
ce7a064
test(app): fix e2e test action
adamdotdevin Feb 4, 2026
910f4ef
fix(app): clear comments on prompt submission (#12148)
adamdotdevin Feb 4, 2026
6f42901
fix: ensure that plugin installs use --no-cache when using http proxy…
rekram1-node Feb 4, 2026
4ecf852
fix: cloudflare workers ai provider (#12157)
rekram1-node Feb 4, 2026
db161a9
fix: ensure kimi-for-coding plan has thinking on by default for k2p5 …
monotykamary Feb 4, 2026
0d76c36
fix(app): safety triangle for sidebar hover (#12179)
adamdotdevin Feb 4, 2026
34d4c5a
fix(app): last turn changes rendered in review pane (#12182)
adamdotdevin Feb 4, 2026
e6430f5
fix(app): derive terminal WebSocket URL from browser origin instead o…
0xdsqr Feb 4, 2026
2cacc68
fix(desktop): Refresh file contents when changing workspaces to not h…
ParkerSm1th Feb 4, 2026
9851a11
fix(app): terminal EOL issues
adamdotdevin Feb 4, 2026
6e06b31
fix(core): skip dependency install in read-only config dirs (#12128)
shantur Feb 4, 2026
4eefacd
fix(app): terminal url
adamdotdevin Feb 4, 2026
3f24534
fix(tui): add hover states to question tool tabs (#12203)
maharshi365 Feb 5, 2026
3921093
feat: Allow the function to hide or show thinking blocks to be bound …
ariane-emory Feb 5, 2026
dd23716
chore: generate
opencode-agent[bot] Feb 5, 2026
7933e1c
fix(app): refresh workspace sessions on project switch (#12189)
neriousy Feb 5, 2026
cff1830
zen: set session affinity header
fwang Feb 5, 2026
ce7fccb
wip: zen
fwang Feb 5, 2026
dccd00a
fix: wait for dependencies before loading custom tools and plugins (#…
thdxr Feb 5, 2026
069cd99
修复Windows路径处理问题
Feb 4, 2026
107b303
Merge branch 'dev' into dev
01luyicheng Feb 5, 2026
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
9 changes: 7 additions & 2 deletions packages/app/src/context/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Project } from "@opencode-ai/sdk/v2"
import { Persist, persisted, removePersisted } from "@/utils/persist"
import { same } from "@/utils/same"
import { createScrollPersistence, type SessionScroll } from "./layout-scroll"
import { normalizePathForComparison } from "@/utils/path"

const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
export type AvatarColorKey = (typeof AVATAR_COLOR_KEYS)[number]
Expand Down Expand Up @@ -299,7 +300,9 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
for (const project of globalSync.data.project) {
const sandboxes = project.sandboxes ?? []
for (const sandbox of sandboxes) {
map.set(sandbox, project.worktree)
// 使用规范化路径作为键,避免Windows上的路径不一致问题
// 修复Issue #11666: Windows路径规范化不一致导致重复创建项目
map.set(normalizePathForComparison(sandbox), project.worktree)
}
}
return map
Expand All @@ -316,7 +319,9 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
const current = chain[chain.length - 1]
if (!current) return directory

const next = map.get(current)
// 使用规范化路径查找,避免Windows上的路径不一致问题
// 修复Issue #11666: Windows路径规范化不一致导致重复创建项目
const next = map.get(normalizePathForComparison(current))
if (!next) return current

if (visited.has(next)) return directory
Expand Down
27 changes: 22 additions & 5 deletions packages/app/src/context/server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { batch, createEffect, createMemo, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import { usePlatform } from "@/context/platform"
import { Persist, persisted } from "@/utils/persist"
import { normalizePathForComparison } from "@/utils/path"

type StoredProject = { worktree: string; expanded: boolean }

Expand Down Expand Up @@ -164,38 +165,54 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
const key = origin()
if (!key) return
const current = store.projects[key] ?? []
if (current.find((x) => x.worktree === directory)) return
// 使用规范化路径检查项目是否已存在,避免Windows上的重复项目
// 修复Issue #11666: Windows路径规范化不一致导致重复创建项目
const normalizedDirectory = normalizePathForComparison(directory)
const existing = current.find((x) => normalizePathForComparison(x.worktree) === normalizedDirectory)
if (existing) return
setStore("projects", key, [{ worktree: directory, expanded: true }, ...current])
},
close(directory: string) {
const key = origin()
if (!key) return
const current = store.projects[key] ?? []
// 使用规范化路径查找项目,避免Windows上的路径不一致问题
// 修复Issue #11666: Windows路径规范化不一致导致重复创建项目
const normalizedDirectory = normalizePathForComparison(directory)
setStore(
"projects",
key,
current.filter((x) => x.worktree !== directory),
current.filter((x) => normalizePathForComparison(x.worktree) !== normalizedDirectory),
)
},
expand(directory: string) {
const key = origin()
if (!key) return
const current = store.projects[key] ?? []
const index = current.findIndex((x) => x.worktree === directory)
// 使用规范化路径查找项目,避免Windows上的路径不一致问题
// 修复Issue #11666: Windows路径规范化不一致导致重复创建项目
const normalizedDirectory = normalizePathForComparison(directory)
const index = current.findIndex((x) => normalizePathForComparison(x.worktree) === normalizedDirectory)
if (index !== -1) setStore("projects", key, index, "expanded", true)
},
collapse(directory: string) {
const key = origin()
if (!key) return
const current = store.projects[key] ?? []
const index = current.findIndex((x) => x.worktree === directory)
// 使用规范化路径查找项目,避免Windows上的路径不一致问题
// 修复Issue #11666: Windows路径规范化不一致导致重复创建项目
const normalizedDirectory = normalizePathForComparison(directory)
const index = current.findIndex((x) => normalizePathForComparison(x.worktree) === normalizedDirectory)
if (index !== -1) setStore("projects", key, index, "expanded", false)
},
move(directory: string, toIndex: number) {
const key = origin()
if (!key) return
const current = store.projects[key] ?? []
const fromIndex = current.findIndex((x) => x.worktree === directory)
// 使用规范化路径查找项目,避免Windows上的路径不一致问题
// 修复Issue #11666: Windows路径规范化不一致导致重复创建项目
const normalizedDirectory = normalizePathForComparison(directory)
const fromIndex = current.findIndex((x) => normalizePathForComparison(x.worktree) === normalizedDirectory)
if (fromIndex === -1 || fromIndex === toIndex) return
const result = [...current]
const [item] = result.splice(fromIndex, 1)
Expand Down
112 changes: 90 additions & 22 deletions packages/app/src/pages/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ import { DialogEditProject } from "@/components/dialog-edit-project"
import { Titlebar } from "@/components/titlebar"
import { useServer } from "@/context/server"
import { useLanguage, type Locale } from "@/context/language"
import { normalizePathForComparison } from "@/utils/path"

export default function Layout(props: ParentProps) {
const [store, setStore, , ready] = persisted(
Expand Down Expand Up @@ -553,11 +554,14 @@ export default function Layout(props: ParentProps) {
if (!directory) return

const projects = layout.projects.list()
// 使用规范化路径查找项目,避免Windows上的路径不一致问题
// 修复Issue #11666: Windows路径规范化不一致导致重复创建项目
const normalizedDirectory = normalizePathForComparison(directory)

const sandbox = projects.find((p) => p.sandboxes?.includes(directory))
if (sandbox) return sandbox

const direct = projects.find((p) => p.worktree === directory)
const direct = projects.find((p) => normalizePathForComparison(p.worktree) === normalizedDirectory)
if (direct) return direct

const [child] = globalSync.child(directory, { bootstrap: false })
Expand All @@ -568,7 +572,7 @@ export default function Layout(props: ParentProps) {
const root = meta?.worktree
if (!root) return

return projects.find((p) => p.worktree === root)
return projects.find((p) => normalizePathForComparison(p.worktree) === normalizePathForComparison(root))
})

createEffect(
Expand Down Expand Up @@ -614,11 +618,16 @@ export default function Layout(props: ParentProps) {
),
)

const workspaceKey = (directory: string) => directory.replace(/[\\/]+$/, "")
/**
* 生成工作区唯一标识键
* 使用规范化路径确保在Windows上同一物理路径不会产生重复键
* 修复Issue #11666: Windows路径规范化不一致导致重复创建项目
*/
const workspaceKey = (directory: string) => normalizePathForComparison(directory)

const workspaceName = (directory: string, projectId?: string, branch?: string) => {
const key = workspaceKey(directory)
const direct = store.workspaceName[key] ?? store.workspaceName[directory]
const direct = store.workspaceName[key]
if (direct) return direct
if (!projectId) return
if (!branch) return
Expand Down Expand Up @@ -1219,12 +1228,32 @@ export default function Layout(props: ParentProps) {

const deepLinkEvent = "opencode:deep-link"

const parseDeepLink = (input: string) => {
/**
* 解析深度链接URL
* 返回原始项目目录路径,保留原始格式
* 在需要比较路径时再进行规范化
<<<<<<< HEAD
*
* @param input - 深度链接URL字符串
* @returns 原始目录路径,如果URL无效则返回undefined
*
=======
*
* @param input - 深度链接URL字符串
* @returns 原始目录路径,如果URL无效则返回undefined
*
>>>>>>> 065844dbe (优化: 重构路径规范化逻辑,消除代码重复)
* @example
* parseDeepLink('opencode://open-project?directory=C:\\Users\\Project')
* // 返回: 'C:\\Users\\Project'
*/
const parseDeepLink = (input: string): string | undefined => {
if (!input.startsWith("opencode://")) return
const url = new URL(input)
if (url.hostname !== "open-project") return
const directory = url.searchParams.get("directory")
if (!directory) return
// 返回原始路径,保留原始格式
return directory
}

Expand Down Expand Up @@ -1279,7 +1308,10 @@ export default function Layout(props: ParentProps) {
}

function closeProject(directory: string) {
const index = layout.projects.list().findIndex((x) => x.worktree === directory)
// 使用规范化路径查找项目,避免Windows上的路径不一致问题
// 修复Issue #11666: Windows路径规范化不一致导致重复创建项目
const normalizedDirectory = normalizePathForComparison(directory)
const index = layout.projects.list().findIndex((x) => normalizePathForComparison(x.worktree) === normalizedDirectory)
const next = layout.projects.list()[index + 1]
layout.projects.close(directory)
if (next) navigateToProject(next.worktree)
Expand Down Expand Up @@ -1617,8 +1649,12 @@ export default function Layout(props: ParentProps) {
const { draggable, droppable } = event
if (draggable && droppable) {
const projects = layout.projects.list()
const fromIndex = projects.findIndex((p) => p.worktree === draggable.id.toString())
const toIndex = projects.findIndex((p) => p.worktree === droppable.id.toString())
// 使用规范化路径查找项目,避免Windows上的路径不一致问题
// 修复Issue #11666: Windows路径规范化不一致导致重复创建项目
const normalizedDraggable = normalizePathForComparison(draggable.id.toString())
const normalizedDroppable = normalizePathForComparison(droppable.id.toString())
const fromIndex = projects.findIndex((p) => normalizePathForComparison(p.worktree) === normalizedDraggable)
const toIndex = projects.findIndex((p) => normalizePathForComparison(p.worktree) === normalizedDroppable)
if (fromIndex !== toIndex && toIndex !== -1) {
layout.projects.move(draggable.id.toString(), toIndex)
}
Expand All @@ -1634,7 +1670,11 @@ export default function Layout(props: ParentProps) {
const local = project.worktree
const dirs = [local, ...(project.sandboxes ?? [])]
const active = currentProject()
const directory = active?.worktree === project.worktree ? decode64(params.dir) : undefined
// 使用规范化路径比较,避免Windows上的路径不一致问题
// 修复Issue #11666: Windows路径规范化不一致导致重复创建项目
const normalizedActiveWorktree = active?.worktree ? normalizePathForComparison(active.worktree) : undefined
const normalizedProjectWorktree = normalizePathForComparison(project.worktree)
const directory = normalizedActiveWorktree === normalizedProjectWorktree ? decode64(params.dir) : undefined
const extra = directory && directory !== local && !dirs.includes(directory) ? directory : undefined
const pending = extra ? WorktreeState.get(extra)?.status === "pending" : false

Expand Down Expand Up @@ -1670,8 +1710,12 @@ export default function Layout(props: ParentProps) {
if (!project) return

const ids = workspaceIds(project)
const fromIndex = ids.findIndex((dir) => dir === draggable.id.toString())
const toIndex = ids.findIndex((dir) => dir === droppable.id.toString())
// 使用规范化路径查找项目,避免Windows上的路径不一致问题
// 修复Issue #11666: Windows路径规范化不一致导致重复创建项目
const normalizedDraggable = normalizePathForComparison(draggable.id.toString())
const normalizedDroppable = normalizePathForComparison(droppable.id.toString())
const fromIndex = ids.findIndex((dir) => normalizePathForComparison(dir) === normalizedDraggable)
const toIndex = ids.findIndex((dir) => normalizePathForComparison(dir) === normalizedDroppable)
if (fromIndex === -1 || toIndex === -1) return
if (fromIndex === toIndex) return

Expand Down Expand Up @@ -1980,7 +2024,12 @@ export default function Layout(props: ParentProps) {
}

const ProjectDragOverlay = (): JSX.Element => {
const project = createMemo(() => layout.projects.list().find((p) => p.worktree === store.activeProject))
const project = createMemo(() => {
// 使用规范化路径查找项目,避免Windows上的路径不一致问题
// 修复Issue #11666: Windows路径规范化不一致导致重复创建项目
const normalizedActiveProject = store.activeProject ? normalizePathForComparison(store.activeProject) : undefined
return layout.projects.list().find((p) => normalizedActiveProject && normalizePathForComparison(p.worktree) === normalizedActiveProject)
})
return (
<Show when={project()}>
{(p) => (
Expand Down Expand Up @@ -2042,10 +2091,20 @@ export default function Layout(props: ParentProps) {
}
return map
})
const local = createMemo(() => props.directory === props.project.worktree)
const local = createMemo(() => {
// 使用规范化路径比较,避免Windows上的路径不一致问题
// 修复Issue #11666: Windows路径规范化不一致导致重复创建项目
const normalizedDirectory = normalizePathForComparison(props.directory)
const normalizedWorktree = normalizePathForComparison(props.project.worktree)
return normalizedDirectory === normalizedWorktree
})
const active = createMemo(() => {
const current = decode64(params.dir) ?? ""
return current === props.directory
// 使用规范化路径比较,避免Windows上的路径不一致问题
// 修复Issue #11666: Windows路径规范化不一致导致重复创建项目
const normalizedCurrent = normalizePathForComparison(current)
const normalizedDirectory = normalizePathForComparison(props.directory)
return normalizedCurrent === normalizedDirectory
})
const workspaceValue = createMemo(() => {
const branch = workspaceStore.vcs?.branch
Expand Down Expand Up @@ -2257,7 +2316,11 @@ export default function Layout(props: ParentProps) {
const sortable = createSortable(props.project.worktree)
const selected = createMemo(() => {
const current = decode64(params.dir) ?? ""
return props.project.worktree === current || props.project.sandboxes?.includes(current)
// 使用规范化路径比较,避免Windows上的路径不一致问题
// 修复Issue #11666: Windows路径规范化不一致导致重复创建项目
const normalizedCurrent = normalizePathForComparison(current)
const normalizedWorktree = normalizePathForComparison(props.project.worktree)
return normalizedWorktree === normalizedCurrent || props.project.sandboxes?.includes(current)
})

const workspaces = createMemo(() => workspaceIds(props.project).slice(0, 2))
Expand All @@ -2269,9 +2332,13 @@ export default function Layout(props: ParentProps) {

const preview = createMemo(() => !props.mobile && layout.sidebar.opened())
const overlay = createMemo(() => !props.mobile && !layout.sidebar.opened())
const active = createMemo(
() => menu() || (preview() ? open() : overlay() && state.hoverProject === props.project.worktree),
)
const active = createMemo(() => {
// 使用规范化路径比较,避免Windows上的路径不一致问题
// 修复Issue #11666: Windows路径规范化不一致导致重复创建项目
const normalizedHoverProject = state.hoverProject ? normalizePathForComparison(state.hoverProject) : undefined
const normalizedWorktree = normalizePathForComparison(props.project.worktree)
return (preview() ? open() : overlay() && normalizedHoverProject === normalizedWorktree)
})

createEffect(() => {
if (preview()) return
Expand All @@ -2281,8 +2348,12 @@ export default function Layout(props: ParentProps) {

const label = (directory: string) => {
const [data] = globalSync.child(directory, { bootstrap: false })
// 使用规范化路径比较,避免Windows上的路径不一致问题
// 修复Issue #11666: Windows路径规范化不一致导致重复创建项目
const normalizedDirectory = normalizePathForComparison(directory)
const normalizedWorktree = normalizePathForComparison(props.project.worktree)
const kind =
directory === props.project.worktree ? language.t("workspace.type.local") : language.t("workspace.type.sandbox")
normalizedDirectory === normalizedWorktree ? language.t("workspace.type.local") : language.t("workspace.type.sandbox")
const name = workspaceLabel(directory, data.vcs?.branch, props.project.id)
return `${kind} : ${name}`
}
Expand Down Expand Up @@ -2581,9 +2652,6 @@ export default function Layout(props: ParentProps) {
setBusy(created.directory, true)
WorktreeState.pending(created.directory)
setStore("workspaceExpanded", key, true)
if (key !== created.directory) {
setStore("workspaceExpanded", created.directory, true)
}
setStore("workspaceOrder", project.worktree, (prev) => {
const existing = prev ?? []
const next = existing.filter((item) => {
Expand Down
61 changes: 61 additions & 0 deletions packages/app/src/utils/path.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* 文件用途: 路径工具模块
* 作者: TRAE, 创建日期: 2026-02-02
*
* 输入输出签名:
* - normalizePathForComparison(path: string): string - 规范化路径用于比较
*
* 依赖列表: 无
*
* 与其他模块交互方式:
* - 被 packages/app/src/context/server.tsx 导入使用
* - 被 packages/app/src/pages/layout.tsx 导入使用
*
* 其他备注:
* - 此模块提供路径规范化功能,用于解决Windows路径大小写不一致问题
* - 与 packages/opencode/src/util/filesystem.ts 中的 normalizePath 不同,后者使用 realpathSync.native 获取文件系统真实路径
*/

/**
* 规范化路径用于比较,避免在Windows上重复创建项目
*
* @param path - 需要规范化的路径
* @returns 规范化后的路径,格式为:正斜杠、无末尾斜杠、Windows上为小写
* @throws {Error} 如果路径为空或无效
*
* @example
* // Windows
* normalizePathForComparison('C:\\Users\\Project') // 'c:/users/project'
* normalizePathForComparison('C:/Users/Project/') // 'c:/users/project'
*
* @example
* // macOS/Linux
* normalizePathForComparison('/Users/Project') // '/Users/Project'
* normalizePathForComparison('/Users/Project/') // '/Users/Project'
*/
export function normalizePathForComparison(path: string): string {
// 输入验证
if (!path || typeof path !== 'string') {
throw new Error('Invalid path: path must be a non-empty string')
}

let normalized = path.trim()

// 检查空路径
if (normalized.length === 0) {
throw new Error('Invalid path: path cannot be empty')
}

// 统一斜杠为正斜杠
normalized = normalized.replace(/\\/g, '/')

// 移除末尾斜杠
normalized = normalized.replace(/\/$/, '')

// 在Windows上,统一为小写以进行不区分大小写的比较
if (process.platform === 'win32') {
normalized = normalized.toLowerCase()
}

return normalized
}
1 change: 0 additions & 1 deletion packages/opencode/src/cli/cmd/tui/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,6 @@ export function tui(input: {
gatherStats: false,
exitOnCtrlC: false,
useKittyKeyboard: {},
autoFocus: false,
consoleOptions: {
keyBindings: [{ name: "y", ctrl: true, action: "copy-selection" }],
onCopySelection: (text) => {
Expand Down
Loading
Loading