kanna-code 0.9.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,132 @@
1
+ import { afterEach, describe, expect, test } from "bun:test"
2
+ import { mkdtemp, rm, writeFile } from "node:fs/promises"
3
+ import { tmpdir } from "node:os"
4
+ import path from "node:path"
5
+ import { DEFAULT_KEYBINDINGS } from "../shared/types"
6
+ import { KeybindingsManager, normalizeKeybindings, readKeybindingsSnapshot } from "./keybindings"
7
+
8
+ let tempDirs: string[] = []
9
+ const TEST_FILE_PATH = "/tmp/kanna-test-keybindings.json"
10
+
11
+ afterEach(async () => {
12
+ await Promise.all(tempDirs.map((dir) => rm(dir, { recursive: true, force: true })))
13
+ tempDirs = []
14
+ })
15
+
16
+ async function createTempFilePath() {
17
+ const dir = await mkdtemp(path.join(tmpdir(), "kanna-keybindings-"))
18
+ tempDirs.push(dir)
19
+ return path.join(dir, "keybindings.json")
20
+ }
21
+
22
+ describe("normalizeKeybindings", () => {
23
+ test("falls back to defaults for invalid entries", () => {
24
+ const snapshot = normalizeKeybindings({
25
+ toggleEmbeddedTerminal: [],
26
+ toggleRightSidebar: "Ctrl+B",
27
+ }, TEST_FILE_PATH)
28
+
29
+ expect(snapshot.bindings).toEqual(DEFAULT_KEYBINDINGS)
30
+ expect(snapshot.warning).toContain("toggleEmbeddedTerminal")
31
+ expect(snapshot.warning).toContain("toggleRightSidebar")
32
+ })
33
+
34
+ test("keeps valid shortcut arrays", () => {
35
+ const snapshot = normalizeKeybindings({
36
+ toggleEmbeddedTerminal: [" Cmd+K ", "Ctrl+`"],
37
+ toggleRightSidebar: ["Ctrl+Shift+B"],
38
+ openInFinder: ["Cmd+Alt+F"],
39
+ openInEditor: ["Cmd+Shift+O"],
40
+ addSplitTerminal: ["Cmd+Shift+J"],
41
+ }, TEST_FILE_PATH)
42
+
43
+ expect(snapshot).toEqual({
44
+ bindings: {
45
+ toggleEmbeddedTerminal: ["cmd+k", "ctrl+`"],
46
+ toggleRightSidebar: ["ctrl+shift+b"],
47
+ openInFinder: ["cmd+alt+f"],
48
+ openInEditor: ["cmd+shift+o"],
49
+ addSplitTerminal: ["cmd+shift+j"],
50
+ },
51
+ warning: null,
52
+ filePathDisplay: TEST_FILE_PATH,
53
+ })
54
+ })
55
+ })
56
+
57
+ describe("readKeybindingsSnapshot", () => {
58
+ test("returns defaults when the file does not exist", async () => {
59
+ const filePath = await createTempFilePath()
60
+ const snapshot = await readKeybindingsSnapshot(filePath)
61
+ expect(snapshot).toEqual({
62
+ bindings: DEFAULT_KEYBINDINGS,
63
+ warning: null,
64
+ filePathDisplay: filePath,
65
+ })
66
+ })
67
+
68
+ test("returns a warning when the file contains invalid json", async () => {
69
+ const filePath = await createTempFilePath()
70
+ await writeFile(filePath, "{not-json", "utf8")
71
+
72
+ const snapshot = await readKeybindingsSnapshot(filePath)
73
+ expect(snapshot.bindings).toEqual(DEFAULT_KEYBINDINGS)
74
+ expect(snapshot.warning).toContain("invalid JSON")
75
+ })
76
+ })
77
+
78
+ describe("KeybindingsManager", () => {
79
+ test("creates the keybindings file with defaults during initialization", async () => {
80
+ const filePath = await createTempFilePath()
81
+ const manager = new KeybindingsManager(filePath)
82
+
83
+ await manager.initialize()
84
+
85
+ expect(await Bun.file(filePath).json()).toEqual(DEFAULT_KEYBINDINGS)
86
+ manager.dispose()
87
+ })
88
+
89
+ test("writes normalized bindings to disk", async () => {
90
+ const filePath = await createTempFilePath()
91
+ const manager = new KeybindingsManager(filePath)
92
+
93
+ await manager.initialize()
94
+ const snapshot = await manager.write({
95
+ toggleEmbeddedTerminal: ["Cmd+K"],
96
+ toggleRightSidebar: ["Ctrl+Shift+B"],
97
+ openInFinder: ["Cmd+Alt+F"],
98
+ openInEditor: ["Cmd+Shift+O"],
99
+ addSplitTerminal: ["Cmd+Shift+J"],
100
+ })
101
+
102
+ expect(snapshot).toEqual({
103
+ bindings: {
104
+ toggleEmbeddedTerminal: ["cmd+k"],
105
+ toggleRightSidebar: ["ctrl+shift+b"],
106
+ openInFinder: ["cmd+alt+f"],
107
+ openInEditor: ["cmd+shift+o"],
108
+ addSplitTerminal: ["cmd+shift+j"],
109
+ },
110
+ warning: null,
111
+ filePathDisplay: filePath,
112
+ })
113
+ expect(JSON.parse(await Bun.file(filePath).text())).toEqual(snapshot.bindings)
114
+
115
+ manager.dispose()
116
+ })
117
+
118
+ test("uses the runtime profile for the default keybindings path", () => {
119
+ const previous = process.env.KANNA_RUNTIME_PROFILE
120
+ process.env.KANNA_RUNTIME_PROFILE = "dev"
121
+
122
+ const manager = new KeybindingsManager()
123
+
124
+ expect(manager.filePath).toEndWith("/.kanna-dev/keybindings.json")
125
+
126
+ if (previous === undefined) {
127
+ delete process.env.KANNA_RUNTIME_PROFILE
128
+ } else {
129
+ process.env.KANNA_RUNTIME_PROFILE = previous
130
+ }
131
+ })
132
+ })
@@ -0,0 +1,175 @@
1
+ import { watch, type FSWatcher } from "node:fs"
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises"
3
+ import { homedir } from "node:os"
4
+ import path from "node:path"
5
+ import { getKeybindingsFilePath, LOG_PREFIX } from "../shared/branding"
6
+ import { DEFAULT_KEYBINDINGS, type KeybindingAction, type KeybindingsSnapshot } from "../shared/types"
7
+
8
+ const KEYBINDING_ACTIONS = Object.keys(DEFAULT_KEYBINDINGS) as KeybindingAction[]
9
+
10
+ type KeybindingsFile = Partial<Record<KeybindingAction, unknown>>
11
+
12
+ export class KeybindingsManager {
13
+ readonly filePath: string
14
+ private watcher: FSWatcher | null = null
15
+ private snapshot: KeybindingsSnapshot
16
+ private readonly listeners = new Set<(snapshot: KeybindingsSnapshot) => void>()
17
+
18
+ constructor(filePath = getKeybindingsFilePath(homedir())) {
19
+ this.filePath = filePath
20
+ this.snapshot = createDefaultSnapshot(this.filePath)
21
+ }
22
+
23
+ async initialize() {
24
+ await mkdir(path.dirname(this.filePath), { recursive: true })
25
+ const file = Bun.file(this.filePath)
26
+ if (!(await file.exists())) {
27
+ await writeFile(this.filePath, `${JSON.stringify(DEFAULT_KEYBINDINGS, null, 2)}\n`, "utf8")
28
+ }
29
+ await this.reload()
30
+ this.startWatching()
31
+ }
32
+
33
+ dispose() {
34
+ this.watcher?.close()
35
+ this.watcher = null
36
+ this.listeners.clear()
37
+ }
38
+
39
+ getSnapshot() {
40
+ return this.snapshot
41
+ }
42
+
43
+ onChange(listener: (snapshot: KeybindingsSnapshot) => void) {
44
+ this.listeners.add(listener)
45
+ return () => {
46
+ this.listeners.delete(listener)
47
+ }
48
+ }
49
+
50
+ async reload() {
51
+ const nextSnapshot = await readKeybindingsSnapshot(this.filePath)
52
+ this.setSnapshot(nextSnapshot)
53
+ }
54
+
55
+ async write(bindings: Partial<Record<KeybindingAction, string[]>>) {
56
+ const nextSnapshot = normalizeKeybindings(bindings, this.filePath)
57
+ await mkdir(path.dirname(this.filePath), { recursive: true })
58
+ await writeFile(this.filePath, `${JSON.stringify(nextSnapshot.bindings, null, 2)}\n`, "utf8")
59
+ this.setSnapshot(nextSnapshot)
60
+ return nextSnapshot
61
+ }
62
+
63
+ private setSnapshot(snapshot: KeybindingsSnapshot) {
64
+ this.snapshot = snapshot
65
+ for (const listener of this.listeners) {
66
+ listener(snapshot)
67
+ }
68
+ }
69
+
70
+ private startWatching() {
71
+ this.watcher?.close()
72
+ try {
73
+ this.watcher = watch(path.dirname(this.filePath), { persistent: false }, (_eventType, filename) => {
74
+ if (filename && filename !== path.basename(this.filePath)) {
75
+ return
76
+ }
77
+ void this.reload().catch((error: unknown) => {
78
+ console.warn(`${LOG_PREFIX} Failed to reload keybindings:`, error)
79
+ })
80
+ })
81
+ } catch (error) {
82
+ console.warn(`${LOG_PREFIX} Failed to watch keybindings file:`, error)
83
+ this.watcher = null
84
+ }
85
+ }
86
+ }
87
+
88
+ export async function readKeybindingsSnapshot(filePath: string) {
89
+ try {
90
+ const text = await readFile(filePath, "utf8")
91
+ if (!text.trim()) {
92
+ return createDefaultSnapshot(filePath, "Keybindings file was empty. Using defaults.")
93
+ }
94
+ const parsed = JSON.parse(text) as KeybindingsFile
95
+ return normalizeKeybindings(parsed, filePath)
96
+ } catch (error) {
97
+ if ((error as NodeJS.ErrnoException)?.code === "ENOENT") {
98
+ return createDefaultSnapshot(filePath)
99
+ }
100
+
101
+ if (error instanceof SyntaxError) {
102
+ return createDefaultSnapshot(filePath, "Keybindings file is invalid JSON. Using defaults.")
103
+ }
104
+
105
+ throw error
106
+ }
107
+ }
108
+
109
+ export function normalizeKeybindings(value: KeybindingsFile | null | undefined, filePath = getKeybindingsFilePath(homedir())): KeybindingsSnapshot {
110
+ const warnings: string[] = []
111
+ const source = value && typeof value === "object" && !Array.isArray(value)
112
+ ? value
113
+ : null
114
+
115
+ if (!source) {
116
+ return createDefaultSnapshot(filePath, "Keybindings file must contain a JSON object. Using defaults.")
117
+ }
118
+
119
+ const bindings = {} as Record<KeybindingAction, string[]>
120
+ for (const action of KEYBINDING_ACTIONS) {
121
+ const rawValue = source[action]
122
+ if (!Array.isArray(rawValue)) {
123
+ bindings[action] = [...DEFAULT_KEYBINDINGS[action]]
124
+ if (rawValue !== undefined) {
125
+ warnings.push(`${action} must be an array of shortcut strings`)
126
+ }
127
+ continue
128
+ }
129
+
130
+ const normalized = rawValue
131
+ .filter((entry): entry is string => typeof entry === "string")
132
+ .map((entry) => entry.trim())
133
+ .map((entry) => entry.toLowerCase())
134
+ .filter(Boolean)
135
+
136
+ if (normalized.length === 0) {
137
+ bindings[action] = [...DEFAULT_KEYBINDINGS[action]]
138
+ if (rawValue.length > 0 || source[action] !== undefined) {
139
+ warnings.push(`${action} did not contain any valid shortcut strings`)
140
+ }
141
+ continue
142
+ }
143
+
144
+ bindings[action] = normalized
145
+ }
146
+
147
+ return {
148
+ bindings,
149
+ warning: warnings.length > 0 ? `Some keybindings were reset to defaults: ${warnings.join("; ")}` : null,
150
+ filePathDisplay: formatDisplayPath(filePath),
151
+ }
152
+ }
153
+
154
+ function createDefaultSnapshot(filePath: string, warning: string | null = null): KeybindingsSnapshot {
155
+ return {
156
+ bindings: {
157
+ toggleEmbeddedTerminal: [...DEFAULT_KEYBINDINGS.toggleEmbeddedTerminal],
158
+ toggleRightSidebar: [...DEFAULT_KEYBINDINGS.toggleRightSidebar],
159
+ openInFinder: [...DEFAULT_KEYBINDINGS.openInFinder],
160
+ openInEditor: [...DEFAULT_KEYBINDINGS.openInEditor],
161
+ addSplitTerminal: [...DEFAULT_KEYBINDINGS.addSplitTerminal],
162
+ },
163
+ warning,
164
+ filePathDisplay: formatDisplayPath(filePath),
165
+ }
166
+ }
167
+
168
+ function formatDisplayPath(filePath: string) {
169
+ const homePath = homedir()
170
+ if (filePath === homePath) return "~"
171
+ if (filePath.startsWith(`${homePath}${path.sep}`)) {
172
+ return `~${filePath.slice(homePath.length)}`
173
+ }
174
+ return filePath
175
+ }
@@ -3,7 +3,7 @@ import { APP_NAME } from "../shared/branding"
3
3
  import { EventStore } from "./event-store"
4
4
  import { AgentCoordinator } from "./agent"
5
5
  import { discoverProjects, type DiscoveredProject } from "./discovery"
6
- import { FileTreeManager } from "./file-tree-manager"
6
+ import { KeybindingsManager } from "./keybindings"
7
7
  import { getMachineDisplayName } from "./machine-name"
8
8
  import { TerminalManager } from "./terminal-manager"
9
9
  import { createWsRouter, type ClientState } from "./ws-router"
@@ -31,9 +31,8 @@ export async function startKannaServer(options: StartKannaServerOptions = {}) {
31
31
  let server: ReturnType<typeof Bun.serve<ClientState>>
32
32
  let router: ReturnType<typeof createWsRouter>
33
33
  const terminals = new TerminalManager()
34
- const fileTree = new FileTreeManager({
35
- getProject: (projectId) => store.getProject(projectId),
36
- })
34
+ const keybindings = new KeybindingsManager()
35
+ await keybindings.initialize()
37
36
  const agent = new AgentCoordinator({
38
37
  store,
39
38
  onStateChange: () => {
@@ -44,7 +43,7 @@ export async function startKannaServer(options: StartKannaServerOptions = {}) {
44
43
  store,
45
44
  agent,
46
45
  terminals,
47
- fileTree,
46
+ keybindings,
48
47
  refreshDiscovery,
49
48
  getDiscoveredProjects: () => discoveredProjects,
50
49
  machineDisplayName,
@@ -106,7 +105,7 @@ export async function startKannaServer(options: StartKannaServerOptions = {}) {
106
105
  await agent.cancel(chatId)
107
106
  }
108
107
  router.dispose()
109
- fileTree.dispose()
108
+ keybindings.dispose()
110
109
  terminals.closeAll()
111
110
  await store.compact()
112
111
  server.stop(true)
@@ -1,4 +1,5 @@
1
1
  import { describe, expect, test } from "bun:test"
2
+ import type { KeybindingsSnapshot } from "../shared/types"
2
3
  import { PROTOCOL_VERSION } from "../shared/types"
3
4
  import { createEmptyState } from "./events"
4
5
  import { createWsRouter } from "./ws-router"
@@ -14,6 +15,18 @@ class FakeWebSocket {
14
15
  }
15
16
  }
16
17
 
18
+ const DEFAULT_KEYBINDINGS_SNAPSHOT: KeybindingsSnapshot = {
19
+ bindings: {
20
+ toggleEmbeddedTerminal: ["cmd+j", "ctrl+`"],
21
+ toggleRightSidebar: ["ctrl+b"],
22
+ openInFinder: ["cmd+alt+f"],
23
+ openInEditor: ["cmd+shift+o"],
24
+ addSplitTerminal: ["cmd+shift+j"],
25
+ },
26
+ warning: null,
27
+ filePathDisplay: "~/.kanna/keybindings.json",
28
+ }
29
+
17
30
  describe("ws-router", () => {
18
31
  test("acks system.ping without broadcasting snapshots", () => {
19
32
  const router = createWsRouter({
@@ -23,9 +36,9 @@ describe("ws-router", () => {
23
36
  getSnapshot: () => null,
24
37
  onEvent: () => () => {},
25
38
  } as never,
26
- fileTree: {
27
- getSnapshot: () => ({ projectId: "project-1", rootPath: "/tmp/project-1", pageSize: 200, supportsRealtime: true }),
28
- onInvalidate: () => () => {},
39
+ keybindings: {
40
+ getSnapshot: () => DEFAULT_KEYBINDINGS_SNAPSHOT,
41
+ onChange: () => () => {},
29
42
  } as never,
30
43
  refreshDiscovery: async () => [],
31
44
  getDiscoveredProjects: () => [],
@@ -62,9 +75,9 @@ describe("ws-router", () => {
62
75
  onEvent: () => () => {},
63
76
  write: () => {},
64
77
  } as never,
65
- fileTree: {
66
- getSnapshot: () => ({ projectId: "project-1", rootPath: "/tmp/project-1", pageSize: 200, supportsRealtime: true }),
67
- onInvalidate: () => () => {},
78
+ keybindings: {
79
+ getSnapshot: () => DEFAULT_KEYBINDINGS_SNAPSHOT,
80
+ onChange: () => () => {},
68
81
  } as never,
69
82
  refreshDiscovery: async () => [],
70
83
  getDiscoveredProjects: () => [],
@@ -96,31 +109,7 @@ describe("ws-router", () => {
96
109
  ])
97
110
  })
98
111
 
99
- test("subscribes and unsubscribes file-tree topics and acks directory reads", async () => {
100
- const fileTree = {
101
- subscribeCalls: [] as string[],
102
- unsubscribeCalls: [] as string[],
103
- subscribe(projectId: string) {
104
- this.subscribeCalls.push(projectId)
105
- },
106
- unsubscribe(projectId: string) {
107
- this.unsubscribeCalls.push(projectId)
108
- },
109
- getSnapshot: (projectId: string) => ({
110
- projectId,
111
- rootPath: "/tmp/project-1",
112
- pageSize: 200,
113
- supportsRealtime: true as const,
114
- }),
115
- readDirectory: async () => ({
116
- directoryPath: "",
117
- entries: [],
118
- nextCursor: null,
119
- hasMore: false,
120
- }),
121
- onInvalidate: () => () => {},
122
- }
123
-
112
+ test("subscribes and unsubscribes chat topics", () => {
124
113
  const router = createWsRouter({
125
114
  store: { state: createEmptyState() } as never,
126
115
  agent: { getActiveStatuses: () => new Map() } as never,
@@ -128,7 +117,10 @@ describe("ws-router", () => {
128
117
  getSnapshot: () => null,
129
118
  onEvent: () => () => {},
130
119
  } as never,
131
- fileTree: fileTree as never,
120
+ keybindings: {
121
+ getSnapshot: () => DEFAULT_KEYBINDINGS_SNAPSHOT,
122
+ onChange: () => () => {},
123
+ } as never,
132
124
  refreshDiscovery: async () => [],
133
125
  getDiscoveredProjects: () => [],
134
126
  machineDisplayName: "Local Machine",
@@ -140,24 +132,18 @@ describe("ws-router", () => {
140
132
  JSON.stringify({
141
133
  v: 1,
142
134
  type: "subscribe",
143
- id: "tree-sub-1",
144
- topic: { type: "file-tree", projectId: "project-1" },
135
+ id: "chat-sub-1",
136
+ topic: { type: "chat", chatId: "chat-1" },
145
137
  })
146
138
  )
147
139
 
148
- expect(fileTree.subscribeCalls).toEqual(["project-1"])
149
140
  expect(ws.sent[0]).toEqual({
150
141
  v: PROTOCOL_VERSION,
151
142
  type: "snapshot",
152
- id: "tree-sub-1",
143
+ id: "chat-sub-1",
153
144
  snapshot: {
154
- type: "file-tree",
155
- data: {
156
- projectId: "project-1",
157
- rootPath: "/tmp/project-1",
158
- pageSize: 200,
159
- supportsRealtime: true,
160
- },
145
+ type: "chat",
146
+ data: null,
161
147
  },
162
148
  })
163
149
 
@@ -165,26 +151,63 @@ describe("ws-router", () => {
165
151
  ws as never,
166
152
  JSON.stringify({
167
153
  v: 1,
168
- type: "command",
169
- id: "tree-read-1",
170
- command: {
171
- type: "file-tree.readDirectory",
172
- projectId: "project-1",
173
- directoryPath: "",
174
- },
154
+ type: "unsubscribe",
155
+ id: "chat-sub-1",
175
156
  })
176
157
  )
177
158
 
178
- await Promise.resolve()
179
159
  expect(ws.sent[1]).toEqual({
180
160
  v: PROTOCOL_VERSION,
181
161
  type: "ack",
182
- id: "tree-read-1",
183
- result: {
184
- directoryPath: "",
185
- entries: [],
186
- nextCursor: null,
187
- hasMore: false,
162
+ id: "chat-sub-1",
163
+ })
164
+ })
165
+
166
+ test("subscribes to keybindings snapshots and writes keybindings through the router", async () => {
167
+ const initialSnapshot: KeybindingsSnapshot = DEFAULT_KEYBINDINGS_SNAPSHOT
168
+ const keybindings = {
169
+ snapshot: initialSnapshot,
170
+ getSnapshot() {
171
+ return this.snapshot
172
+ },
173
+ onChange: () => () => {},
174
+ async write(bindings: KeybindingsSnapshot["bindings"]) {
175
+ this.snapshot = { bindings, warning: null, filePathDisplay: "~/.kanna/keybindings.json" }
176
+ return this.snapshot
177
+ },
178
+ }
179
+
180
+ const router = createWsRouter({
181
+ store: { state: createEmptyState() } as never,
182
+ agent: { getActiveStatuses: () => new Map() } as never,
183
+ terminals: {
184
+ getSnapshot: () => null,
185
+ onEvent: () => () => {},
186
+ } as never,
187
+ keybindings: keybindings as never,
188
+ refreshDiscovery: async () => [],
189
+ getDiscoveredProjects: () => [],
190
+ machineDisplayName: "Local Machine",
191
+ })
192
+ const ws = new FakeWebSocket()
193
+
194
+ router.handleMessage(
195
+ ws as never,
196
+ JSON.stringify({
197
+ v: 1,
198
+ type: "subscribe",
199
+ id: "keybindings-sub-1",
200
+ topic: { type: "keybindings" },
201
+ })
202
+ )
203
+
204
+ expect(ws.sent[0]).toEqual({
205
+ v: PROTOCOL_VERSION,
206
+ type: "snapshot",
207
+ id: "keybindings-sub-1",
208
+ snapshot: {
209
+ type: "keybindings",
210
+ data: keybindings.snapshot,
188
211
  },
189
212
  })
190
213
 
@@ -192,16 +215,37 @@ describe("ws-router", () => {
192
215
  ws as never,
193
216
  JSON.stringify({
194
217
  v: 1,
195
- type: "unsubscribe",
196
- id: "tree-sub-1",
218
+ type: "command",
219
+ id: "keybindings-write-1",
220
+ command: {
221
+ type: "settings.writeKeybindings",
222
+ bindings: {
223
+ toggleEmbeddedTerminal: ["cmd+k"],
224
+ toggleRightSidebar: ["ctrl+shift+b"],
225
+ openInFinder: ["cmd+shift+g"],
226
+ openInEditor: ["cmd+shift+p"],
227
+ addSplitTerminal: ["cmd+alt+j"],
228
+ },
229
+ },
197
230
  })
198
231
  )
199
232
 
200
- expect(fileTree.unsubscribeCalls).toEqual(["project-1"])
201
- expect(ws.sent[2]).toEqual({
233
+ await Promise.resolve()
234
+ expect(ws.sent[1]).toEqual({
202
235
  v: PROTOCOL_VERSION,
203
236
  type: "ack",
204
- id: "tree-sub-1",
205
- })
237
+ id: "keybindings-write-1",
238
+ result: {
239
+ bindings: {
240
+ toggleEmbeddedTerminal: ["cmd+k"],
241
+ toggleRightSidebar: ["ctrl+shift+b"],
242
+ openInFinder: ["cmd+shift+g"],
243
+ openInEditor: ["cmd+shift+p"],
244
+ addSplitTerminal: ["cmd+alt+j"],
245
+ },
246
+ warning: null,
247
+ filePathDisplay: "~/.kanna/keybindings.json",
248
+ },
249
+ })
206
250
  })
207
251
  })