kanna-code 0.10.0 → 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.
@@ -2,7 +2,7 @@ import { watch, type FSWatcher } from "node:fs"
2
2
  import { mkdir, readFile, writeFile } from "node:fs/promises"
3
3
  import { homedir } from "node:os"
4
4
  import path from "node:path"
5
- import { getDataRootDir, LOG_PREFIX } from "../shared/branding"
5
+ import { getKeybindingsFilePath, LOG_PREFIX } from "../shared/branding"
6
6
  import { DEFAULT_KEYBINDINGS, type KeybindingAction, type KeybindingsSnapshot } from "../shared/types"
7
7
 
8
8
  const KEYBINDING_ACTIONS = Object.keys(DEFAULT_KEYBINDINGS) as KeybindingAction[]
@@ -12,11 +12,12 @@ type KeybindingsFile = Partial<Record<KeybindingAction, unknown>>
12
12
  export class KeybindingsManager {
13
13
  readonly filePath: string
14
14
  private watcher: FSWatcher | null = null
15
- private snapshot: KeybindingsSnapshot = createDefaultSnapshot()
15
+ private snapshot: KeybindingsSnapshot
16
16
  private readonly listeners = new Set<(snapshot: KeybindingsSnapshot) => void>()
17
17
 
18
- constructor(filePath = path.join(getDataRootDir(homedir()), "keybindings.json")) {
18
+ constructor(filePath = getKeybindingsFilePath(homedir())) {
19
19
  this.filePath = filePath
20
+ this.snapshot = createDefaultSnapshot(this.filePath)
20
21
  }
21
22
 
22
23
  async initialize() {
@@ -52,7 +53,7 @@ export class KeybindingsManager {
52
53
  }
53
54
 
54
55
  async write(bindings: Partial<Record<KeybindingAction, string[]>>) {
55
- const nextSnapshot = normalizeKeybindings(bindings)
56
+ const nextSnapshot = normalizeKeybindings(bindings, this.filePath)
56
57
  await mkdir(path.dirname(this.filePath), { recursive: true })
57
58
  await writeFile(this.filePath, `${JSON.stringify(nextSnapshot.bindings, null, 2)}\n`, "utf8")
58
59
  this.setSnapshot(nextSnapshot)
@@ -88,31 +89,31 @@ export async function readKeybindingsSnapshot(filePath: string) {
88
89
  try {
89
90
  const text = await readFile(filePath, "utf8")
90
91
  if (!text.trim()) {
91
- return createDefaultSnapshot("Keybindings file was empty. Using defaults.")
92
+ return createDefaultSnapshot(filePath, "Keybindings file was empty. Using defaults.")
92
93
  }
93
94
  const parsed = JSON.parse(text) as KeybindingsFile
94
- return normalizeKeybindings(parsed)
95
+ return normalizeKeybindings(parsed, filePath)
95
96
  } catch (error) {
96
97
  if ((error as NodeJS.ErrnoException)?.code === "ENOENT") {
97
- return createDefaultSnapshot()
98
+ return createDefaultSnapshot(filePath)
98
99
  }
99
100
 
100
101
  if (error instanceof SyntaxError) {
101
- return createDefaultSnapshot("Keybindings file is invalid JSON. Using defaults.")
102
+ return createDefaultSnapshot(filePath, "Keybindings file is invalid JSON. Using defaults.")
102
103
  }
103
104
 
104
105
  throw error
105
106
  }
106
107
  }
107
108
 
108
- export function normalizeKeybindings(value: KeybindingsFile | null | undefined): KeybindingsSnapshot {
109
+ export function normalizeKeybindings(value: KeybindingsFile | null | undefined, filePath = getKeybindingsFilePath(homedir())): KeybindingsSnapshot {
109
110
  const warnings: string[] = []
110
111
  const source = value && typeof value === "object" && !Array.isArray(value)
111
112
  ? value
112
113
  : null
113
114
 
114
115
  if (!source) {
115
- return createDefaultSnapshot("Keybindings file must contain a JSON object. Using defaults.")
116
+ return createDefaultSnapshot(filePath, "Keybindings file must contain a JSON object. Using defaults.")
116
117
  }
117
118
 
118
119
  const bindings = {} as Record<KeybindingAction, string[]>
@@ -146,10 +147,11 @@ export function normalizeKeybindings(value: KeybindingsFile | null | undefined):
146
147
  return {
147
148
  bindings,
148
149
  warning: warnings.length > 0 ? `Some keybindings were reset to defaults: ${warnings.join("; ")}` : null,
150
+ filePathDisplay: formatDisplayPath(filePath),
149
151
  }
150
152
  }
151
153
 
152
- function createDefaultSnapshot(warning: string | null = null): KeybindingsSnapshot {
154
+ function createDefaultSnapshot(filePath: string, warning: string | null = null): KeybindingsSnapshot {
153
155
  return {
154
156
  bindings: {
155
157
  toggleEmbeddedTerminal: [...DEFAULT_KEYBINDINGS.toggleEmbeddedTerminal],
@@ -159,5 +161,15 @@ function createDefaultSnapshot(warning: string | null = null): KeybindingsSnapsh
159
161
  addSplitTerminal: [...DEFAULT_KEYBINDINGS.addSplitTerminal],
160
162
  },
161
163
  warning,
164
+ filePathDisplay: formatDisplayPath(filePath),
162
165
  }
163
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,6 @@ 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"
7
6
  import { KeybindingsManager } from "./keybindings"
8
7
  import { getMachineDisplayName } from "./machine-name"
9
8
  import { TerminalManager } from "./terminal-manager"
@@ -34,9 +33,6 @@ export async function startKannaServer(options: StartKannaServerOptions = {}) {
34
33
  const terminals = new TerminalManager()
35
34
  const keybindings = new KeybindingsManager()
36
35
  await keybindings.initialize()
37
- const fileTree = new FileTreeManager({
38
- getProject: (projectId) => store.getProject(projectId),
39
- })
40
36
  const agent = new AgentCoordinator({
41
37
  store,
42
38
  onStateChange: () => {
@@ -48,7 +44,6 @@ export async function startKannaServer(options: StartKannaServerOptions = {}) {
48
44
  agent,
49
45
  terminals,
50
46
  keybindings,
51
- fileTree,
52
47
  refreshDiscovery,
53
48
  getDiscoveredProjects: () => discoveredProjects,
54
49
  machineDisplayName,
@@ -110,7 +105,6 @@ export async function startKannaServer(options: StartKannaServerOptions = {}) {
110
105
  await agent.cancel(chatId)
111
106
  }
112
107
  router.dispose()
113
- fileTree.dispose()
114
108
  keybindings.dispose()
115
109
  terminals.closeAll()
116
110
  await store.compact()
@@ -15,6 +15,18 @@ class FakeWebSocket {
15
15
  }
16
16
  }
17
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
+
18
30
  describe("ws-router", () => {
19
31
  test("acks system.ping without broadcasting snapshots", () => {
20
32
  const router = createWsRouter({
@@ -25,13 +37,9 @@ describe("ws-router", () => {
25
37
  onEvent: () => () => {},
26
38
  } as never,
27
39
  keybindings: {
28
- getSnapshot: () => ({ bindings: { toggleEmbeddedTerminal: ["cmd+j", "ctrl+`"], toggleRightSidebar: ["ctrl+b"], openInFinder: ["cmd+alt+f"], openInEditor: ["cmd+shift+o"], addSplitTerminal: ["cmd+shift+j"] }, warning: null }),
40
+ getSnapshot: () => DEFAULT_KEYBINDINGS_SNAPSHOT,
29
41
  onChange: () => () => {},
30
42
  } as never,
31
- fileTree: {
32
- getSnapshot: () => ({ projectId: "project-1", rootPath: "/tmp/project-1", pageSize: 200, supportsRealtime: true }),
33
- onInvalidate: () => () => {},
34
- } as never,
35
43
  refreshDiscovery: async () => [],
36
44
  getDiscoveredProjects: () => [],
37
45
  machineDisplayName: "Local Machine",
@@ -68,13 +76,9 @@ describe("ws-router", () => {
68
76
  write: () => {},
69
77
  } as never,
70
78
  keybindings: {
71
- getSnapshot: () => ({ bindings: { toggleEmbeddedTerminal: ["cmd+j", "ctrl+`"], toggleRightSidebar: ["ctrl+b"], openInFinder: ["cmd+alt+f"], openInEditor: ["cmd+shift+o"], addSplitTerminal: ["cmd+shift+j"] }, warning: null }),
79
+ getSnapshot: () => DEFAULT_KEYBINDINGS_SNAPSHOT,
72
80
  onChange: () => () => {},
73
81
  } as never,
74
- fileTree: {
75
- getSnapshot: () => ({ projectId: "project-1", rootPath: "/tmp/project-1", pageSize: 200, supportsRealtime: true }),
76
- onInvalidate: () => () => {},
77
- } as never,
78
82
  refreshDiscovery: async () => [],
79
83
  getDiscoveredProjects: () => [],
80
84
  machineDisplayName: "Local Machine",
@@ -105,31 +109,7 @@ describe("ws-router", () => {
105
109
  ])
106
110
  })
107
111
 
108
- test("subscribes and unsubscribes file-tree topics and acks directory reads", async () => {
109
- const fileTree = {
110
- subscribeCalls: [] as string[],
111
- unsubscribeCalls: [] as string[],
112
- subscribe(projectId: string) {
113
- this.subscribeCalls.push(projectId)
114
- },
115
- unsubscribe(projectId: string) {
116
- this.unsubscribeCalls.push(projectId)
117
- },
118
- getSnapshot: (projectId: string) => ({
119
- projectId,
120
- rootPath: "/tmp/project-1",
121
- pageSize: 200,
122
- supportsRealtime: true as const,
123
- }),
124
- readDirectory: async () => ({
125
- directoryPath: "",
126
- entries: [],
127
- nextCursor: null,
128
- hasMore: false,
129
- }),
130
- onInvalidate: () => () => {},
131
- }
132
-
112
+ test("subscribes and unsubscribes chat topics", () => {
133
113
  const router = createWsRouter({
134
114
  store: { state: createEmptyState() } as never,
135
115
  agent: { getActiveStatuses: () => new Map() } as never,
@@ -138,10 +118,9 @@ describe("ws-router", () => {
138
118
  onEvent: () => () => {},
139
119
  } as never,
140
120
  keybindings: {
141
- getSnapshot: () => ({ bindings: { toggleEmbeddedTerminal: ["cmd+j", "ctrl+`"], toggleRightSidebar: ["ctrl+b"], openInFinder: ["cmd+alt+f"], openInEditor: ["cmd+shift+o"], addSplitTerminal: ["cmd+shift+j"] }, warning: null }),
121
+ getSnapshot: () => DEFAULT_KEYBINDINGS_SNAPSHOT,
142
122
  onChange: () => () => {},
143
123
  } as never,
144
- fileTree: fileTree as never,
145
124
  refreshDiscovery: async () => [],
146
125
  getDiscoveredProjects: () => [],
147
126
  machineDisplayName: "Local Machine",
@@ -153,51 +132,18 @@ describe("ws-router", () => {
153
132
  JSON.stringify({
154
133
  v: 1,
155
134
  type: "subscribe",
156
- id: "tree-sub-1",
157
- topic: { type: "file-tree", projectId: "project-1" },
135
+ id: "chat-sub-1",
136
+ topic: { type: "chat", chatId: "chat-1" },
158
137
  })
159
138
  )
160
139
 
161
- expect(fileTree.subscribeCalls).toEqual(["project-1"])
162
140
  expect(ws.sent[0]).toEqual({
163
141
  v: PROTOCOL_VERSION,
164
142
  type: "snapshot",
165
- id: "tree-sub-1",
143
+ id: "chat-sub-1",
166
144
  snapshot: {
167
- type: "file-tree",
168
- data: {
169
- projectId: "project-1",
170
- rootPath: "/tmp/project-1",
171
- pageSize: 200,
172
- supportsRealtime: true,
173
- },
174
- },
175
- })
176
-
177
- router.handleMessage(
178
- ws as never,
179
- JSON.stringify({
180
- v: 1,
181
- type: "command",
182
- id: "tree-read-1",
183
- command: {
184
- type: "file-tree.readDirectory",
185
- projectId: "project-1",
186
- directoryPath: "",
187
- },
188
- })
189
- )
190
-
191
- await Promise.resolve()
192
- expect(ws.sent[1]).toEqual({
193
- v: PROTOCOL_VERSION,
194
- type: "ack",
195
- id: "tree-read-1",
196
- result: {
197
- directoryPath: "",
198
- entries: [],
199
- nextCursor: null,
200
- hasMore: false,
145
+ type: "chat",
146
+ data: null,
201
147
  },
202
148
  })
203
149
 
@@ -206,29 +152,19 @@ describe("ws-router", () => {
206
152
  JSON.stringify({
207
153
  v: 1,
208
154
  type: "unsubscribe",
209
- id: "tree-sub-1",
155
+ id: "chat-sub-1",
210
156
  })
211
157
  )
212
158
 
213
- expect(fileTree.unsubscribeCalls).toEqual(["project-1"])
214
- expect(ws.sent[2]).toEqual({
159
+ expect(ws.sent[1]).toEqual({
215
160
  v: PROTOCOL_VERSION,
216
161
  type: "ack",
217
- id: "tree-sub-1",
162
+ id: "chat-sub-1",
218
163
  })
219
164
  })
220
165
 
221
166
  test("subscribes to keybindings snapshots and writes keybindings through the router", async () => {
222
- const initialSnapshot: KeybindingsSnapshot = {
223
- bindings: {
224
- toggleEmbeddedTerminal: ["cmd+j", "ctrl+`"],
225
- toggleRightSidebar: ["ctrl+b"],
226
- openInFinder: ["cmd+alt+f"],
227
- openInEditor: ["cmd+shift+o"],
228
- addSplitTerminal: ["cmd+shift+j"],
229
- },
230
- warning: null,
231
- }
167
+ const initialSnapshot: KeybindingsSnapshot = DEFAULT_KEYBINDINGS_SNAPSHOT
232
168
  const keybindings = {
233
169
  snapshot: initialSnapshot,
234
170
  getSnapshot() {
@@ -236,7 +172,7 @@ describe("ws-router", () => {
236
172
  },
237
173
  onChange: () => () => {},
238
174
  async write(bindings: KeybindingsSnapshot["bindings"]) {
239
- this.snapshot = { bindings, warning: null }
175
+ this.snapshot = { bindings, warning: null, filePathDisplay: "~/.kanna/keybindings.json" }
240
176
  return this.snapshot
241
177
  },
242
178
  }
@@ -249,10 +185,6 @@ describe("ws-router", () => {
249
185
  onEvent: () => () => {},
250
186
  } as never,
251
187
  keybindings: keybindings as never,
252
- fileTree: {
253
- getSnapshot: () => ({ projectId: "project-1", rootPath: "/tmp/project-1", pageSize: 200, supportsRealtime: true }),
254
- onInvalidate: () => () => {},
255
- } as never,
256
188
  refreshDiscovery: async () => [],
257
189
  getDiscoveredProjects: () => [],
258
190
  machineDisplayName: "Local Machine",
@@ -312,7 +244,8 @@ describe("ws-router", () => {
312
244
  addSplitTerminal: ["cmd+alt+j"],
313
245
  },
314
246
  warning: null,
247
+ filePathDisplay: "~/.kanna/keybindings.json",
315
248
  },
316
- })
249
+ })
317
250
  })
318
251
  })
@@ -6,7 +6,6 @@ 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"
10
9
  import { KeybindingsManager } from "./keybindings"
11
10
  import { ensureProjectDirectory } from "./paths"
12
11
  import { TerminalManager } from "./terminal-manager"
@@ -21,7 +20,6 @@ interface CreateWsRouterArgs {
21
20
  agent: AgentCoordinator
22
21
  terminals: TerminalManager
23
22
  keybindings: KeybindingsManager
24
- fileTree: FileTreeManager
25
23
  refreshDiscovery: () => Promise<DiscoveredProject[]>
26
24
  getDiscoveredProjects: () => DiscoveredProject[]
27
25
  machineDisplayName: string
@@ -36,7 +34,6 @@ export function createWsRouter({
36
34
  agent,
37
35
  terminals,
38
36
  keybindings,
39
- fileTree,
40
37
  refreshDiscovery,
41
38
  getDiscoveredProjects,
42
39
  machineDisplayName,
@@ -95,18 +92,6 @@ export function createWsRouter({
95
92
  }
96
93
  }
97
94
 
98
- if (topic.type === "file-tree") {
99
- return {
100
- v: PROTOCOL_VERSION,
101
- type: "snapshot",
102
- id,
103
- snapshot: {
104
- type: "file-tree",
105
- data: fileTree.getSnapshot(topic.projectId),
106
- },
107
- }
108
- }
109
-
110
95
  return {
111
96
  v: PROTOCOL_VERSION,
112
97
  type: "snapshot",
@@ -157,20 +142,6 @@ export function createWsRouter({
157
142
  pushTerminalEvent(event.terminalId, event)
158
143
  })
159
144
 
160
- const disposeFileTreeEvents = fileTree.onInvalidate((event) => {
161
- for (const ws of sockets) {
162
- for (const [id, topic] of ws.data.subscriptions.entries()) {
163
- if (topic.type !== "file-tree" || topic.projectId !== event.projectId) continue
164
- send(ws, {
165
- v: PROTOCOL_VERSION,
166
- type: "event",
167
- id,
168
- event,
169
- })
170
- }
171
- }
172
- })
173
-
174
145
  const disposeKeybindingEvents = keybindings.onChange(() => {
175
146
  for (const ws of sockets) {
176
147
  for (const [id, topic] of ws.data.subscriptions.entries()) {
@@ -290,11 +261,6 @@ export function createWsRouter({
290
261
  pushTerminalSnapshot(command.terminalId)
291
262
  return
292
263
  }
293
- case "file-tree.readDirectory": {
294
- const result = await fileTree.readDirectory(command)
295
- send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
296
- return
297
- }
298
264
  }
299
265
 
300
266
  broadcastSnapshots()
@@ -309,11 +275,6 @@ export function createWsRouter({
309
275
  sockets.add(ws)
310
276
  },
311
277
  handleClose(ws: ServerWebSocket<ClientState>) {
312
- for (const topic of ws.data.subscriptions.values()) {
313
- if (topic.type === "file-tree") {
314
- fileTree.unsubscribe(topic.projectId)
315
- }
316
- }
317
278
  sockets.delete(ws)
318
279
  },
319
280
  broadcastSnapshots,
@@ -333,9 +294,6 @@ export function createWsRouter({
333
294
 
334
295
  if (parsed.type === "subscribe") {
335
296
  ws.data.subscriptions.set(parsed.id, parsed.topic)
336
- if (parsed.topic.type === "file-tree") {
337
- fileTree.subscribe(parsed.topic.projectId)
338
- }
339
297
  if (parsed.topic.type === "local-projects") {
340
298
  void refreshDiscovery().then(() => {
341
299
  if (ws.data.subscriptions.has(parsed.id)) {
@@ -348,11 +306,7 @@ export function createWsRouter({
348
306
  }
349
307
 
350
308
  if (parsed.type === "unsubscribe") {
351
- const topic = ws.data.subscriptions.get(parsed.id)
352
309
  ws.data.subscriptions.delete(parsed.id)
353
- if (topic?.type === "file-tree") {
354
- fileTree.unsubscribe(topic.projectId)
355
- }
356
310
  send(ws, { v: PROTOCOL_VERSION, type: "ack", id: parsed.id })
357
311
  return
358
312
  }
@@ -361,7 +315,6 @@ export function createWsRouter({
361
315
  },
362
316
  dispose() {
363
317
  disposeTerminalEvents()
364
- disposeFileTreeEvents()
365
318
  disposeKeybindingEvents()
366
319
  },
367
320
  }
@@ -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,27 +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`
13
44
  }
14
45
 
15
- export function getDataRootDir(homeDir: string) {
16
- return `${homeDir}/${DATA_ROOT_NAME}`
46
+ export function getDataDirDisplay(env: RuntimeEnv = getRuntimeEnv()) {
47
+ return `${getDataRootDirDisplay(env)}/data`
17
48
  }
18
49
 
19
- export function getDataDir(homeDir: string) {
20
- return `${getDataRootDir(homeDir)}/data`
50
+ export function getKeybindingsFilePath(homeDir: string, env: RuntimeEnv = getRuntimeEnv()) {
51
+ return `${getDataRootDir(homeDir, env)}/keybindings.json`
21
52
  }
22
53
 
23
- export function getDataDirDisplay() {
24
- return `~/${DATA_ROOT_NAME.slice(1)}/data`
54
+ export function getKeybindingsFilePathDisplay(env: RuntimeEnv = getRuntimeEnv()) {
55
+ return `${getDataRootDirDisplay(env)}/keybindings.json`
25
56
  }
26
57
 
27
58
  export function getCliInvocation(arg?: string) {
@@ -1,8 +1,6 @@
1
1
  import type {
2
2
  AgentProvider,
3
3
  ChatSnapshot,
4
- FileTreeDirectoryPage,
5
- FileTreeSnapshot,
6
4
  KeybindingsSnapshot,
7
5
  LocalProjectsSnapshot,
8
6
  ModelOptions,
@@ -20,7 +18,6 @@ export type SubscriptionTopic =
20
18
  | { type: "sidebar" }
21
19
  | { type: "local-projects" }
22
20
  | { type: "keybindings" }
23
- | { type: "file-tree"; projectId: string }
24
21
  | { type: "chat"; chatId: string }
25
22
  | { type: "terminal"; terminalId: string }
26
23
 
@@ -77,13 +74,6 @@ export type ClientCommand =
77
74
  | { type: "terminal.input"; terminalId: string; data: string }
78
75
  | { type: "terminal.resize"; terminalId: string; cols: number; rows: number }
79
76
  | { type: "terminal.close"; terminalId: string }
80
- | {
81
- type: "file-tree.readDirectory"
82
- projectId: string
83
- directoryPath: string
84
- cursor?: string
85
- limit?: number
86
- }
87
77
 
88
78
  export type ClientEnvelope =
89
79
  | { v: 1; type: "subscribe"; id: string; topic: SubscriptionTopic }
@@ -94,24 +84,15 @@ export type ServerSnapshot =
94
84
  | { type: "sidebar"; data: SidebarData }
95
85
  | { type: "local-projects"; data: LocalProjectsSnapshot }
96
86
  | { type: "keybindings"; data: KeybindingsSnapshot }
97
- | { type: "file-tree"; data: FileTreeSnapshot }
98
87
  | { type: "chat"; data: ChatSnapshot | null }
99
88
  | { type: "terminal"; data: TerminalSnapshot | null }
100
89
 
101
- export type FileTreeEvent = {
102
- type: "file-tree.invalidate"
103
- projectId: string
104
- directoryPaths: string[]
105
- }
106
-
107
90
  export type ServerEnvelope =
108
91
  | { v: 1; type: "snapshot"; id: string; snapshot: ServerSnapshot }
109
- | { v: 1; type: "event"; id: string; event: TerminalEvent | FileTreeEvent }
92
+ | { v: 1; type: "event"; id: string; event: TerminalEvent }
110
93
  | { v: 1; type: "ack"; id: string; result?: unknown }
111
94
  | { v: 1; type: "error"; id?: string; message: string }
112
95
 
113
- export type FileTreeReadDirectoryResult = FileTreeDirectoryPage
114
-
115
96
  export function isClientEnvelope(value: unknown): value is ClientEnvelope {
116
97
  if (!value || typeof value !== "object") return false
117
98
  const candidate = value as Partial<ClientEnvelope>
@@ -167,30 +167,6 @@ export interface LocalProjectsSnapshot {
167
167
  projects: LocalProjectSummary[]
168
168
  }
169
169
 
170
- export type FileTreeEntryKind = "file" | "directory" | "symlink"
171
-
172
- export interface FileTreeEntry {
173
- name: string
174
- relativePath: string
175
- kind: FileTreeEntryKind
176
- extension?: string
177
- }
178
-
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
192
- }
193
-
194
170
  export type KeybindingAction =
195
171
  | "toggleEmbeddedTerminal"
196
172
  | "toggleRightSidebar"
@@ -209,6 +185,7 @@ export const DEFAULT_KEYBINDINGS: Record<KeybindingAction, string[]> = {
209
185
  export interface KeybindingsSnapshot {
210
186
  bindings: Record<KeybindingAction, string[]>
211
187
  warning: string | null
188
+ filePathDisplay: string
212
189
  }
213
190
 
214
191
  export interface McpServerInfo {