Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 5 additions & 1 deletion packages/opencode/src/config/tui-migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const decodeTheme = Schema.decodeUnknownOption(Schema.String)
const decodeRecord = Schema.decodeUnknownOption(Schema.Record(Schema.String, Schema.Unknown))
const decodeScrollSpeed = Schema.decodeUnknownOption(TuiConfig.ScrollSpeed)
const decodeScrollAcceleration = Schema.decodeUnknownOption(TuiConfig.ScrollAcceleration)
const decodeAutoScroll = Schema.decodeUnknownOption(Schema.Boolean)
const decodeDiffStyle = Schema.decodeUnknownOption(TuiConfig.DiffStyle)

interface MigrateInput {
Expand Down Expand Up @@ -71,17 +72,20 @@ function normalizeTui(data: Record<string, unknown>):
| {
scroll_speed: number | undefined
scroll_acceleration: { enabled: boolean } | undefined
auto_scroll: boolean | undefined
diff_style: "auto" | "stacked" | undefined
}
| undefined {
const parsed = {
scroll_speed: Option.getOrUndefined(decodeScrollSpeed(data.scroll_speed)),
scroll_acceleration: Option.getOrUndefined(decodeScrollAcceleration(data.scroll_acceleration)),
auto_scroll: Option.getOrUndefined(decodeAutoScroll(data.auto_scroll)),
diff_style: Option.getOrUndefined(decodeDiffStyle(data.diff_style)),
}
return parsed.scroll_speed === undefined &&
parsed.diff_style === undefined &&
parsed.scroll_acceleration === undefined
parsed.scroll_acceleration === undefined &&
parsed.auto_scroll === undefined
? undefined
: parsed
}
Expand Down
4 changes: 3 additions & 1 deletion packages/opencode/test/config/tui.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,17 +191,19 @@ it.instance("migrates tui-specific keys from opencode.json when tui.json does no
const source = path.join(test.directory, "opencode.json")
yield* fs.writeJson(source, {
theme: "migrated-theme",
tui: { scroll_speed: 5 },
tui: { scroll_speed: 5, auto_scroll: false },
keybinds: { app_exit: "ctrl+q" },
})

const config = yield* getTuiConfig(test.directory)
expect(config.theme).toBe("migrated-theme")
expect(config.scroll_speed).toBe(5)
expect(config.auto_scroll).toBe(false)
expect(config.keybinds.get("app.exit")?.[0]?.key).toBe("ctrl+q")
expect(JSON.parse(yield* fs.readFileString(path.join(test.directory, "tui.json")))).toMatchObject({
theme: "migrated-theme",
scroll_speed: 5,
auto_scroll: false,
})
const server = JSON.parse(yield* fs.readFileString(source))
expect(server.theme).toBeUndefined()
Expand Down
4 changes: 4 additions & 0 deletions packages/sdk/js/src/gen/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1203,6 +1203,10 @@ export type Config = {
*/
enabled: boolean
}
/**
* Follow new output by sticking the conversation viewport to the bottom. When the user scrolls up, follow pauses until they return to the bottom (default: true)
*/
auto_scroll?: boolean
/**
* Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column
*/
Expand Down
4 changes: 4 additions & 0 deletions packages/tui/src/config/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ export const Info = Schema.Struct({
prompt: Schema.optional(Prompt),
scroll_speed: Schema.optional(ScrollSpeed).annotate({ description: "TUI scroll speed" }),
scroll_acceleration: Schema.optional(ScrollAcceleration),
auto_scroll: Schema.optional(Schema.Boolean).annotate({
description:
"Follow new output by sticking the conversation viewport to the bottom. When the user scrolls up, follow pauses until they return to the bottom (default: true)",
}),
diff_style: Schema.optional(DiffStyle),
mouse: Schema.optional(Schema.Boolean).annotate({ description: "Enable or disable mouse capture (default: true)" }),
})
Expand Down
3 changes: 2 additions & 1 deletion packages/tui/src/routes/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ export function Session() {
const providers = createMemo(() => Model.index(sync.data.provider))

const scrollAcceleration = createMemo(() => getScrollAcceleration(tuiConfig))
const autoScroll = createMemo(() => tuiConfig.auto_scroll ?? true)
const toast = useToast()
const sdk = useSDK()
const editor = useEditorContext()
Expand Down Expand Up @@ -1173,7 +1174,7 @@ export function Session() {
foregroundColor: theme.border,
},
}}
stickyScroll={true}
stickyScroll={autoScroll()}
stickyStart="bottom"
flexGrow={1}
scrollAcceleration={scrollAcceleration()}
Expand Down
1 change: 1 addition & 0 deletions packages/tui/src/util/scroll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { MacOSScrollAccel, type ScrollAcceleration } from "@opentui/core"
export type ScrollConfig = {
scroll_acceleration?: { enabled?: boolean }
scroll_speed?: number
auto_scroll?: boolean
}

export class CustomSpeedScroll implements ScrollAcceleration {
Expand Down
4 changes: 3 additions & 1 deletion packages/tui/test/config.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,16 @@ test("validates config constraints", () => {
attention: { volume: 1, sounds: { done: "done.wav" } },
prompt: { max_height: 10, max_width: "auto" },
scroll_speed: 0.001,
auto_scroll: false,
diff_style: "stacked",
plugin: ["example-plugin"],
}),
).toMatchObject({ leader_timeout: 250, attention: { volume: 1 }, diff_style: "stacked" })
).toMatchObject({ leader_timeout: 250, attention: { volume: 1 }, auto_scroll: false, diff_style: "stacked" })
expect(() => decodeInfo({ leader_timeout: 0 })).toThrow()
expect(() => decodeInfo({ attention: { volume: 1.1 } })).toThrow()
expect(() => decodeInfo({ prompt: { max_width: 0 } })).toThrow()
expect(() => decodeInfo({ scroll_speed: 0 })).toThrow()
expect(() => decodeInfo({ auto_scroll: "yes" })).toThrow()
expect(decodeInfo({ attention: { sounds: { unknown: "sound.wav" } } })).toEqual({ attention: { sounds: {} } })
})

Expand Down
2 changes: 2 additions & 0 deletions packages/web/src/content/docs/tui.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@ You can customize TUI behavior through `tui.json` (or `tui.jsonc`).
"scroll_acceleration": {
"enabled": false
},
"auto_scroll": true,
"diff_style": "auto",
"mouse": true,
"attention": {
Expand All @@ -394,6 +395,7 @@ This is separate from `opencode.json`, which configures server/runtime behavior.
- `leader_timeout` - Controls how long OpenCode waits after the leader key. Defaults to `2000`.
- `scroll_acceleration.enabled` - Enable macOS-style scroll acceleration for smooth, natural scrolling. When enabled, scroll speed increases with rapid scrolling gestures and stays precise for slower movements. **This setting takes precedence over `scroll_speed` and overrides it when enabled.**
- `scroll_speed` - Controls how fast the TUI scrolls when using scroll commands (minimum: `0.001`, supports decimal values). Defaults to `3`. **Note: This is ignored if `scroll_acceleration.enabled` is set to `true`.**
- `auto_scroll` - Follow new output by sticking the conversation viewport to the bottom (default: `true`). Set to `false` to disable auto-follow so the viewport stays where you left it while messages stream in.
- `diff_style` - Controls diff rendering. `"auto"` adapts to terminal width, `"stacked"` always shows a single-column layout.
- `mouse` - Enable or disable mouse capture in the TUI (default: `true`). When disabled, the terminal's native mouse selection/scrolling behavior is preserved.
- `attention` - Configures TUI desktop notifications and sounds. Disabled by default.
Expand Down
Loading