mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 02:24:10 +08:00
feat(tui): add automatic tabs mode (#50456)
This commit is contained in:
committed by
GitHub
parent
1814dd9799
commit
5b9dc35eec
@@ -7,7 +7,7 @@ import { produce, type Draft } from "immer"
|
||||
import { applyEdits, modify, parse, type ParseError } from "jsonc-parser"
|
||||
import path from "path"
|
||||
import { ConfigMigration } from "./migrate"
|
||||
import { Info, SchemaURL } from "./schema"
|
||||
import { Info, normalizeLegacyTabs, SchemaURL } from "./schema"
|
||||
|
||||
export * from "./schema"
|
||||
|
||||
@@ -119,7 +119,7 @@ function merge(...values: readonly (Info | undefined)[]) {
|
||||
return Option.getOrElse(
|
||||
decode(
|
||||
values.reduce<Record<string, unknown>>(
|
||||
(result, value) => mergeRecords(result, value ?? {}),
|
||||
(result, value) => mergeRecords(result, normalizeLegacyTabs(value) ?? {}),
|
||||
{},
|
||||
),
|
||||
),
|
||||
|
||||
@@ -8,3 +8,11 @@ export const Info = Schema.Struct({
|
||||
...Config.Info.fields,
|
||||
})
|
||||
export type Info = Schema.Schema.Type<typeof Info>
|
||||
|
||||
export function normalizeLegacyTabs(info: Info | undefined) {
|
||||
if (info?.tabs?.enabled === undefined) return info
|
||||
const tabs = { ...info.tabs }
|
||||
tabs.mode ??= tabs.enabled ? "on" : "off"
|
||||
delete tabs.enabled
|
||||
return { ...info, tabs }
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ test("merges inline CLI config content over the global config", async () => {
|
||||
await Bun.write(
|
||||
file,
|
||||
JSON.stringify({
|
||||
tabs: { enabled: true, scope: "global" },
|
||||
tabs: { mode: "on", scope: "global" },
|
||||
keybinds: { "app.exit": "ctrl+q" },
|
||||
plugins: ["global"],
|
||||
animations: true,
|
||||
@@ -105,7 +105,7 @@ test("merges inline CLI config content over the global config", async () => {
|
||||
}),
|
||||
)
|
||||
|
||||
expect(result.loaded.tabs).toEqual({ enabled: false, scope: "global" })
|
||||
expect(result.loaded.tabs).toEqual({ mode: "off", scope: "global" })
|
||||
expect(result.loaded.keybinds).toEqual({ "app.exit": "ctrl+q", "help.show": false })
|
||||
expect(result.loaded.plugins).toEqual(["inline"])
|
||||
expect(result.updated).toMatchObject({ animations: false, mouse: false })
|
||||
@@ -116,6 +116,26 @@ test("merges inline CLI config content over the global config", async () => {
|
||||
}
|
||||
})
|
||||
|
||||
test("reads the legacy tabs toggle without rewriting it", async () => {
|
||||
await using directory = await tmpdir()
|
||||
const file = path.join(directory.path, "cli.json")
|
||||
await Bun.write(file, JSON.stringify({ tabs: { enabled: false } }))
|
||||
|
||||
const config = await run(
|
||||
directory.path,
|
||||
Effect.gen(function* () {
|
||||
const service = yield* Config.Service
|
||||
expect((yield* service.get()).tabs).toEqual({ mode: "off" })
|
||||
return yield* service.update((draft) => {
|
||||
draft.animations = false
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
expect(config.tabs).toEqual({ mode: "off" })
|
||||
expect(await Bun.file(file).json()).toEqual({ tabs: { enabled: false }, animations: false })
|
||||
})
|
||||
|
||||
test("migrates tui and kv config into cli.json", async () => {
|
||||
await using directory = await tmpdir()
|
||||
await Bun.write(
|
||||
|
||||
@@ -121,12 +121,11 @@ export const settings: Setting[] = [
|
||||
keywords: ["approve", "accept", "permission requests"],
|
||||
},
|
||||
{
|
||||
title: "Enabled",
|
||||
title: "Mode",
|
||||
category: "Tabs",
|
||||
path: ["tabs", "enabled"],
|
||||
default: true,
|
||||
values: [false, true],
|
||||
labels: ["off", "on"],
|
||||
path: ["tabs", "mode"],
|
||||
default: "auto",
|
||||
values: ["off", "on", "auto"],
|
||||
},
|
||||
{
|
||||
title: "Scope",
|
||||
|
||||
@@ -173,8 +173,11 @@ export const Info = Schema.Struct({
|
||||
).annotate({ description: "Session transcript presentation settings" }),
|
||||
tabs: Schema.optional(
|
||||
Schema.Struct({
|
||||
mode: Schema.optional(Schema.Literals(["auto", "on", "off"])).annotate({
|
||||
description: "Use session tabs always, never, or when the terminal environment supports them",
|
||||
}),
|
||||
enabled: Schema.optional(Schema.Boolean).annotate({
|
||||
description: "Use a persistent tab strip instead of pinned quick-switch sessions",
|
||||
description: "Legacy tab toggle; use mode instead",
|
||||
}),
|
||||
scope: Schema.optional(Schema.Literals(["global", "cwd"])).annotate({
|
||||
description: "Share tabs globally or keep a separate set for each working directory",
|
||||
@@ -261,6 +264,7 @@ export type Resolved = Omit<Info, "attention" | "cursor" | "keybinds" | "leader"
|
||||
tps: boolean
|
||||
}
|
||||
tabs: {
|
||||
mode: "auto" | "on" | "off"
|
||||
enabled: boolean
|
||||
scope: "global" | "cwd"
|
||||
layout: "horizontal" | "vertical"
|
||||
@@ -268,7 +272,12 @@ export type Resolved = Omit<Info, "attention" | "cursor" | "keybinds" | "leader"
|
||||
}
|
||||
}
|
||||
|
||||
export function resolve(input: Info, options: { terminalSuspend: boolean }): Resolved {
|
||||
export function resolve(
|
||||
input: Info,
|
||||
options: { terminalSuspend: boolean; environment?: Readonly<Record<string, string | undefined>> },
|
||||
): Resolved {
|
||||
const tabsMode =
|
||||
input.tabs?.mode ?? (input.tabs?.enabled === undefined ? "auto" : input.tabs.enabled ? "on" : "off")
|
||||
const keybinds: TuiKeybind.KeybindOverrides = { ...input.keybinds }
|
||||
if (!options.terminalSuspend) {
|
||||
keybinds["terminal.suspend"] = "none"
|
||||
@@ -310,7 +319,8 @@ export function resolve(input: Info, options: { terminalSuspend: boolean }): Res
|
||||
},
|
||||
tabs: {
|
||||
...input.tabs,
|
||||
enabled: input.tabs?.enabled ?? true,
|
||||
mode: tabsMode,
|
||||
enabled: tabsMode === "on" || (tabsMode === "auto" && (options.environment ?? process.env).HERDR_ENV !== "1"),
|
||||
scope: input.tabs?.scope ?? "cwd",
|
||||
layout: input.tabs?.layout ?? "horizontal",
|
||||
indicators: input.tabs?.indicators ?? "status",
|
||||
@@ -327,7 +337,7 @@ const ConfigContext = createContext<{
|
||||
export function ConfigProvider(props: {
|
||||
config: Resolved
|
||||
service?: Interface
|
||||
options?: { terminalSuspend: boolean }
|
||||
options?: { terminalSuspend: boolean; environment?: Readonly<Record<string, string | undefined>> }
|
||||
children: JSX.Element
|
||||
}) {
|
||||
const [config, setConfig] = createStore(props.config)
|
||||
|
||||
@@ -123,7 +123,7 @@ test.each(["dismissed", "refreshing"])(
|
||||
const locations: string[] = []
|
||||
await using setup = await createAppFixture({
|
||||
state: state.path,
|
||||
config: { animations: false, tabs: { enabled: false } },
|
||||
config: { animations: false, tabs: { mode: "off" } },
|
||||
fetch: (url) => {
|
||||
if (url.pathname === "/api/session") {
|
||||
if (url.searchParams.has("parentID")) {
|
||||
@@ -447,7 +447,7 @@ test("vertical session tabs switch to horizontal below readable content width",
|
||||
state: state.path,
|
||||
config: {
|
||||
animations: false,
|
||||
tabs: { enabled: true, layout: "vertical", indicators: "status" },
|
||||
tabs: { mode: "on", layout: "vertical", indicators: "status" },
|
||||
session: { sidebar: "hide" },
|
||||
},
|
||||
args: { sessionID: session.id },
|
||||
@@ -485,7 +485,7 @@ test("narrow vertical session tabs collapse to a compact rail with the terminal"
|
||||
state: state.path,
|
||||
config: {
|
||||
animations: false,
|
||||
tabs: { enabled: true, layout: "vertical", indicators: "status" },
|
||||
tabs: { mode: "on", layout: "vertical", indicators: "status" },
|
||||
session: { sidebar: "hide" },
|
||||
},
|
||||
args: { sessionID: session.id },
|
||||
@@ -526,7 +526,7 @@ test("automatic rename refreshes the displayed title before settling, even witho
|
||||
width: 110,
|
||||
height: 20,
|
||||
state: state.path,
|
||||
config: { tabs: { enabled: true, layout: "vertical" }, session: { sidebar: "hide" } },
|
||||
config: { tabs: { mode: "on", layout: "vertical" }, session: { sidebar: "hide" } },
|
||||
args: { sessionID: session.id },
|
||||
fetch: async (url, request) => {
|
||||
if (url.pathname === "/api/location") return json(location)
|
||||
@@ -585,7 +585,7 @@ test.each([80, 120])("completes custom Markdown and ordinary fences in a session
|
||||
width,
|
||||
height: 55,
|
||||
state: state.path,
|
||||
config: { animations: false, tabs: { enabled: false }, session: { sidebar: "hide" } },
|
||||
config: { animations: false, tabs: { mode: "off" }, session: { sidebar: "hide" } },
|
||||
args: { sessionID: session.id },
|
||||
fetch: (url) => {
|
||||
if (url.pathname === `/api/session/${session.id}`) return json({ data: session })
|
||||
@@ -690,7 +690,7 @@ test("keeps assistant footer metrics current after prepend, same-length refresh,
|
||||
width: 100,
|
||||
height: 40,
|
||||
state: state.path,
|
||||
config: { animations: false, tabs: { enabled: false }, session: { sidebar: "hide", tps: true } },
|
||||
config: { animations: false, tabs: { mode: "off" }, session: { sidebar: "hide", tps: true } },
|
||||
args: { sessionID: session.id },
|
||||
fetch: (url) => {
|
||||
if (url.pathname === `/api/session/${session.id}`) return json({ data: session })
|
||||
@@ -886,7 +886,7 @@ test.each([false, true])("uses the resolved launch directory for new prompts (fa
|
||||
let session: unknown
|
||||
await using setup = await createAppFixture({
|
||||
state: state.path,
|
||||
config: { animations: false, tabs: { enabled: false }, keybinds: { "session.new": "f6" } },
|
||||
config: { animations: false, tabs: { mode: "off" }, keybinds: { "session.new": "f6" } },
|
||||
fetch: async (url, request) => {
|
||||
requests.push(url)
|
||||
if (url.searchParams.has("location[directory]") && url.searchParams.get("location[directory]") !== target)
|
||||
@@ -1024,7 +1024,7 @@ test("completed user shell output replaces a partial live read when the final re
|
||||
let failedReads = 0
|
||||
await using setup = await createAppFixture({
|
||||
state: state.path,
|
||||
config: { animations: false, tabs: { enabled: false }, session: { sidebar: "hide" } },
|
||||
config: { animations: false, tabs: { mode: "off" }, session: { sidebar: "hide" } },
|
||||
args: { sessionID: session.id },
|
||||
fetch: (url) => {
|
||||
if (url.pathname === "/api/session") return json({ data: [session], cursor: {} })
|
||||
@@ -1456,7 +1456,7 @@ test.each([100, 44])(
|
||||
width,
|
||||
state: state.path,
|
||||
args: { sessionID: session.id },
|
||||
config: { animations: false, tabs: { enabled: false } },
|
||||
config: { animations: false, tabs: { mode: "off" } },
|
||||
fetch: (url) => {
|
||||
if (url.pathname === "/api/session") return json({ data: [session], cursor: {} })
|
||||
if (url.pathname === `/api/session/${session.id}`) return json({ data: session })
|
||||
@@ -1620,7 +1620,7 @@ test.each([44, 100])(
|
||||
width,
|
||||
state: state.path,
|
||||
args: { sessionID: session.id },
|
||||
config: { animations: false, tabs: { enabled: false } },
|
||||
config: { animations: false, tabs: { mode: "off" } },
|
||||
fetch: (url) => {
|
||||
if (url.pathname === "/api/session") return json({ data: [session], cursor: {} })
|
||||
if (url.pathname === `/api/session/${session.id}`) return json({ data: session })
|
||||
|
||||
@@ -89,7 +89,7 @@ test.each([40, 120])("shell completion notices do not navigate at width %s", asy
|
||||
app: { name: "test", version: "test", channel: "test" },
|
||||
server: { endpoint: { url: server.url.toString() } },
|
||||
config: {
|
||||
get: async () => ({ animations: false, tabs: { enabled: false } }),
|
||||
get: async () => ({ animations: false, tabs: { mode: "off" } }),
|
||||
update: async () => ({}),
|
||||
},
|
||||
packages: { prepare: async () => ({ directory: "" }) },
|
||||
@@ -170,7 +170,7 @@ test.each([40, 120])("subagent completion notices navigate to the child session
|
||||
app: { name: "test", version: "test", channel: "test" },
|
||||
server: { endpoint: { url: server.url.toString() } },
|
||||
config: {
|
||||
get: async () => ({ animations: false, tabs: { enabled: false } }),
|
||||
get: async () => ({ animations: false, tabs: { mode: "off" } }),
|
||||
update: async () => ({}),
|
||||
},
|
||||
packages: { prepare: async () => ({ directory: "" }) },
|
||||
|
||||
@@ -28,7 +28,7 @@ test("releasing a transcript selection over tab controls does not activate them"
|
||||
const app = await testRender(
|
||||
() => (
|
||||
<TestTuiContexts>
|
||||
<ConfigProvider config={createTuiResolvedConfig({ tabs: { enabled: true } })}>
|
||||
<ConfigProvider config={createTuiResolvedConfig({ tabs: { mode: "on" } })}>
|
||||
<ThemeProvider mode="dark" source={emptyThemeSource}>
|
||||
<box flexDirection="column">
|
||||
<SessionTabs controller={controller} animations={false} />
|
||||
@@ -79,7 +79,7 @@ test("middle-click closes a session tab without selecting it", async () => {
|
||||
const app = await testRender(
|
||||
() => (
|
||||
<TestTuiContexts>
|
||||
<ConfigProvider config={createTuiResolvedConfig({ tabs: { enabled: true } })}>
|
||||
<ConfigProvider config={createTuiResolvedConfig({ tabs: { mode: "on" } })}>
|
||||
<ThemeProvider mode="dark" source={emptyThemeSource}>
|
||||
<SessionTabs controller={controller} animations={false} />
|
||||
</ThemeProvider>
|
||||
@@ -127,7 +127,7 @@ test("keeps consecutive close controls fixed across overflow window changes", as
|
||||
const app = await testRender(
|
||||
() => (
|
||||
<TestTuiContexts>
|
||||
<ConfigProvider config={createTuiResolvedConfig({ tabs: { enabled: true } })}>
|
||||
<ConfigProvider config={createTuiResolvedConfig({ tabs: { mode: "on" } })}>
|
||||
<ThemeProvider mode="dark" source={emptyThemeSource}>
|
||||
<SessionTabs controller={controller} animations={false} />
|
||||
</ThemeProvider>
|
||||
@@ -178,7 +178,7 @@ test("reflows held tabs when the pointer leaves the strip", async () => {
|
||||
const app = await testRender(
|
||||
() => (
|
||||
<TestTuiContexts>
|
||||
<ConfigProvider config={createTuiResolvedConfig({ tabs: { enabled: true } })}>
|
||||
<ConfigProvider config={createTuiResolvedConfig({ tabs: { mode: "on" } })}>
|
||||
<ThemeProvider mode="dark" source={emptyThemeSource}>
|
||||
<box flexDirection="column">
|
||||
<SessionTabs controller={controller} animations={false} />
|
||||
|
||||
@@ -33,7 +33,7 @@ for (const orientation of ["horizontal", "vertical"] as const) {
|
||||
const [status, setStatus] = createSignal<SessionTabsStatus>(EMPTY_SESSION_TAB_STATUS)
|
||||
const [active, setActive] = createSignal("second")
|
||||
const [newTab, setNewTab] = createSignal(false)
|
||||
const settings: Info = { tabs: { enabled: true } }
|
||||
const settings: Info = { tabs: { mode: "on" } }
|
||||
const copied: string[] = []
|
||||
let config!: ReturnType<typeof useConfig>
|
||||
let theme!: ReturnType<typeof useTheme>
|
||||
|
||||
@@ -33,12 +33,13 @@ test("validates mini replay and work spinner settings", () => {
|
||||
test("validates the session tabs setting", () => {
|
||||
const decode = Schema.decodeUnknownSync(Info)
|
||||
|
||||
expect(decode({ tabs: { enabled: true, layout: "vertical", indicators: "numbers" } })).toEqual({
|
||||
tabs: { enabled: true, layout: "vertical", indicators: "numbers" },
|
||||
expect(decode({ tabs: { mode: "on", layout: "vertical", indicators: "numbers" } })).toEqual({
|
||||
tabs: { mode: "on", layout: "vertical", indicators: "numbers" },
|
||||
})
|
||||
expect(() => decode({ tabs: { indicators: "unknown" } })).toThrow()
|
||||
expect(() => decode({ tabs: { layout: true } })).toThrow()
|
||||
expect(() => decode({ tabs: { enabled: "on" } })).toThrow()
|
||||
expect(() => decode({ tabs: { mode: true } })).toThrow()
|
||||
expect(decode({ tabs: { enabled: false } })).toEqual({ tabs: { enabled: false } })
|
||||
expect(decode({ prompt: { image_preview: true } })).toEqual({ prompt: { image_preview: true } })
|
||||
expect(decode({ session: { image_preview: true } })).toEqual({ session: { image_preview: true } })
|
||||
expect(decode({ session: { tps: false } })).toEqual({ session: { tps: false } })
|
||||
@@ -55,7 +56,7 @@ test("resolves nested config and keybind defaults", () => {
|
||||
diffs: { view: "split" },
|
||||
debug: { devtools: true },
|
||||
},
|
||||
{ terminalSuspend: true },
|
||||
{ terminalSuspend: true, environment: {} },
|
||||
)
|
||||
|
||||
expect(config.leader.timeout).toBe(500)
|
||||
@@ -63,13 +64,38 @@ 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", layout: "horizontal", indicators: "status" })
|
||||
expect(config.tabs).toEqual({
|
||||
mode: "auto",
|
||||
enabled: true,
|
||||
scope: "cwd",
|
||||
layout: "horizontal",
|
||||
indicators: "status",
|
||||
})
|
||||
expect(config.session.new_location).toBe("launch")
|
||||
expect(config.session.tps).toBe(true)
|
||||
})
|
||||
|
||||
test("resolves automatic tabs from the terminal environment", () => {
|
||||
expect(resolve({}, { terminalSuspend: true, environment: {} }).tabs.enabled).toBe(true)
|
||||
expect(resolve({}, { terminalSuspend: true, environment: { HERDR_ENV: "1" } }).tabs.enabled).toBe(false)
|
||||
expect(
|
||||
resolve({ tabs: { mode: "on" } }, { terminalSuspend: true, environment: { HERDR_ENV: "1" } }).tabs.enabled,
|
||||
).toBe(true)
|
||||
expect(resolve({ tabs: { mode: "off" } }, { terminalSuspend: true, environment: {} }).tabs.enabled).toBe(false)
|
||||
expect(resolve({ tabs: { enabled: false } }, { terminalSuspend: true, environment: {} }).tabs).toMatchObject({
|
||||
mode: "off",
|
||||
enabled: false,
|
||||
})
|
||||
expect(
|
||||
resolve({ tabs: { mode: "on", enabled: false } }, { terminalSuspend: true, environment: {} }).tabs,
|
||||
).toMatchObject({ mode: "on", enabled: true })
|
||||
})
|
||||
|
||||
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.mode")).toMatchObject({
|
||||
default: "auto",
|
||||
values: ["off", "on", "auto"],
|
||||
})
|
||||
expect(settings.find((setting) => setting.path.join(".") === "tabs.scope")?.default).toBe("cwd")
|
||||
expect(settings.find((setting) => setting.path.join(".") === "tabs.layout")?.default).toBe("horizontal")
|
||||
expect(settings.find((setting) => setting.path.join(".") === "tabs.indicators")).toMatchObject({
|
||||
|
||||
@@ -135,7 +135,7 @@ async function renderSessionTabs(
|
||||
let storage!: ReturnType<typeof useStorage>
|
||||
let config!: ReturnType<typeof useConfig>
|
||||
let configuration = {
|
||||
tabs: { enabled: options?.tabsEnabled ?? true },
|
||||
tabs: { mode: options?.tabsEnabled === false ? ("off" as const) : ("on" as const) },
|
||||
experimental: options?.experimental,
|
||||
session: { new_location: options?.newLocation ?? "launch" },
|
||||
}
|
||||
@@ -205,7 +205,7 @@ async function renderSessionTabs(
|
||||
setTabsEnabled: (enabled: boolean) =>
|
||||
config.update((draft) => {
|
||||
draft.tabs ??= {}
|
||||
draft.tabs.enabled = enabled
|
||||
draft.tabs.mode = enabled ? "on" : "off"
|
||||
}),
|
||||
async destroy() {
|
||||
app.renderer.destroy()
|
||||
|
||||
@@ -8,7 +8,7 @@ type ResolvedInput = Omit<Info, "attention" | "keybinds" | "leader"> & {
|
||||
}
|
||||
|
||||
export function createTuiResolvedConfig(input: ResolvedInput = {}, options?: { terminal?: boolean }) {
|
||||
const config = resolve(input, { terminalSuspend: process.platform !== "win32" })
|
||||
const config = resolve(input, { terminalSuspend: process.platform !== "win32", environment: {} })
|
||||
return {
|
||||
...config,
|
||||
session: { ...config.session, terminal: options?.terminal ?? config.session.terminal },
|
||||
|
||||
@@ -93,7 +93,7 @@ test.each([
|
||||
config: {
|
||||
get: async () => ({
|
||||
animations: false,
|
||||
tabs: { enabled: false },
|
||||
tabs: { mode: "off" },
|
||||
keybinds: {
|
||||
"session.line.up": "f6",
|
||||
"session.page.down": "f7",
|
||||
|
||||
@@ -78,7 +78,7 @@ test
|
||||
app: { name: "test", version: "test", channel: "test" },
|
||||
server: { endpoint: { url: server.url.toString() } },
|
||||
config: {
|
||||
get: async () => ({ animations: false, tabs: { enabled: false } }),
|
||||
get: async () => ({ animations: false, tabs: { mode: "off" } }),
|
||||
update: async () => ({}),
|
||||
},
|
||||
packages: { prepare: async () => ({ directory: "" }) },
|
||||
|
||||
@@ -38,7 +38,7 @@ test("stats shows only this year and returns after errors or success", async ()
|
||||
app: { name: "test", version: "test", channel: "test" },
|
||||
server: { endpoint: { url: server.url.toString() } },
|
||||
config: {
|
||||
get: async () => ({ animations: false, tabs: { enabled: false } }),
|
||||
get: async () => ({ animations: false, tabs: { mode: "off" } }),
|
||||
update: async () => ({}),
|
||||
},
|
||||
packages: { prepare: async () => ({ directory: "" }) },
|
||||
|
||||
@@ -12,7 +12,7 @@ export default OpenCodeDriver.use(
|
||||
keepArtifacts: true,
|
||||
tui: { recording: true, keypressOverlay: true, viewport: { cols: 90, rows: 30 } },
|
||||
config: { autoupdate: false, username: "Demo" },
|
||||
tuiConfig: { theme: { name: "opencode", mode: "dark" }, animations: false, tabs: { enabled: false } },
|
||||
tuiConfig: { theme: { name: "opencode", mode: "dark" }, animations: false, tabs: { mode: "off" } },
|
||||
project: {
|
||||
git: true,
|
||||
files: {
|
||||
|
||||
@@ -40,7 +40,7 @@ while the TUI is running.
|
||||
Set `OPENCODE_CLI_CONFIG_CONTENT` to apply CLI settings from inline JSON:
|
||||
|
||||
```sh
|
||||
OPENCODE_CLI_CONFIG_CONTENT='{"tabs":{"enabled":false}}' opencode
|
||||
OPENCODE_CLI_CONFIG_CONTENT='{"tabs":{"mode":"off"}}' opencode
|
||||
```
|
||||
|
||||
OpenCode merges the inline settings over the global `cli.json`. Nested objects are merged, while arrays and scalar
|
||||
@@ -152,7 +152,7 @@ Configure the persistent session tab strip:
|
||||
```json title="cli.json"
|
||||
{
|
||||
"tabs": {
|
||||
"enabled": true,
|
||||
"mode": "auto",
|
||||
"scope": "cwd",
|
||||
"layout": "horizontal",
|
||||
"indicators": "status"
|
||||
@@ -160,11 +160,13 @@ Configure the persistent session tab strip:
|
||||
}
|
||||
```
|
||||
|
||||
The legacy boolean `tabs.enabled` setting remains supported: `true` is `on` and `false` is `off`. OpenCode reads it without rewriting the config file.
|
||||
|
||||
<div class="docs-table-scroll" role="region" aria-label="Tab settings" tabIndex={0}>
|
||||
|
||||
| Setting | Values | Description |
|
||||
| ----------------- | --------------------------- | ----------------------------------------------------------------------------- |
|
||||
| `tabs.enabled` | boolean | Uses a persistent tab strip instead of pinned quick-switch sessions. |
|
||||
| `tabs.mode` | `auto`, `on`, or `off` | Uses tabs automatically, always, or never. Auto hides them inside Herdr. |
|
||||
| `tabs.scope` | `cwd` or `global` | Keeps separate tabs per working directory or shares them globally. |
|
||||
| `tabs.layout` | `horizontal` or `vertical` | Places tabs in a horizontal strip or a vertical sidebar. |
|
||||
| `tabs.indicators` | `status` or `numbers` | Shows status icons or always shows tab numbers. |
|
||||
|
||||
Reference in New Issue
Block a user