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.
@@ -6,7 +6,7 @@ import type { AgentCoordinator } from "./agent"
6
6
  import type { DiscoveredProject } from "./discovery"
7
7
  import { EventStore } from "./event-store"
8
8
  import { openExternal } from "./external-open"
9
- import { FileTreeManager } from "./file-tree-manager"
9
+ import { KeybindingsManager } from "./keybindings"
10
10
  import { ensureProjectDirectory } from "./paths"
11
11
  import { TerminalManager } from "./terminal-manager"
12
12
  import { deriveChatSnapshot, deriveLocalProjectsSnapshot, deriveSidebarData } from "./read-models"
@@ -19,7 +19,7 @@ interface CreateWsRouterArgs {
19
19
  store: EventStore
20
20
  agent: AgentCoordinator
21
21
  terminals: TerminalManager
22
- fileTree: FileTreeManager
22
+ keybindings: KeybindingsManager
23
23
  refreshDiscovery: () => Promise<DiscoveredProject[]>
24
24
  getDiscoveredProjects: () => DiscoveredProject[]
25
25
  machineDisplayName: string
@@ -33,7 +33,7 @@ export function createWsRouter({
33
33
  store,
34
34
  agent,
35
35
  terminals,
36
- fileTree,
36
+ keybindings,
37
37
  refreshDiscovery,
38
38
  getDiscoveredProjects,
39
39
  machineDisplayName,
@@ -68,26 +68,26 @@ export function createWsRouter({
68
68
  }
69
69
  }
70
70
 
71
- if (topic.type === "terminal") {
71
+ if (topic.type === "keybindings") {
72
72
  return {
73
73
  v: PROTOCOL_VERSION,
74
74
  type: "snapshot",
75
75
  id,
76
76
  snapshot: {
77
- type: "terminal",
78
- data: terminals.getSnapshot(topic.terminalId),
77
+ type: "keybindings",
78
+ data: keybindings.getSnapshot(),
79
79
  },
80
80
  }
81
81
  }
82
82
 
83
- if (topic.type === "file-tree") {
83
+ if (topic.type === "terminal") {
84
84
  return {
85
85
  v: PROTOCOL_VERSION,
86
86
  type: "snapshot",
87
87
  id,
88
88
  snapshot: {
89
- type: "file-tree",
90
- data: fileTree.getSnapshot(topic.projectId),
89
+ type: "terminal",
90
+ data: terminals.getSnapshot(topic.terminalId),
91
91
  },
92
92
  }
93
93
  }
@@ -142,16 +142,11 @@ export function createWsRouter({
142
142
  pushTerminalEvent(event.terminalId, event)
143
143
  })
144
144
 
145
- const disposeFileTreeEvents = fileTree.onInvalidate((event) => {
145
+ const disposeKeybindingEvents = keybindings.onChange(() => {
146
146
  for (const ws of sockets) {
147
147
  for (const [id, topic] of ws.data.subscriptions.entries()) {
148
- if (topic.type !== "file-tree" || topic.projectId !== event.projectId) continue
149
- send(ws, {
150
- v: PROTOCOL_VERSION,
151
- type: "event",
152
- id,
153
- event,
154
- })
148
+ if (topic.type !== "keybindings") continue
149
+ send(ws, createEnvelope(id, topic))
155
150
  }
156
151
  }
157
152
  })
@@ -164,6 +159,15 @@ export function createWsRouter({
164
159
  send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
165
160
  return
166
161
  }
162
+ case "settings.readKeybindings": {
163
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: keybindings.getSnapshot() })
164
+ return
165
+ }
166
+ case "settings.writeKeybindings": {
167
+ const snapshot = await keybindings.write(command.bindings)
168
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: snapshot })
169
+ return
170
+ }
167
171
  case "project.open": {
168
172
  await ensureProjectDirectory(command.localPath)
169
173
  const project = await store.openProject(command.localPath)
@@ -257,11 +261,6 @@ export function createWsRouter({
257
261
  pushTerminalSnapshot(command.terminalId)
258
262
  return
259
263
  }
260
- case "file-tree.readDirectory": {
261
- const result = await fileTree.readDirectory(command)
262
- send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
263
- return
264
- }
265
264
  }
266
265
 
267
266
  broadcastSnapshots()
@@ -276,11 +275,6 @@ export function createWsRouter({
276
275
  sockets.add(ws)
277
276
  },
278
277
  handleClose(ws: ServerWebSocket<ClientState>) {
279
- for (const topic of ws.data.subscriptions.values()) {
280
- if (topic.type === "file-tree") {
281
- fileTree.unsubscribe(topic.projectId)
282
- }
283
- }
284
278
  sockets.delete(ws)
285
279
  },
286
280
  broadcastSnapshots,
@@ -300,9 +294,6 @@ export function createWsRouter({
300
294
 
301
295
  if (parsed.type === "subscribe") {
302
296
  ws.data.subscriptions.set(parsed.id, parsed.topic)
303
- if (parsed.topic.type === "file-tree") {
304
- fileTree.subscribe(parsed.topic.projectId)
305
- }
306
297
  if (parsed.topic.type === "local-projects") {
307
298
  void refreshDiscovery().then(() => {
308
299
  if (ws.data.subscriptions.has(parsed.id)) {
@@ -315,11 +306,7 @@ export function createWsRouter({
315
306
  }
316
307
 
317
308
  if (parsed.type === "unsubscribe") {
318
- const topic = ws.data.subscriptions.get(parsed.id)
319
309
  ws.data.subscriptions.delete(parsed.id)
320
- if (topic?.type === "file-tree") {
321
- fileTree.unsubscribe(topic.projectId)
322
- }
323
310
  send(ws, { v: PROTOCOL_VERSION, type: "ack", id: parsed.id })
324
311
  return
325
312
  }
@@ -328,7 +315,7 @@ export function createWsRouter({
328
315
  },
329
316
  dispose() {
330
317
  disposeTerminalEvents()
331
- disposeFileTreeEvents()
318
+ disposeKeybindingEvents()
332
319
  },
333
320
  }
334
321
  }
@@ -0,0 +1,31 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import {
3
+ getDataDir,
4
+ getDataDirDisplay,
5
+ getDataRootName,
6
+ getKeybindingsFilePath,
7
+ getKeybindingsFilePathDisplay,
8
+ getRuntimeProfile,
9
+ } from "./branding"
10
+
11
+ describe("runtime profile helpers", () => {
12
+ test("defaults to the prod profile when unset", () => {
13
+ expect(getRuntimeProfile({})).toBe("prod")
14
+ expect(getDataRootName({})).toBe(".kanna")
15
+ expect(getDataDir("/tmp/home", {})).toBe("/tmp/home/.kanna/data")
16
+ expect(getDataDirDisplay({})).toBe("~/.kanna/data")
17
+ expect(getKeybindingsFilePath("/tmp/home", {})).toBe("/tmp/home/.kanna/keybindings.json")
18
+ expect(getKeybindingsFilePathDisplay({})).toBe("~/.kanna/keybindings.json")
19
+ })
20
+
21
+ test("switches to dev paths for the dev profile", () => {
22
+ const env = { KANNA_RUNTIME_PROFILE: "dev" }
23
+
24
+ expect(getRuntimeProfile(env)).toBe("dev")
25
+ expect(getDataRootName(env)).toBe(".kanna-dev")
26
+ expect(getDataDir("/tmp/home", env)).toBe("/tmp/home/.kanna-dev/data")
27
+ expect(getDataDirDisplay(env)).toBe("~/.kanna-dev/data")
28
+ expect(getKeybindingsFilePath("/tmp/home", env)).toBe("/tmp/home/.kanna-dev/keybindings.json")
29
+ expect(getKeybindingsFilePathDisplay(env)).toBe("~/.kanna-dev/keybindings.json")
30
+ })
31
+ })
@@ -1,23 +1,58 @@
1
1
  export const APP_NAME = "Kanna"
2
2
  export const CLI_COMMAND = "kanna"
3
3
  export const DATA_ROOT_NAME = ".kanna"
4
+ export const DEV_DATA_ROOT_NAME = ".kanna-dev"
4
5
  export const PACKAGE_NAME = "kanna-code"
6
+ export const RUNTIME_PROFILE_ENV_VAR = "KANNA_RUNTIME_PROFILE"
5
7
  // Read version from package.json — JSON import works in both Bun and Vite
6
8
  import pkg from "../../package.json"
7
9
  export const SDK_CLIENT_APP = `kanna/${pkg.version}`
8
10
  export const LOG_PREFIX = "[kanna]"
9
11
  export const DEFAULT_NEW_PROJECT_ROOT = `~/${APP_NAME}`
10
12
 
11
- export function getDataRootName() {
12
- return DATA_ROOT_NAME
13
+ export type RuntimeProfile = "dev" | "prod"
14
+
15
+ type RuntimeEnv = Record<string, string | undefined> | undefined
16
+
17
+ function getRuntimeEnv(): RuntimeEnv {
18
+ const candidate = globalThis as typeof globalThis & {
19
+ process?: {
20
+ env?: Record<string, string | undefined>
21
+ }
22
+ }
23
+ return candidate.process?.env
24
+ }
25
+
26
+ export function getRuntimeProfile(env: RuntimeEnv = getRuntimeEnv()): RuntimeProfile {
27
+ return env?.[RUNTIME_PROFILE_ENV_VAR]?.trim().toLowerCase() === "dev" ? "dev" : "prod"
28
+ }
29
+
30
+ export function getDataRootName(env: RuntimeEnv = getRuntimeEnv()) {
31
+ return getRuntimeProfile(env) === "dev" ? DEV_DATA_ROOT_NAME : DATA_ROOT_NAME
32
+ }
33
+
34
+ export function getDataRootDir(homeDir: string, env: RuntimeEnv = getRuntimeEnv()) {
35
+ return `${homeDir}/${getDataRootName(env)}`
36
+ }
37
+
38
+ export function getDataRootDirDisplay(env: RuntimeEnv = getRuntimeEnv()) {
39
+ return `~/${getDataRootName(env)}`
40
+ }
41
+
42
+ export function getDataDir(homeDir: string, env: RuntimeEnv = getRuntimeEnv()) {
43
+ return `${getDataRootDir(homeDir, env)}/data`
44
+ }
45
+
46
+ export function getDataDirDisplay(env: RuntimeEnv = getRuntimeEnv()) {
47
+ return `${getDataRootDirDisplay(env)}/data`
13
48
  }
14
49
 
15
- export function getDataDir(homeDir: string) {
16
- return `${homeDir}/${DATA_ROOT_NAME}/data`
50
+ export function getKeybindingsFilePath(homeDir: string, env: RuntimeEnv = getRuntimeEnv()) {
51
+ return `${getDataRootDir(homeDir, env)}/keybindings.json`
17
52
  }
18
53
 
19
- export function getDataDirDisplay() {
20
- return `~/${DATA_ROOT_NAME.slice(1)}/data`
54
+ export function getKeybindingsFilePathDisplay(env: RuntimeEnv = getRuntimeEnv()) {
55
+ return `${getDataRootDirDisplay(env)}/keybindings.json`
21
56
  }
22
57
 
23
58
  export function getCliInvocation(arg?: string) {
@@ -1,8 +1,7 @@
1
1
  import type {
2
2
  AgentProvider,
3
3
  ChatSnapshot,
4
- FileTreeDirectoryPage,
5
- FileTreeSnapshot,
4
+ KeybindingsSnapshot,
6
5
  LocalProjectsSnapshot,
7
6
  ModelOptions,
8
7
  SidebarData,
@@ -18,7 +17,7 @@ export interface EditorOpenSettings {
18
17
  export type SubscriptionTopic =
19
18
  | { type: "sidebar" }
20
19
  | { type: "local-projects" }
21
- | { type: "file-tree"; projectId: string }
20
+ | { type: "keybindings" }
22
21
  | { type: "chat"; chatId: string }
23
22
  | { type: "terminal"; terminalId: string }
24
23
 
@@ -45,6 +44,8 @@ export type ClientCommand =
45
44
  | { type: "project.create"; localPath: string; title: string }
46
45
  | { type: "project.remove"; projectId: string }
47
46
  | { type: "system.ping" }
47
+ | { type: "settings.readKeybindings" }
48
+ | { type: "settings.writeKeybindings"; bindings: KeybindingsSnapshot["bindings"] }
48
49
  | {
49
50
  type: "system.openExternal"
50
51
  localPath: string
@@ -73,13 +74,6 @@ export type ClientCommand =
73
74
  | { type: "terminal.input"; terminalId: string; data: string }
74
75
  | { type: "terminal.resize"; terminalId: string; cols: number; rows: number }
75
76
  | { type: "terminal.close"; terminalId: string }
76
- | {
77
- type: "file-tree.readDirectory"
78
- projectId: string
79
- directoryPath: string
80
- cursor?: string
81
- limit?: number
82
- }
83
77
 
84
78
  export type ClientEnvelope =
85
79
  | { v: 1; type: "subscribe"; id: string; topic: SubscriptionTopic }
@@ -89,24 +83,16 @@ export type ClientEnvelope =
89
83
  export type ServerSnapshot =
90
84
  | { type: "sidebar"; data: SidebarData }
91
85
  | { type: "local-projects"; data: LocalProjectsSnapshot }
92
- | { type: "file-tree"; data: FileTreeSnapshot }
86
+ | { type: "keybindings"; data: KeybindingsSnapshot }
93
87
  | { type: "chat"; data: ChatSnapshot | null }
94
88
  | { type: "terminal"; data: TerminalSnapshot | null }
95
89
 
96
- export type FileTreeEvent = {
97
- type: "file-tree.invalidate"
98
- projectId: string
99
- directoryPaths: string[]
100
- }
101
-
102
90
  export type ServerEnvelope =
103
91
  | { v: 1; type: "snapshot"; id: string; snapshot: ServerSnapshot }
104
- | { v: 1; type: "event"; id: string; event: TerminalEvent | FileTreeEvent }
92
+ | { v: 1; type: "event"; id: string; event: TerminalEvent }
105
93
  | { v: 1; type: "ack"; id: string; result?: unknown }
106
94
  | { v: 1; type: "error"; id?: string; message: string }
107
95
 
108
- export type FileTreeReadDirectoryResult = FileTreeDirectoryPage
109
-
110
96
  export function isClientEnvelope(value: unknown): value is ClientEnvelope {
111
97
  if (!value || typeof value !== "object") return false
112
98
  const candidate = value as Partial<ClientEnvelope>
@@ -167,28 +167,25 @@ export interface LocalProjectsSnapshot {
167
167
  projects: LocalProjectSummary[]
168
168
  }
169
169
 
170
- export type FileTreeEntryKind = "file" | "directory" | "symlink"
170
+ export type KeybindingAction =
171
+ | "toggleEmbeddedTerminal"
172
+ | "toggleRightSidebar"
173
+ | "openInFinder"
174
+ | "openInEditor"
175
+ | "addSplitTerminal"
171
176
 
172
- export interface FileTreeEntry {
173
- name: string
174
- relativePath: string
175
- kind: FileTreeEntryKind
176
- extension?: string
177
+ export const DEFAULT_KEYBINDINGS: Record<KeybindingAction, string[]> = {
178
+ toggleEmbeddedTerminal: ["cmd+j", "ctrl+`"],
179
+ toggleRightSidebar: ["cmd+b", "ctrl+b"],
180
+ openInFinder: ["cmd+alt+f", "ctrl+alt+f"],
181
+ openInEditor: ["cmd+shift+o", "ctrl+shift+o"],
182
+ addSplitTerminal: ["cmd+/", "ctrl+/"],
177
183
  }
178
184
 
179
- export interface FileTreeDirectoryPage {
180
- directoryPath: string
181
- entries: FileTreeEntry[]
182
- nextCursor: string | null
183
- hasMore: boolean
184
- error?: string
185
- }
186
-
187
- export interface FileTreeSnapshot {
188
- projectId: string
189
- rootPath: string
190
- pageSize: number
191
- supportsRealtime: true
185
+ export interface KeybindingsSnapshot {
186
+ bindings: Record<KeybindingAction, string[]>
187
+ warning: string | null
188
+ filePathDisplay: string
192
189
  }
193
190
 
194
191
  export interface McpServerInfo {
@@ -244,46 +241,46 @@ interface ToolCallBase<TKind extends string, TInput> {
244
241
  }
245
242
 
246
243
  export interface AskUserQuestionToolCall
247
- extends ToolCallBase<"ask_user_question", { questions: AskUserQuestionItem[] }> {}
244
+ extends ToolCallBase<"ask_user_question", { questions: AskUserQuestionItem[] }> { }
248
245
 
249
246
  export interface ExitPlanModeToolCall
250
- extends ToolCallBase<"exit_plan_mode", { plan?: string; summary?: string }> {}
247
+ extends ToolCallBase<"exit_plan_mode", { plan?: string; summary?: string }> { }
251
248
 
252
249
  export interface TodoWriteToolCall
253
- extends ToolCallBase<"todo_write", { todos: TodoItem[] }> {}
250
+ extends ToolCallBase<"todo_write", { todos: TodoItem[] }> { }
254
251
 
255
252
  export interface SkillToolCall
256
- extends ToolCallBase<"skill", { skill: string }> {}
253
+ extends ToolCallBase<"skill", { skill: string }> { }
257
254
 
258
255
  export interface GlobToolCall
259
- extends ToolCallBase<"glob", { pattern: string }> {}
256
+ extends ToolCallBase<"glob", { pattern: string }> { }
260
257
 
261
258
  export interface GrepToolCall
262
- extends ToolCallBase<"grep", { pattern: string; outputMode?: string }> {}
259
+ extends ToolCallBase<"grep", { pattern: string; outputMode?: string }> { }
263
260
 
264
261
  export interface BashToolCall
265
- extends ToolCallBase<"bash", { command: string; description?: string; timeoutMs?: number; runInBackground?: boolean }> {}
262
+ extends ToolCallBase<"bash", { command: string; description?: string; timeoutMs?: number; runInBackground?: boolean }> { }
266
263
 
267
264
  export interface WebSearchToolCall
268
- extends ToolCallBase<"web_search", { query: string }> {}
265
+ extends ToolCallBase<"web_search", { query: string }> { }
269
266
 
270
267
  export interface ReadFileToolCall
271
- extends ToolCallBase<"read_file", { filePath: string }> {}
268
+ extends ToolCallBase<"read_file", { filePath: string }> { }
272
269
 
273
270
  export interface WriteFileToolCall
274
- extends ToolCallBase<"write_file", { filePath: string; content: string }> {}
271
+ extends ToolCallBase<"write_file", { filePath: string; content: string }> { }
275
272
 
276
273
  export interface EditFileToolCall
277
- extends ToolCallBase<"edit_file", { filePath: string; oldString: string; newString: string }> {}
274
+ extends ToolCallBase<"edit_file", { filePath: string; oldString: string; newString: string }> { }
278
275
 
279
276
  export interface SubagentTaskToolCall
280
- extends ToolCallBase<"subagent_task", { subagentType?: string }> {}
277
+ extends ToolCallBase<"subagent_task", { subagentType?: string }> { }
281
278
 
282
279
  export interface McpGenericToolCall
283
- extends ToolCallBase<"mcp_generic", { server: string; tool: string; payload: Record<string, unknown> }> {}
280
+ extends ToolCallBase<"mcp_generic", { server: string; tool: string; payload: Record<string, unknown> }> { }
284
281
 
285
282
  export interface UnknownToolCall
286
- extends ToolCallBase<"unknown_tool", { payload: Record<string, unknown> }> {}
283
+ extends ToolCallBase<"unknown_tool", { payload: Record<string, unknown> }> { }
287
284
 
288
285
  export type NormalizedToolCall =
289
286
  | AskUserQuestionToolCall