From 667c8c6166592f614ab8316756fc2f817e8b761e Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 26 Feb 2026 11:42:35 -0800 Subject: [PATCH] Bootstrap default keybindings and expose config path - Sync and persist missing default keybindings on server startup without overriding existing command mappings - Return `keybindingsConfigPath` from `server.getConfig` and add Settings UI action to open `keybindings.json` - Update server/keybindings tests to cover bootstrapping, conflict handling, and config responses --- apps/server/src/keybindings.test.ts | 87 ++++++++++++- apps/server/src/keybindings.ts | 167 ++++++++++++++++++++++--- apps/server/src/wsServer.test.ts | 78 ++++++------ apps/server/src/wsServer.ts | 13 +- apps/web/src/routes/_chat.settings.tsx | 62 +++++++++ packages/contracts/src/server.ts | 1 + 6 files changed, 355 insertions(+), 53 deletions(-) diff --git a/apps/server/src/keybindings.test.ts b/apps/server/src/keybindings.test.ts index 4dba9aa1baca1d53063ec5d78519c44002d27b41..ecc091a9bef3ddcfcf2a06d5e2dccef53d90d791 100644 --- a/apps/server/src/keybindings.test.ts +++ b/apps/server/src/keybindings.test.ts @@ -2,10 +2,11 @@ import { KeybindingCommand, KeybindingRule, KeybindingsConfig } from "@t3tools/c import { NodeServices } from "@effect/platform-node"; import { assert, it } from "@effect/vitest"; import { assertFailure } from "@effect/vitest/utils"; -import { Effect, FileSystem, Layer, Path, Schema } from "effect"; +import { Effect, FileSystem, Layer, Logger, Path, Schema } from "effect"; import { ServerConfig, type ServerConfigShape } from "./config"; import { + DEFAULT_KEYBINDINGS, Keybindings, KeybindingsConfigError, KeybindingsLive, @@ -149,6 +150,90 @@ it.layer(NodeServices.layer)("keybindings", (it) => { }), ); + it.effect("bootstraps default keybindings when config file is missing", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const { keybindingsConfigPath } = yield* ServerConfig; + assert.isFalse(yield* fs.exists(keybindingsConfigPath)); + + yield* Effect.gen(function* () { + const keybindings = yield* Keybindings; + yield* keybindings.syncDefaultKeybindingsOnStartup; + }); + + const persisted = yield* readKeybindingsConfig(keybindingsConfigPath); + assert.deepEqual(persisted, DEFAULT_KEYBINDINGS); + }).pipe(Effect.provide(makeKeybindingsLayer())), + ); + + it.effect( + "upserts missing default keybindings on startup without overriding existing command rules", + () => + Effect.gen(function* () { + const { keybindingsConfigPath } = yield* ServerConfig; + yield* writeKeybindingsConfig(keybindingsConfigPath, [ + { key: "mod+shift+t", command: "terminal.toggle" }, + { key: "mod+shift+r", command: "script.run-tests.run" }, + ]); + + yield* Effect.gen(function* () { + const keybindings = yield* Keybindings; + yield* keybindings.syncDefaultKeybindingsOnStartup; + }); + + const persisted = yield* readKeybindingsConfig(keybindingsConfigPath); + const byCommand = new Map(persisted.map((entry) => [entry.command, entry])); + + const persistedToggle = byCommand.get("terminal.toggle"); + assert.isNotNull(persistedToggle); + assert.equal(persistedToggle?.key, "mod+shift+t"); + assert.isFalse( + persisted.some((entry) => entry.command === "terminal.toggle" && entry.key === "mod+j"), + ); + + for (const defaultRule of DEFAULT_KEYBINDINGS) { + assert.isTrue(byCommand.has(defaultRule.command), `expected ${defaultRule.command}`); + } + assert.isTrue(byCommand.has("script.run-tests.run")); + }).pipe(Effect.provide(makeKeybindingsLayer())), + ); + + it.effect("skips conflicting default keybindings on startup and logs a detailed warning", () => { + const messages: string[] = []; + const logger = Logger.make(({ message }) => { + messages.push(String(message)); + }); + + return Effect.gen(function* () { + const { keybindingsConfigPath } = yield* ServerConfig; + yield* writeKeybindingsConfig(keybindingsConfigPath, [ + { key: "mod+j", command: "script.custom-action.run" }, + ]); + + yield* Effect.gen(function* () { + const keybindings = yield* Keybindings; + yield* keybindings.syncDefaultKeybindingsOnStartup; + }); + + const persisted = yield* readKeybindingsConfig(keybindingsConfigPath); + assert.isFalse(persisted.some((entry) => entry.command === "terminal.toggle")); + assert.isTrue(persisted.some((entry) => entry.command === "script.custom-action.run")); + + assert.isTrue( + messages.some((message) => + message.includes("skipping default keybinding due to shortcut conflict"), + ), + ); + }).pipe( + Effect.provide( + Layer.mergeAll( + makeKeybindingsLayer(), + Logger.layer([logger], { mergeWithExisting: false }), + ), + ), + ); + }); + it.effect("upserts custom keybindings to configured path", () => Effect.gen(function* () { const { keybindingsConfigPath } = yield* ServerConfig; diff --git a/apps/server/src/keybindings.ts b/apps/server/src/keybindings.ts index 95e526319885460a4dd9104dbb2f955ece8fc72b..4a5a060e712ad14bc89835838f76a19eb29db811 100644 --- a/apps/server/src/keybindings.ts +++ b/apps/server/src/keybindings.ts @@ -19,6 +19,7 @@ import { import { Mutable } from "effect/Types"; import { Array, + Cache, Cause, Effect, FileSystem, @@ -26,8 +27,8 @@ import { Layer, Option, Predicate, - Ref, Schema, + SchemaGetter, SchemaIssue, SchemaTransformation, ServiceMap, @@ -348,6 +349,29 @@ export const ResolvedKeybindingsFromConfig = Schema.Array(ResolvedKeybindingFrom 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"); @@ -378,6 +402,15 @@ const DEFAULT_RESOLVED_KEYBINDINGS = compileResolvedKeybindingsConfig(DEFAULT_KE const RawKeybindingsEntries = Schema.fromJsonString(Schema.Array(Schema.Unknown)); const KeybindingsConfigJson = Schema.fromJsonString(KeybindingsConfig); +const PrettyJsonString = SchemaGetter.parseJson().compose( + SchemaGetter.stringifyJson({ space: 2 }), +); +const KeybindingsConfigPrettyJson = KeybindingsConfigJson.pipe( + Schema.encode({ + decode: PrettyJsonString, + encode: PrettyJsonString, + }), +); function mergeWithDefaultKeybindings(custom: ResolvedKeybindingsConfig): ResolvedKeybindingsConfig { if (custom.length === 0) { @@ -402,6 +435,12 @@ function mergeWithDefaultKeybindings(custom: ResolvedKeybindingsConfig): Resolve * KeybindingsShape - Service API for keybinding configuration operations. */ export interface KeybindingsShape { + /** + * Ensure the on-disk keybindings file exists and includes all default + * commands so newly-added defaults are backfilled on startup. + */ + readonly syncDefaultKeybindingsOnStartup: Effect.Effect; + /** * Load and resolve keybindings from disk merged with defaults. * @@ -434,13 +473,28 @@ const makeKeybindings = Effect.gen(function* () { const { keybindingsConfigPath } = yield* ServerConfig; const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const cachedResolvedConfig = yield* Ref.make(null); const upsertSemaphore = yield* Semaphore.make(1); + const resolvedConfigCacheKey = "resolved" as const; + + const readConfigExists = fs.exists(keybindingsConfigPath).pipe( + Effect.mapError( + (cause) => + new KeybindingsConfigError({ + configPath: keybindingsConfigPath, + detail: "failed to access keybindings config", + cause, + }), + ), + ); const loadCustomKeybindingsConfig = Effect.fn(function* (): Effect.fn.Return< readonly KeybindingRule[], KeybindingsConfigError > { + if (!(yield* readConfigExists)) { + return []; + } + const rawConfig = yield* fs.readFileString(keybindingsConfigPath).pipe( Effect.flatMap(Schema.decodeEffect(RawKeybindingsEntries)), Effect.mapError( @@ -481,9 +535,8 @@ const makeKeybindings = Effect.gen(function* () { const writeConfigAtomically = (rules: readonly KeybindingRule[]) => { const tempPath = `${keybindingsConfigPath}.${process.pid}.${Date.now()}.tmp`; - return Schema.encodeEffect(KeybindingsConfigJson)( - rules, - ).pipe( + return Schema.encodeEffect(KeybindingsConfigPrettyJson)(rules).pipe( + Effect.map((encoded) => `${encoded}\n`), Effect.tap(() => fs.makeDirectory(path.dirname(keybindingsConfigPath), { recursive: true })), Effect.tap((encoded) => fs.writeFileString(tempPath, encoded)), Effect.flatMap(() => fs.rename(tempPath, keybindingsConfigPath)), @@ -498,18 +551,102 @@ const makeKeybindings = Effect.gen(function* () { ); }; - const loadResolvedFromCacheOrDisk = Effect.gen(function* () { - const cached = yield* Ref.get(cachedResolvedConfig); - if (cached) return cached; - const resolved = yield* loadCustomKeybindingsConfig().pipe( - Effect.map(compileResolvedKeybindingsConfig), - Effect.map(mergeWithDefaultKeybindings), - ); - yield* Ref.set(cachedResolvedConfig, resolved); - return resolved; + const loadResolvedFromDisk = loadCustomKeybindingsConfig().pipe( + Effect.map(compileResolvedKeybindingsConfig), + Effect.map(mergeWithDefaultKeybindings), + ); + + const resolvedConfigCache = yield* Cache.make< + typeof resolvedConfigCacheKey, + ResolvedKeybindingsConfig, + KeybindingsConfigError + >({ + capacity: 1, + lookup: () => loadResolvedFromDisk, }); + const syncDefaultKeybindingsOnStartup = upsertSemaphore.withPermits(1)( + Effect.gen(function* () { + const configExists = yield* readConfigExists; + if (!configExists) { + yield* writeConfigAtomically(DEFAULT_KEYBINDINGS); + yield* Cache.invalidate(resolvedConfigCache, resolvedConfigCacheKey); + return; + } + + const customConfig = yield* loadCustomKeybindingsConfig(); + const existingCommands = new Set(customConfig.map((entry) => entry.command)); + const missingDefaults: KeybindingRule[] = []; + const shortcutConflictWarnings: Array<{ + defaultCommand: KeybindingRule["command"]; + conflictingCommand: KeybindingRule["command"]; + key: string; + when: string | null; + }> = []; + for (const defaultRule of DEFAULT_KEYBINDINGS) { + if (existingCommands.has(defaultRule.command)) { + continue; + } + const conflictingEntry = customConfig.find((entry) => + hasSameShortcutContext(entry, defaultRule), + ); + if (conflictingEntry) { + shortcutConflictWarnings.push({ + defaultCommand: defaultRule.command, + conflictingCommand: conflictingEntry.command, + key: defaultRule.key, + when: defaultRule.when ?? null, + }); + continue; + } + missingDefaults.push(defaultRule); + } + for (const conflict of shortcutConflictWarnings) { + yield* Effect.logWarning("skipping default keybinding due to shortcut conflict", { + path: keybindingsConfigPath, + defaultCommand: conflict.defaultCommand, + conflictingCommand: conflict.conflictingCommand, + key: conflict.key, + when: conflict.when, + reason: "shortcut context already used by existing rule", + }); + } + if (missingDefaults.length === 0) { + yield* Cache.invalidate(resolvedConfigCache, resolvedConfigCacheKey); + return; + } + + const matchingDefaults = DEFAULT_KEYBINDINGS.filter((defaultRule) => + customConfig.some((entry) => isSameKeybindingRule(entry, defaultRule)), + ).map((rule) => rule.command); + if (matchingDefaults.length > 0) { + yield* Effect.logWarning("default keybinding rule already defined in user config", { + path: keybindingsConfigPath, + commands: matchingDefaults, + }); + } + + const nextConfig = [...customConfig, ...missingDefaults]; + const cappedConfig = + nextConfig.length > MAX_KEYBINDINGS_COUNT + ? nextConfig.slice(-MAX_KEYBINDINGS_COUNT) + : nextConfig; + if (nextConfig.length > MAX_KEYBINDINGS_COUNT) { + yield* Effect.logWarning("truncating keybindings config to max entries", { + path: keybindingsConfigPath, + maxEntries: MAX_KEYBINDINGS_COUNT, + }); + } + + yield* writeConfigAtomically(cappedConfig); + yield* Cache.invalidate(resolvedConfigCache, resolvedConfigCacheKey); + }), + ); + + const loadResolvedFromCacheOrDisk = Cache.get(resolvedConfigCache, resolvedConfigCacheKey); + return { + syncDefaultKeybindingsOnStartup, loadResolvedKeybindingsConfig: loadResolvedFromCacheOrDisk, upsertKeybindingRule: (rule) => upsertSemaphore.withPermits(1)( @@ -533,7 +670,7 @@ const makeKeybindings = Effect.gen(function* () { const nextResolved = mergeWithDefaultKeybindings( compileResolvedKeybindingsConfig(cappedConfig), ); - yield* Ref.set(cachedResolvedConfig, nextResolved); + yield* Cache.set(resolvedConfigCache, resolvedConfigCacheKey, nextResolved); return nextResolved; }), ), diff --git a/apps/server/src/wsServer.test.ts b/apps/server/src/wsServer.test.ts index 85cf527edb2867a7e83ac7ba3434dc3f3316b208..fc4a131b058b32020af8222706bfd156a32f6716 100644 --- a/apps/server/src/wsServer.test.ts +++ b/apps/server/src/wsServer.test.ts @@ -309,18 +309,6 @@ function compileKeybindings(bindings: KeybindingsConfig): ResolvedKeybindingsCon const DEFAULT_RESOLVED_KEYBINDINGS = compileKeybindings([...DEFAULT_KEYBINDINGS]); -function mergeWithDefaultsForTest(custom: KeybindingsConfig): ResolvedKeybindingsConfig { - if (custom.length === 0) { - return DEFAULT_RESOLVED_KEYBINDINGS; - } - - const overriddenCommands = new Set(custom.map((binding) => binding.command)); - const retainedDefaults = DEFAULT_KEYBINDINGS.filter( - (binding) => !overriddenCommands.has(binding.command), - ); - return compileKeybindings([...retainedDefaults, ...custom].slice(-256)); -} - describe("WebSocket Server", () => { let server: Http.Server | null = null; let serverScope: Scope.Closeable | null = null; @@ -510,7 +498,8 @@ describe("WebSocket Server", () => { it("responds to server.getConfig", async () => { const stateDir = makeTempDir("t3code-state-get-config-"); - fs.writeFileSync(path.join(stateDir, "keybindings.json"), "[]", "utf8"); + const keybindingsPath = path.join(stateDir, "keybindings.json"); + fs.writeFileSync(keybindingsPath, "[]", "utf8"); server = await createTestServer({ cwd: "/my/workspace", stateDir }); const addr = server.address(); @@ -526,10 +515,36 @@ describe("WebSocket Server", () => { expect(response.error).toBeUndefined(); expect(response.result).toEqual({ cwd: "/my/workspace", + keybindingsConfigPath: keybindingsPath, keybindings: DEFAULT_RESOLVED_KEYBINDINGS, }); }); + it("bootstraps default keybindings file when missing", async () => { + const stateDir = makeTempDir("t3code-state-bootstrap-keybindings-"); + const keybindingsPath = path.join(stateDir, "keybindings.json"); + expect(fs.existsSync(keybindingsPath)).toBe(false); + + server = await createTestServer({ cwd: "/my/workspace", stateDir }); + const addr = server.address(); + const port = typeof addr === "object" && addr !== null ? addr.port : 0; + + const ws = await connectWs(port); + connections.push(ws); + await waitForMessage(ws); + + const response = await sendRequest(ws, WS_METHODS.serverGetConfig); + expect(response.error).toBeUndefined(); + expect(response.result).toEqual({ + cwd: "/my/workspace", + keybindingsConfigPath: keybindingsPath, + keybindings: DEFAULT_RESOLVED_KEYBINDINGS, + }); + + const persistedConfig = JSON.parse(fs.readFileSync(keybindingsPath, "utf8")) as KeybindingsConfig; + expect(persistedConfig).toEqual(DEFAULT_KEYBINDINGS); + }); + it("routes shell.openInEditor through the injected open service", async () => { const openCalls: Array<{ cwd: string; editor: string }> = []; const openService: OpenShape = { @@ -558,8 +573,9 @@ describe("WebSocket Server", () => { it("reads keybindings from the configured state directory", async () => { const stateDir = makeTempDir("t3code-state-keybindings-"); + const keybindingsPath = path.join(stateDir, "keybindings.json"); fs.writeFileSync( - path.join(stateDir, "keybindings.json"), + keybindingsPath, JSON.stringify([ { key: "cmd+j", command: "terminal.toggle" }, { key: "mod+d", command: "terminal.split", when: "terminalFocus" }, @@ -578,13 +594,11 @@ describe("WebSocket Server", () => { const response = await sendRequest(ws, WS_METHODS.serverGetConfig); expect(response.error).toBeUndefined(); + const persistedConfig = JSON.parse(fs.readFileSync(keybindingsPath, "utf8")) as KeybindingsConfig; expect(response.result).toEqual({ cwd: "/my/workspace", - keybindings: mergeWithDefaultsForTest([ - { key: "cmd+j", command: "terminal.toggle" }, - { key: "mod+d", command: "terminal.split", when: "terminalFocus" }, - { key: "mod+n", command: "terminal.new", when: "terminalFocus" }, - ]), + keybindingsConfigPath: keybindingsPath, + keybindings: compileKeybindings(persistedConfig), }); }); @@ -610,30 +624,22 @@ describe("WebSocket Server", () => { command: "script.run-tests.run", }); expect(upsertResponse.error).toBeUndefined(); + const persistedConfig = JSON.parse(fs.readFileSync(keybindingsPath, "utf8")) as KeybindingsConfig; + const persistedCommands = new Set(persistedConfig.map((entry) => entry.command)); + for (const defaultRule of DEFAULT_KEYBINDINGS) { + expect(persistedCommands.has(defaultRule.command)).toBe(true); + } + expect(persistedCommands.has("script.run-tests.run")).toBe(true); expect(upsertResponse.result).toEqual({ - keybindings: mergeWithDefaultsForTest([ - { key: "mod+j", command: "terminal.toggle" }, - { key: "mod+shift+r", command: "script.run-tests.run" }, - ]), + keybindings: compileKeybindings(persistedConfig), }); - const persistedConfig = JSON.parse(fs.readFileSync(keybindingsPath, "utf8")) as Array<{ - key: string; - command: string; - }>; - expect(persistedConfig).toEqual([ - { key: "mod+j", command: "terminal.toggle" }, - { key: "mod+shift+r", command: "script.run-tests.run" }, - ]); - const configResponse = await sendRequest(ws, WS_METHODS.serverGetConfig); expect(configResponse.error).toBeUndefined(); expect(configResponse.result).toEqual({ cwd: "/my/workspace", - keybindings: mergeWithDefaultsForTest([ - { key: "mod+j", command: "terminal.toggle" }, - { key: "mod+shift+r", command: "script.run-tests.run" }, - ]), + keybindingsConfigPath: keybindingsPath, + keybindings: compileKeybindings(persistedConfig), }); }); diff --git a/apps/server/src/wsServer.ts b/apps/server/src/wsServer.ts index 9e5dd8fba95567f725cf402dd368174859fb7b50..281977ce31a5b20b3ea03a7cd1d80a9d6cd19019 100644 --- a/apps/server/src/wsServer.ts +++ b/apps/server/src/wsServer.ts @@ -172,6 +172,7 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< const { port, cwd, + keybindingsConfigPath, staticDir, devUrl, authToken, @@ -185,6 +186,16 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< const keybindingsManager = yield* Keybindings; const git = yield* GitCore; + yield* keybindingsManager.syncDefaultKeybindingsOnStartup.pipe( + Effect.catch((error) => + Effect.logWarning("failed to sync keybindings defaults on startup", { + path: error.configPath, + detail: error.detail, + cause: error.cause, + }), + ), + ); + const clients = yield* Ref.make(new Set()); const logger = createLogger("ws"); @@ -462,7 +473,7 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< case WS_METHODS.serverGetConfig: const keybindingsConfig = yield* keybindingsManager.loadResolvedKeybindingsConfig; - return { cwd, keybindings: keybindingsConfig }; + return { cwd, keybindingsConfigPath, keybindings: keybindingsConfig }; case WS_METHODS.serverUpsertKeybinding: { const body = stripRequestTag(request.body); diff --git a/apps/web/src/routes/_chat.settings.tsx b/apps/web/src/routes/_chat.settings.tsx index b2fac54abb13c476070922fe981ac35e3f2f7ffc..96137a9fda7c756ab06447ade624ad8ad4389c72 100644 --- a/apps/web/src/routes/_chat.settings.tsx +++ b/apps/web/src/routes/_chat.settings.tsx @@ -1,9 +1,14 @@ import { createFileRoute } from "@tanstack/react-router"; +import { useQuery } from "@tanstack/react-query"; +import { useCallback, useState } from "react"; import { useAppSettings } from "../appSettings"; import { isElectron } from "../env"; import { useTheme } from "../hooks/useTheme"; +import { serverConfigQueryOptions } from "../lib/serverReactQuery"; +import { ensureNativeApi } from "../nativeApi"; import { useStore } from "../store"; +import { preferredTerminalEditor } from "../terminal-links"; import { Button } from "../components/ui/button"; import { Input } from "../components/ui/input"; import { Switch } from "../components/ui/switch"; @@ -44,9 +49,30 @@ function SettingsRouteView() { const { theme, setTheme, resolvedTheme } = useTheme(); const { state, dispatch } = useStore(); const { settings, defaults, updateSettings } = useAppSettings(); + const serverConfigQuery = useQuery(serverConfigQueryOptions()); + const [isOpeningKeybindings, setIsOpeningKeybindings] = useState(false); + const [openKeybindingsError, setOpenKeybindingsError] = useState(null); const codexBinaryPath = settings.codexBinaryPath; const codexHomePath = settings.codexHomePath; + const keybindingsConfigPath = serverConfigQuery.data?.keybindingsConfigPath ?? null; + + const openKeybindingsFile = useCallback(() => { + if (!keybindingsConfigPath) return; + setOpenKeybindingsError(null); + setIsOpeningKeybindings(true); + const api = ensureNativeApi(); + void api.shell + .openInEditor(keybindingsConfigPath, preferredTerminalEditor()) + .catch((error) => { + setOpenKeybindingsError( + error instanceof Error ? error.message : "Unable to open keybindings file.", + ); + }) + .finally(() => { + setIsOpeningKeybindings(false); + }); + }, [keybindingsConfigPath]); return ( @@ -211,6 +237,42 @@ function SettingsRouteView() { +
+
+

Keybindings

+

+ Open the persisted keybindings.json file to edit advanced + bindings directly. +

+
+ +
+
+
+

Config file path

+

+ {keybindingsConfigPath ?? "Resolving keybindings path..."} +

+
+ +
+ +

+ Opens in your preferred editor selection. +

+ {openKeybindingsError ? ( +

{openKeybindingsError}

+ ) : null} +
+
+

Safety

diff --git a/packages/contracts/src/server.ts b/packages/contracts/src/server.ts index 774cf5d37c8a5f703bf26a2d6d1501fbf5ce5853..6e52f8b81d757b3a7f356988a5a97886a6d44980 100644 --- a/packages/contracts/src/server.ts +++ b/packages/contracts/src/server.ts @@ -4,6 +4,7 @@ import { KeybindingRule, ResolvedKeybindingsConfig } from "./keybindings"; export const ServerConfig = Schema.Struct({ cwd: TrimmedNonEmptyString, + keybindingsConfigPath: TrimmedNonEmptyString, keybindings: ResolvedKeybindingsConfig, }); export type ServerConfig = typeof ServerConfig.Type; -- 2.54.0