/** * Keybindings - Keybinding configuration service definitions. * * Owns parsing, validation, merge, and persistence of user keybinding * configuration consumed by the server runtime. * * @module Keybindings */import { KeybindingRule, KeybindingsConfig, KeybindingsConfigError, KeybindingShortcut, KeybindingWhenNode, MAX_KEYBINDINGS_COUNT, MODEL_PICKER_JUMP_KEYBINDING_COMMANDS, MAX_WHEN_EXPRESSION_DEPTH, ResolvedKeybindingRule, ResolvedKeybindingsConfig, THREAD_JUMP_KEYBINDING_COMMANDS, type ServerConfigIssue,} from "@t3tools/contracts";import type { Mutable } from "effect/Types";import { Array, Cache, Cause, Deferred, Duration, Effect, Exit, FileSystem, Path, Layer, Option, Predicate, PubSub, Schema, SchemaGetter, SchemaIssue, SchemaTransformation, Ref, Context, Scope, Stream,} from "effect";import * as Semaphore from "effect/Semaphore";import { ServerConfig } from "./config.ts";import { writeFileStringAtomically } from "./atomicWrite.ts";import { fromLenientJson } from "@t3tools/shared/schemaJson"; type WhenToken = | { type: "identifier"; value: string } | { type: "not" } | { type: "and" } | { type: "or" } | { type: "lparen" } | { type: "rparen" }; export const DEFAULT_KEYBINDINGS: ReadonlyArray<KeybindingRule> = [ { key: "mod+j", command: "terminal.toggle" }, { key: "mod+d", command: "terminal.split", when: "terminalFocus" }, { key: "mod+n", command: "terminal.new", when: "terminalFocus" }, { key: "mod+w", command: "terminal.close", when: "terminalFocus" }, { key: "mod+d", command: "diff.toggle", when: "!terminalFocus" }, { key: "mod+k", command: "commandPalette.toggle", when: "!terminalFocus" }, { key: "mod+n", command: "chat.new", when: "!terminalFocus" }, { key: "mod+shift+o", command: "chat.new", when: "!terminalFocus" }, { key: "mod+shift+n", command: "chat.newLocal", when: "!terminalFocus" }, { key: "mod+shift+m", command: "modelPicker.toggle", when: "!terminalFocus" }, { key: "mod+o", command: "editor.openFavorite" }, { key: "mod+shift+[", command: "thread.previous" }, { key: "mod+shift+]", command: "thread.next" }, ...THREAD_JUMP_KEYBINDING_COMMANDS.map((command, index) => ({ key: `mod+${index + 1}`, command, })), ...MODEL_PICKER_JUMP_KEYBINDING_COMMANDS.map((command, index) => ({ key: `mod+${index + 1}`, command, when: "modelPickerOpen", })),]; function normalizeKeyToken(token: string): string { if (token === "space") return " "; if (token === "esc") return "escape"; return token;} /** @internal - Exported for testing */export function parseKeybindingShortcut(value: string): KeybindingShortcut | null { const rawTokens = value .toLowerCase() .split("+") .map((token) => token.trim()); const tokens = [...rawTokens]; let trailingEmptyCount = 0; while (tokens[tokens.length - 1] === "") { trailingEmptyCount += 1; tokens.pop(); } if (trailingEmptyCount > 0) { tokens.push("+"); } if (tokens.some((token) => token.length === 0)) { return null; } if (tokens.length === 0) return null; let key: string | null = null; let metaKey = false; let ctrlKey = false; let shiftKey = false; let altKey = false; let modKey = false; for (const token of tokens) { switch (token) { case "cmd": case "meta": metaKey = true; break; case "ctrl": case "control": ctrlKey = true; break; case "shift": shiftKey = true; break; case "alt": case "option": altKey = true; break; case "mod": modKey = true; break; default: { if (key !== null) return null; key = normalizeKeyToken(token); } } } if (key === null) return null; return { key, metaKey, ctrlKey, shiftKey, altKey, modKey, };} function tokenizeWhenExpression(expression: string): WhenToken[] | null { const tokens: WhenToken[] = []; let index = 0; while (index < expression.length) { const current = expression[index]; if (!current) break; if (/\s/.test(current)) { index += 1; continue; } if (expression.startsWith("&&", index)) { tokens.push({ type: "and" }); index += 2; continue; } if (expression.startsWith("||", index)) { tokens.push({ type: "or" }); index += 2; continue; } if (current === "!") { tokens.push({ type: "not" }); index += 1; continue; } if (current === "(") { tokens.push({ type: "lparen" }); index += 1; continue; } if (current === ")") { tokens.push({ type: "rparen" }); index += 1; continue; } const identifier = /^[A-Za-z_][A-Za-z0-9_.-]*/.exec(expression.slice(index)); if (!identifier) { return null; } tokens.push({ type: "identifier", value: identifier[0] }); index += identifier[0].length; } return tokens;} function parseKeybindingWhenExpression(expression: string): KeybindingWhenNode | null { const tokens = tokenizeWhenExpression(expression); if (!tokens || tokens.length === 0) return null; let index = 0; const parsePrimary = (depth: number): KeybindingWhenNode | null => { if (depth > MAX_WHEN_EXPRESSION_DEPTH) { return null; } const token = tokens[index]; if (!token) return null; if (token.type === "identifier") { index += 1; return { type: "identifier", name: token.value }; } if (token.type === "lparen") { index += 1; const expressionNode = parseOr(depth + 1); const closeToken = tokens[index]; if (!expressionNode || !closeToken || closeToken.type !== "rparen") { return null; } index += 1; return expressionNode; } return null; }; const parseUnary = (depth: number): KeybindingWhenNode | null => { let notCount = 0; while (tokens[index]?.type === "not") { index += 1; notCount += 1; if (notCount > MAX_WHEN_EXPRESSION_DEPTH) { return null; } } let node = parsePrimary(depth); if (!node) return null; while (notCount > 0) { node = { type: "not", node }; notCount -= 1; } return node; }; const parseAnd = (depth: number): KeybindingWhenNode | null => { let left = parseUnary(depth); if (!left) return null; while (tokens[index]?.type === "and") { index += 1; const right = parseUnary(depth); if (!right) return null; left = { type: "and", left, right }; } return left; }; const parseOr = (depth: number): KeybindingWhenNode | null => { let left = parseAnd(depth); if (!left) return null; while (tokens[index]?.type === "or") { index += 1; const right = parseAnd(depth); if (!right) return null; left = { type: "or", left, right }; } return left; }; const ast = parseOr(0); if (!ast || index !== tokens.length) return null; return ast;} /** @internal - Exported for testing */export function compileResolvedKeybindingRule(rule: KeybindingRule): ResolvedKeybindingRule | null { const shortcut = parseKeybindingShortcut(rule.key); if (!shortcut) return null; if (rule.when !== undefined) { const whenAst = parseKeybindingWhenExpression(rule.when); if (!whenAst) return null; return { command: rule.command, shortcut, whenAst, }; } return { command: rule.command, shortcut, };} export function compileResolvedKeybindingsConfig( config: KeybindingsConfig,): ResolvedKeybindingsConfig { const compiled: Mutable<ResolvedKeybindingsConfig> = []; for (const rule of config) { const result = Schema.decodeExit(ResolvedKeybindingFromConfig)(rule); if (result._tag === "Success") { compiled.push(result.value); } } return compiled;} export const ResolvedKeybindingFromConfig = KeybindingRule.pipe( Schema.decodeTo( Schema.toType(ResolvedKeybindingRule), SchemaTransformation.transformOrFail({ decode: (rule) => Effect.succeed(compileResolvedKeybindingRule(rule)).pipe( Effect.filterOrFail( Predicate.isNotNull, () => new SchemaIssue.InvalidValue(Option.some(rule), { message: "Invalid keybinding rule", }), ), Effect.map((resolved) => resolved), ), encode: (resolved) => Effect.gen(function* () { const key = encodeShortcut(resolved.shortcut); if (!key) { return yield* Effect.fail( new SchemaIssue.InvalidValue(Option.some(resolved), { message: "Resolved shortcut cannot be encoded to key string", }), ); } const when = resolved.whenAst ? encodeWhenAst(resolved.whenAst) : undefined; return { key, command: resolved.command, when, }; }), }), ),); export const ResolvedKeybindingsFromConfig = Schema.Array(ResolvedKeybindingFromConfig).check( Schema.isMaxLength(MAX_KEYBINDINGS_COUNT),); function isSameKeybindingRule(left: KeybindingRule, right: KeybindingRule): boolean { return ( left.command === right.command && left.key === right.key && (left.when ?? undefined) === (right.when ?? undefined) );} function keybindingShortcutContext(rule: KeybindingRule): string | null { const parsed = parseKeybindingShortcut(rule.key); if (!parsed) return null; const encoded = encodeShortcut(parsed); if (!encoded) return null; return `${encoded}\u0000${rule.when ?? ""}`;} function hasSameShortcutContext(left: KeybindingRule, right: KeybindingRule): boolean { const leftContext = keybindingShortcutContext(left); const rightContext = keybindingShortcutContext(right); if (!leftContext || !rightContext) return false; return leftContext === rightContext;} function encodeShortcut(shortcut: KeybindingShortcut): string | null { const modifiers: string[] = []; if (shortcut.modKey) modifiers.push("mod"); if (shortcut.metaKey) modifiers.push("meta"); if (shortcut.ctrlKey) modifiers.push("ctrl"); if (shortcut.altKey) modifiers.push("alt"); if (shortcut.shiftKey) modifiers.push("shift"); if (!shortcut.key) return null; if (shortcut.key !== "+" && shortcut.key.includes("+")) return null; const key = shortcut.key === " " ? "space" : shortcut.key; return [...modifiers, key].join("+");} function encodeWhenAst(node: KeybindingWhenNode): string { switch (node.type) { case "identifier": return node.name; case "not": return `!(${encodeWhenAst(node.node)})`; case "and": return `(${encodeWhenAst(node.left)} && ${encodeWhenAst(node.right)})`; case "or": return `(${encodeWhenAst(node.left)} || ${encodeWhenAst(node.right)})`; }} const DEFAULT_RESOLVED_KEYBINDINGS = compileResolvedKeybindingsConfig(DEFAULT_KEYBINDINGS); const RawKeybindingsEntries = fromLenientJson(Schema.Array(Schema.Unknown));const KeybindingsConfigJson = Schema.fromJsonString(KeybindingsConfig);const PrettyJsonString = SchemaGetter.parseJson<string>().compose( SchemaGetter.stringifyJson({ space: 2 }),);const KeybindingsConfigPrettyJson = KeybindingsConfigJson.pipe( Schema.encode({ decode: PrettyJsonString, encode: PrettyJsonString, }),); export interface KeybindingsConfigState { readonly keybindings: ResolvedKeybindingsConfig; readonly issues: readonly ServerConfigIssue[];} export interface KeybindingsChangeEvent { readonly keybindings: ResolvedKeybindingsConfig; readonly issues: readonly ServerConfigIssue[];} function trimIssueMessage(message: string): string { const trimmed = message.trim(); return trimmed.length > 0 ? trimmed : "Invalid keybindings configuration.";} function malformedConfigIssue(detail: string): ServerConfigIssue { return { kind: "keybindings.malformed-config", message: trimIssueMessage(detail), };} function invalidEntryIssue(index: number, detail: string): ServerConfigIssue { return { kind: "keybindings.invalid-entry", index, message: trimIssueMessage(detail), };} function mergeWithDefaultKeybindings(custom: ResolvedKeybindingsConfig): ResolvedKeybindingsConfig { if (custom.length === 0) { return [...DEFAULT_RESOLVED_KEYBINDINGS]; } const overriddenCommands = new Set(custom.map((binding) => binding.command)); const retainedDefaults = DEFAULT_RESOLVED_KEYBINDINGS.filter( (binding) => !overriddenCommands.has(binding.command), ); const merged = [...retainedDefaults, ...custom]; if (merged.length <= MAX_KEYBINDINGS_COUNT) { return merged; } // Keep the latest rules when the config exceeds max size; later rules have higher precedence. return merged.slice(-MAX_KEYBINDINGS_COUNT);} /** * KeybindingsShape - Service API for keybinding configuration operations. */export interface KeybindingsShape { /** * Start the keybindings runtime and attach file watching. * * Safe to call multiple times. The first successful call establishes the * runtime; later calls await the same startup. */ readonly start: Effect.Effect<void, KeybindingsConfigError>; /** * Await keybindings runtime readiness. * * Readiness means the config directory exists, the watcher is attached, the * startup sync has completed, and the current snapshot has been loaded. */ readonly ready: Effect.Effect<void, KeybindingsConfigError>; /** * Ensure the on-disk keybindings file exists and includes all default * commands so newly-added defaults are backfilled on startup.Lines 1–500 of 919. Load more
/** * Keybindings - Keybinding configuration service definitions. * * Owns parsing, validation, merge, and persistence of user keybinding * configuration consumed by the server runtime. * * @module Keybindings */ import { KeybindingRule, KeybindingsConfig, KeybindingsConfigError, KeybindingShortcut, KeybindingWhenNode, MAX_KEYBINDINGS_COUNT, MODEL_PICKER_JUMP_KEYBINDING_COMMANDS, MAX_WHEN_EXPRESSION_DEPTH, ResolvedKeybindingRule, ResolvedKeybindingsConfig, THREAD_JUMP_KEYBINDING_COMMANDS, type ServerConfigIssue, } from "@t3tools/contracts"; import type { Mutable } from "effect/Types"; import { Array, Cache, Cause, Deferred, Duration, Effect, Exit, FileSystem, Path, Layer, Option, Predicate, PubSub, Schema, SchemaGetter, SchemaIssue, SchemaTransformation, Ref, Context, Scope, Stream, } from "effect"; import * as Semaphore from "effect/Semaphore"; import { ServerConfig } from "./config.ts"; import { writeFileStringAtomically } from "./atomicWrite.ts"; import { fromLenientJson } from "@t3tools/shared/schemaJson"; type WhenToken = | { type: "identifier"; value: string } | { type: "not" } | { type: "and" } | { type: "or" } | { type: "lparen" } | { type: "rparen" }; export const DEFAULT_KEYBINDINGS: ReadonlyArray<KeybindingRule> = [ { key: "mod+j", command: "terminal.toggle" }, { key: "mod+d", command: "terminal.split", when: "terminalFocus" }, { key: "mod+n", command: "terminal.new", when: "terminalFocus" }, { key: "mod+w", command: "terminal.close", when: "terminalFocus" }, { key: "mod+d", command: "diff.toggle", when: "!terminalFocus" }, { key: "mod+k", command: "commandPalette.toggle", when: "!terminalFocus" }, { key: "mod+n", command: "chat.new", when: "!terminalFocus" }, { key: "mod+shift+o", command: "chat.new", when: "!terminalFocus" }, { key: "mod+shift+n", command: "chat.newLocal", when: "!terminalFocus" }, { key: "mod+shift+m", command: "modelPicker.toggle", when: "!terminalFocus" }, { key: "mod+o", command: "editor.openFavorite" }, { key: "mod+shift+[", command: "thread.previous" }, { key: "mod+shift+]", command: "thread.next" }, ...THREAD_JUMP_KEYBINDING_COMMANDS.map((command, index) => ({ key: `mod+${index + 1}`, command, })), ...MODEL_PICKER_JUMP_KEYBINDING_COMMANDS.map((command, index) => ({ key: `mod+${index + 1}`, command, when: "modelPickerOpen", })), ]; function normalizeKeyToken(token: string): string { if (token === "space") return " "; if (token === "esc") return "escape"; return token; } /** @internal - Exported for testing */ export function parseKeybindingShortcut(value: string): KeybindingShortcut | null { const rawTokens = value .toLowerCase() .split("+") .map((token) => token.trim()); const tokens = [...rawTokens]; let trailingEmptyCount = 0; while (tokens[tokens.length - 1] === "") { trailingEmptyCount += 1; tokens.pop(); } if (trailingEmptyCount > 0) { tokens.push("+"); } if (tokens.some((token) => token.length === 0)) { return null; } if (tokens.length === 0) return null; let key: string | null = null; let metaKey = false; let ctrlKey = false; let shiftKey = false; let altKey = false; let modKey = false; for (const token of tokens) { switch (token) { case "cmd": case "meta": metaKey = true; break; case "ctrl": case "control": ctrlKey = true; break; case "shift": shiftKey = true; break; case "alt": case "option": altKey = true; break; case "mod": modKey = true; break; default: { if (key !== null) return null; key = normalizeKeyToken(token); } } } if (key === null) return null; return { key, metaKey, ctrlKey, shiftKey, altKey, modKey, }; } function tokenizeWhenExpression(expression: string): WhenToken[] | null { const tokens: WhenToken[] = []; let index = 0; while (index < expression.length) { const current = expression[index]; if (!current) break; if (/\s/.test(current)) { index += 1; continue; } if (expression.startsWith("&&", index)) { tokens.push({ type: "and" }); index += 2; continue; } if (expression.startsWith("||", index)) { tokens.push({ type: "or" }); index += 2; continue; } if (current === "!") { tokens.push({ type: "not" }); index += 1; continue; } if (current === "(") { tokens.push({ type: "lparen" }); index += 1; continue; } if (current === ")") { tokens.push({ type: "rparen" }); index += 1; continue; } const identifier = /^[A-Za-z_][A-Za-z0-9_.-]*/.exec(expression.slice(index)); if (!identifier) { return null; } tokens.push({ type: "identifier", value: identifier[0] }); index += identifier[0].length; } return tokens; } function parseKeybindingWhenExpression(expression: string): KeybindingWhenNode | null { const tokens = tokenizeWhenExpression(expression); if (!tokens || tokens.length === 0) return null; let index = 0; const parsePrimary = (depth: number): KeybindingWhenNode | null => { if (depth > MAX_WHEN_EXPRESSION_DEPTH) { return null; } const token = tokens[index]; if (!token) return null; if (token.type === "identifier") { index += 1; return { type: "identifier", name: token.value }; } if (token.type === "lparen") { index += 1; const expressionNode = parseOr(depth + 1); const closeToken = tokens[index]; if (!expressionNode || !closeToken || closeToken.type !== "rparen") { return null; } index += 1; return expressionNode; } return null; }; const parseUnary = (depth: number): KeybindingWhenNode | null => { let notCount = 0; while (tokens[index]?.type === "not") { index += 1; notCount += 1; if (notCount > MAX_WHEN_EXPRESSION_DEPTH) { return null; } } let node = parsePrimary(depth); if (!node) return null; while (notCount > 0) { node = { type: "not", node }; notCount -= 1; } return node; }; const parseAnd = (depth: number): KeybindingWhenNode | null => { let left = parseUnary(depth); if (!left) return null; while (tokens[index]?.type === "and") { index += 1; const right = parseUnary(depth); if (!right) return null; left = { type: "and", left, right }; } return left; }; const parseOr = (depth: number): KeybindingWhenNode | null => { let left = parseAnd(depth); if (!left) return null; while (tokens[index]?.type === "or") { index += 1; const right = parseAnd(depth); if (!right) return null; left = { type: "or", left, right }; } return left; }; const ast = parseOr(0); if (!ast || index !== tokens.length) return null; return ast; } /** @internal - Exported for testing */ export function compileResolvedKeybindingRule(rule: KeybindingRule): ResolvedKeybindingRule | null { const shortcut = parseKeybindingShortcut(rule.key); if (!shortcut) return null; if (rule.when !== undefined) { const whenAst = parseKeybindingWhenExpression(rule.when); if (!whenAst) return null; return { command: rule.command, shortcut, whenAst, }; } return { command: rule.command, shortcut, }; } export function compileResolvedKeybindingsConfig( config: KeybindingsConfig, ): ResolvedKeybindingsConfig { const compiled: Mutable<ResolvedKeybindingsConfig> = []; for (const rule of config) { const result = Schema.decodeExit(ResolvedKeybindingFromConfig)(rule); if (result._tag === "Success") { compiled.push(result.value); } } return compiled; } export const ResolvedKeybindingFromConfig = KeybindingRule.pipe( Schema.decodeTo( Schema.toType(ResolvedKeybindingRule), SchemaTransformation.transformOrFail({ decode: (rule) => Effect.succeed(compileResolvedKeybindingRule(rule)).pipe( Effect.filterOrFail( Predicate.isNotNull, () => new SchemaIssue.InvalidValue(Option.some(rule), { message: "Invalid keybinding rule", }), ), Effect.map((resolved) => resolved), ), encode: (resolved) => Effect.gen(function* () { const key = encodeShortcut(resolved.shortcut); if (!key) { return yield* Effect.fail( new SchemaIssue.InvalidValue(Option.some(resolved), { message: "Resolved shortcut cannot be encoded to key string", }), ); } const when = resolved.whenAst ? encodeWhenAst(resolved.whenAst) : undefined; return { key, command: resolved.command, when, }; }), }), ), ); export const ResolvedKeybindingsFromConfig = Schema.Array(ResolvedKeybindingFromConfig).check( Schema.isMaxLength(MAX_KEYBINDINGS_COUNT), ); function isSameKeybindingRule(left: KeybindingRule, right: KeybindingRule): boolean { return ( left.command === right.command && left.key === right.key && (left.when ?? undefined) === (right.when ?? undefined) ); } function keybindingShortcutContext(rule: KeybindingRule): string | null { const parsed = parseKeybindingShortcut(rule.key); if (!parsed) return null; const encoded = encodeShortcut(parsed); if (!encoded) return null; return `${encoded}\u0000${rule.when ?? ""}`; } function hasSameShortcutContext(left: KeybindingRule, right: KeybindingRule): boolean { const leftContext = keybindingShortcutContext(left); const rightContext = keybindingShortcutContext(right); if (!leftContext || !rightContext) return false; return leftContext === rightContext; } function encodeShortcut(shortcut: KeybindingShortcut): string | null { const modifiers: string[] = []; if (shortcut.modKey) modifiers.push("mod"); if (shortcut.metaKey) modifiers.push("meta"); if (shortcut.ctrlKey) modifiers.push("ctrl"); if (shortcut.altKey) modifiers.push("alt"); if (shortcut.shiftKey) modifiers.push("shift"); if (!shortcut.key) return null; if (shortcut.key !== "+" && shortcut.key.includes("+")) return null; const key = shortcut.key === " " ? "space" : shortcut.key; return [...modifiers, key].join("+"); } function encodeWhenAst(node: KeybindingWhenNode): string { switch (node.type) { case "identifier": return node.name; case "not": return `!(${encodeWhenAst(node.node)})`; case "and": return `(${encodeWhenAst(node.left)} && ${encodeWhenAst(node.right)})`; case "or": return `(${encodeWhenAst(node.left)} || ${encodeWhenAst(node.right)})`; } } const DEFAULT_RESOLVED_KEYBINDINGS = compileResolvedKeybindingsConfig(DEFAULT_KEYBINDINGS); const RawKeybindingsEntries = fromLenientJson(Schema.Array(Schema.Unknown)); const KeybindingsConfigJson = Schema.fromJsonString(KeybindingsConfig); const PrettyJsonString = SchemaGetter.parseJson<string>().compose( SchemaGetter.stringifyJson({ space: 2 }), ); const KeybindingsConfigPrettyJson = KeybindingsConfigJson.pipe( Schema.encode({ decode: PrettyJsonString, encode: PrettyJsonString, }), ); export interface KeybindingsConfigState { readonly keybindings: ResolvedKeybindingsConfig; readonly issues: readonly ServerConfigIssue[]; } export interface KeybindingsChangeEvent { readonly keybindings: ResolvedKeybindingsConfig; readonly issues: readonly ServerConfigIssue[]; } function trimIssueMessage(message: string): string { const trimmed = message.trim(); return trimmed.length > 0 ? trimmed : "Invalid keybindings configuration."; } function malformedConfigIssue(detail: string): ServerConfigIssue { return { kind: "keybindings.malformed-config", message: trimIssueMessage(detail), }; } function invalidEntryIssue(index: number, detail: string): ServerConfigIssue { return { kind: "keybindings.invalid-entry", index, message: trimIssueMessage(detail), }; } function mergeWithDefaultKeybindings(custom: ResolvedKeybindingsConfig): ResolvedKeybindingsConfig { if (custom.length === 0) { return [...DEFAULT_RESOLVED_KEYBINDINGS]; } const overriddenCommands = new Set(custom.map((binding) => binding.command)); const retainedDefaults = DEFAULT_RESOLVED_KEYBINDINGS.filter( (binding) => !overriddenCommands.has(binding.command), ); const merged = [...retainedDefaults, ...custom]; if (merged.length <= MAX_KEYBINDINGS_COUNT) { return merged; } // Keep the latest rules when the config exceeds max size; later rules have higher precedence. return merged.slice(-MAX_KEYBINDINGS_COUNT); } /** * KeybindingsShape - Service API for keybinding configuration operations. */ export interface KeybindingsShape { /** * Start the keybindings runtime and attach file watching. * * Safe to call multiple times. The first successful call establishes the * runtime; later calls await the same startup. */ readonly start: Effect.Effect<void, KeybindingsConfigError>; /** * Await keybindings runtime readiness. * * Readiness means the config directory exists, the watcher is attached, the * startup sync has completed, and the current snapshot has been loaded. */ readonly ready: Effect.Effect<void, KeybindingsConfigError>; /** * Ensure the on-disk keybindings file exists and includes all default * commands so newly-added defaults are backfilled on startup.