diff --git a/packages/core/src/config/plugin/agent.ts b/packages/core/src/config/plugin/agent.ts index 4e4d6ff7ca4..48b5455f14c 100644 --- a/packages/core/src/config/plugin/agent.ts +++ b/packages/core/src/config/plugin/agent.ts @@ -161,11 +161,14 @@ function isPathAction(action: string): action is PathAction { } function expandHome(resource: string, home: string) { - if (resource.startsWith("~/")) return home + resource.slice(1) if (resource === "~") return home if (resource === "$HOME") return home - if (resource.startsWith("$HOME/")) return home + resource.slice(5) - if (resource.startsWith("$HOME\\")) return home + resource.slice(5) + const relative = resource.startsWith("~/") + ? resource.slice(2) + : resource.startsWith("$HOME/") || resource.startsWith("$HOME\\") + ? resource.slice(6) + : undefined + if (relative !== undefined) return (path.posix.isAbsolute(home) ? path.posix : path.win32).join(home, relative) return resource } diff --git a/packages/core/test/config/agent.test.ts b/packages/core/test/config/agent.test.ts index 49aed818489..dd56f82398a 100644 --- a/packages/core/test/config/agent.test.ts +++ b/packages/core/test/config/agent.test.ts @@ -51,6 +51,11 @@ describe("ConfigAgentPlugin.Plugin", () => { it.effect("matches Windows paths against home-relative permissions", () => Effect.gen(function* () { const permissions = yield* loadHomePermissions("C:\\Users\\test") + expect(permissions).toContainEqual({ + action: "external_directory", + resource: "C:\\Users\\test\\p\\**", + effect: "allow", + }) expect( Permission.evaluate("external_directory", "C:\\Users\\test\\p\\opencode\\src\\*", permissions).effect, ).toBe("allow") diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index 376963cddbe..7db8c6dc773 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -512,7 +512,7 @@ function App(props: { pair?: DialogPairCredentials }) { const terminalTitleEnabled = () => config.data.terminal?.title ?? true const copyOnSelectEnabled = () => config.data.terminal?.copy_on_select ?? process.platform !== "win32" const pasteSummaryEnabled = () => config.data.prompt?.paste !== "full" - const tabsVertical = () => (config.data.tabs?.vertical ?? false) && sessionTabsFitVertically(dimensions().width) + const tabsVertical = () => config.data.tabs.layout === "vertical" && sessionTabsFitVertically(dimensions().width) const tabsVisible = () => sessionTabs.enabled() && (sessionTabs.tabs().length > 0 || sessionTabs.newTab()) && route.data.type !== "plugin" diff --git a/packages/tui/src/component/dialog-config.tsx b/packages/tui/src/component/dialog-config.tsx index 98abf0a99bc..2cbc6c64527 100644 --- a/packages/tui/src/component/dialog-config.tsx +++ b/packages/tui/src/component/dialog-config.tsx @@ -101,12 +101,11 @@ export const settings: Setting[] = [ labels: ["current directory", "global"], }, { - title: "Vertical", + title: "Layout", category: "Tabs", - path: ["tabs", "vertical"], - default: false, - values: [false, true], - labels: ["off", "on"], + path: ["tabs", "layout"], + default: "horizontal", + values: ["horizontal", "vertical"], keywords: ["sidebar", "orientation", "left"], }, { diff --git a/packages/tui/src/config/index.tsx b/packages/tui/src/config/index.tsx index d94616bc9fb..a8358c129d3 100644 --- a/packages/tui/src/config/index.tsx +++ b/packages/tui/src/config/index.tsx @@ -132,8 +132,8 @@ export const Info = Schema.Struct({ scope: Schema.optional(Schema.Literals(["global", "cwd"])).annotate({ description: "Share tabs globally or keep a separate set for each working directory", }), - vertical: Schema.optional(Schema.Boolean).annotate({ - description: "Show tabs in a left sidebar instead of a horizontal strip", + layout: Schema.optional(Schema.Literals(["horizontal", "vertical"])).annotate({ + description: "Show tabs in a horizontal strip or vertical sidebar", }), }), ).annotate({ description: "Tab strip settings" }), @@ -194,7 +194,7 @@ export type Resolved = Omit dimensions().width - - (config.tabs?.enabled && config.tabs.vertical && sessionTabsFitVertically(dimensions().width) + (config.tabs?.enabled && config.tabs.layout === "vertical" && sessionTabsFitVertically(dimensions().width) ? SESSION_SIDEBAR_WIDTH : 0), ) diff --git a/packages/tui/test/config-v2.test.tsx b/packages/tui/test/config-v2.test.tsx index 19a1be0512a..a22c2904dd4 100644 --- a/packages/tui/test/config-v2.test.tsx +++ b/packages/tui/test/config-v2.test.tsx @@ -18,7 +18,10 @@ test("validates mini replay settings", () => { test("validates the session tabs setting", () => { const decode = Schema.decodeUnknownSync(Info) - expect(decode({ tabs: { enabled: true, vertical: true } })).toEqual({ tabs: { enabled: true, vertical: true } }) + expect(decode({ tabs: { enabled: true, layout: "vertical" } })).toEqual({ + tabs: { enabled: true, layout: "vertical" }, + }) + expect(() => decode({ tabs: { layout: true } })).toThrow() expect(() => decode({ tabs: { enabled: "on" } })).toThrow() }) @@ -39,12 +42,13 @@ test("resolves nested config and keybind defaults", () => { expect(config.scroll).toEqual({ speed: 2, acceleration: true }) expect(config.diffs).toEqual({ view: "split" }) expect(config.debug).toEqual({ devtools: true }) - expect(config.tabs).toEqual({ enabled: true, scope: "cwd" }) + expect(config.tabs).toEqual({ enabled: true, scope: "cwd", layout: "horizontal" }) }) test("shows resolved tab defaults in settings", () => { expect(settings.find((setting) => setting.path.join(".") === "tabs.enabled")?.default).toBe(true) expect(settings.find((setting) => setting.path.join(".") === "tabs.scope")?.default).toBe("cwd") + expect(settings.find((setting) => setting.path.join(".") === "tabs.layout")?.default).toBe("horizontal") }) test("provides config and its host interface", async () => {