Add TUI notifications and attention sounds (disabled by default) (#26980)
This commit is contained in:
@@ -1,12 +1,47 @@
|
||||
import { ConfigPlugin } from "@/config/plugin"
|
||||
import { TuiKeybind } from "./keybind"
|
||||
import { Schema } from "effect"
|
||||
import { isRecord } from "@/util/record"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { TuiAttentionSoundNames, type TuiAttentionSoundName } from "@opencode-ai/plugin/tui"
|
||||
|
||||
export type TuiAttentionSoundPaths = Partial<Record<TuiAttentionSoundName, string>>
|
||||
|
||||
export function isAttentionSoundName(value: string): value is TuiAttentionSoundName {
|
||||
return TuiAttentionSoundNames.includes(value as TuiAttentionSoundName)
|
||||
}
|
||||
|
||||
export function resolveAttentionSoundPaths(
|
||||
root: string,
|
||||
sounds: unknown,
|
||||
options?: { trim?: boolean },
|
||||
): TuiAttentionSoundPaths {
|
||||
if (!isRecord(sounds)) return {}
|
||||
return Object.fromEntries(
|
||||
Object.entries(sounds).flatMap(([name, file]) => {
|
||||
if (!isAttentionSoundName(name)) return []
|
||||
if (typeof file !== "string") return []
|
||||
const value = options?.trim ? file.trim() : file
|
||||
if (!value) return []
|
||||
return [[name, Filesystem.resolveFilePath(root, value)]]
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export const KeymapLeaderTimeoutDefault = 2000
|
||||
const KeymapLeaderTimeout = Schema.Int.check(Schema.isGreaterThan(0)).annotate({
|
||||
description: "Leader key timeout in milliseconds",
|
||||
})
|
||||
|
||||
const TuiAttentionSounds = Schema.Struct({
|
||||
default: Schema.optional(Schema.String),
|
||||
question: Schema.optional(Schema.String),
|
||||
permission: Schema.optional(Schema.String),
|
||||
error: Schema.optional(Schema.String),
|
||||
done: Schema.optional(Schema.String),
|
||||
subagent_done: Schema.optional(Schema.String),
|
||||
})
|
||||
|
||||
export const ScrollSpeed = Schema.Number.check(Schema.isGreaterThanOrEqualTo(0.001))
|
||||
|
||||
export const ScrollAcceleration = Schema.Struct({
|
||||
@@ -17,6 +52,15 @@ export const DiffStyle = Schema.Literals(["auto", "stacked"]).annotate({
|
||||
description: "Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column",
|
||||
})
|
||||
|
||||
export const Attention = Schema.Struct({
|
||||
enabled: Schema.optional(Schema.Boolean),
|
||||
notifications: Schema.optional(Schema.Boolean),
|
||||
sound: Schema.optional(Schema.Boolean),
|
||||
volume: Schema.optional(Schema.Number.check(Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(1))),
|
||||
sound_pack: Schema.optional(Schema.String),
|
||||
sounds: Schema.optional(TuiAttentionSounds),
|
||||
}).annotate({ description: "Attention notification and sound settings" })
|
||||
|
||||
export const TuiInfo = Schema.Struct({
|
||||
$schema: Schema.optional(Schema.String),
|
||||
theme: Schema.optional(Schema.String),
|
||||
@@ -24,6 +68,7 @@ export const TuiInfo = Schema.Struct({
|
||||
plugin: Schema.optional(Schema.Array(ConfigPlugin.Spec)),
|
||||
plugin_enabled: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)),
|
||||
leader_timeout: Schema.optional(KeymapLeaderTimeout),
|
||||
attention: Schema.optional(Attention),
|
||||
scroll_speed: Schema.optional(ScrollSpeed).annotate({
|
||||
description: "TUI scroll speed",
|
||||
}),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export * as TuiConfig from "./tui"
|
||||
|
||||
import path from "path"
|
||||
import { createBindingLookup } from "@opentui/keymap/extras"
|
||||
import { mergeDeep, unique } from "remeda"
|
||||
import { Context, Effect, Fiber, Layer, Schema } from "effect"
|
||||
@@ -7,7 +8,7 @@ import { ConfigParse } from "@/config/parse"
|
||||
import { InvalidError } from "@/config/error"
|
||||
import * as ConfigPaths from "@/config/paths"
|
||||
import { migrateTuiConfig } from "./tui-migrate"
|
||||
import { KeymapLeaderTimeoutDefault, TuiInfo } from "./tui-schema"
|
||||
import { KeymapLeaderTimeoutDefault, resolveAttentionSoundPaths, TuiInfo } from "./tui-schema"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { isRecord } from "@/util/record"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
@@ -22,6 +23,7 @@ import * as Log from "@opencode-ai/core/util/log"
|
||||
import { ConfigVariable } from "@/config/variable"
|
||||
import { Npm } from "@opencode-ai/core/npm"
|
||||
import type { DeepMutable } from "@opencode-ai/core/schema"
|
||||
import type { TuiAttentionSoundName } from "@opencode-ai/plugin/tui"
|
||||
|
||||
const log = Log.create({ service: "tui.config" })
|
||||
|
||||
@@ -33,7 +35,15 @@ type Acc = {
|
||||
plugin_origins: ConfigPlugin.Origin[]
|
||||
}
|
||||
|
||||
export type Resolved = Omit<Info, "keybinds" | "leader_timeout"> & {
|
||||
export type Resolved = Omit<Info, "attention" | "keybinds" | "leader_timeout"> & {
|
||||
attention: {
|
||||
enabled: boolean
|
||||
notifications: boolean
|
||||
sound: boolean
|
||||
volume: number
|
||||
sound_pack: string
|
||||
sounds: Partial<Record<TuiAttentionSoundName, string>>
|
||||
}
|
||||
keybinds: TuiKeybind.BindingLookupView
|
||||
leader_timeout: number
|
||||
// Internal resolved plugin list used by runtime loading.
|
||||
@@ -101,7 +111,16 @@ const loadState = Effect.fn("TuiConfig.loadState")(function* (ctx: { directory:
|
||||
})
|
||||
}
|
||||
}
|
||||
const validated = ConfigParse.schema(Info, normalized, configFilepath)
|
||||
const parsed = ConfigParse.schema(Info, normalized, configFilepath)
|
||||
const validated = parsed.attention?.sounds
|
||||
? {
|
||||
...parsed,
|
||||
attention: {
|
||||
...parsed.attention,
|
||||
sounds: resolveAttentionSoundPaths(path.dirname(configFilepath), parsed.attention.sounds),
|
||||
},
|
||||
}
|
||||
: parsed
|
||||
return yield* resolvePlugins(validated, configFilepath)
|
||||
}).pipe(
|
||||
// catchCause (not tapErrorCause + orElseSucceed) because JSONC parsing and validation
|
||||
@@ -197,6 +216,14 @@ const loadState = Effect.fn("TuiConfig.loadState")(function* (ctx: { directory:
|
||||
const parsedKeybinds = TuiKeybind.parse(keybinds)
|
||||
const result: Resolved = {
|
||||
...acc.result,
|
||||
attention: {
|
||||
enabled: acc.result.attention?.enabled ?? false,
|
||||
notifications: acc.result.attention?.notifications ?? true,
|
||||
sound: acc.result.attention?.sound ?? true,
|
||||
volume: acc.result.attention?.volume ?? 0.4,
|
||||
sound_pack: acc.result.attention?.sound_pack ?? "opencode.default",
|
||||
sounds: acc.result.attention?.sounds ?? {},
|
||||
},
|
||||
keybinds: createBindingLookup(TuiKeybind.toBindingConfig(parsedKeybinds), {
|
||||
commandMap: TuiKeybind.CommandMap,
|
||||
bindingDefaults: TuiKeybind.bindingDefaults(),
|
||||
|
||||
Reference in New Issue
Block a user