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.
- package/dist/client/assets/{index-rNCNxifd.js → index-BBq_6S76.js} +121 -151
- package/dist/client/assets/index-C6-Y890P.css +32 -0
- package/dist/client/index.html +2 -2
- package/package.json +2 -2
- package/src/server/cli-runtime.test.ts +21 -1
- package/src/server/event-store.test.ts +22 -0
- package/src/server/event-store.ts +52 -44
- package/src/server/keybindings.test.ts +21 -2
- package/src/server/keybindings.ts +23 -11
- package/src/server/server.ts +0 -6
- package/src/server/ws-router.test.ts +28 -95
- package/src/server/ws-router.ts +0 -47
- package/src/shared/branding.test.ts +31 -0
- package/src/shared/branding.ts +39 -8
- package/src/shared/protocol.ts +1 -20
- package/src/shared/types.ts +1 -24
- package/dist/client/assets/index-wYwkX5A2.css +0 -32
- package/src/server/file-tree-manager.test.ts +0 -116
- package/src/server/file-tree-manager.ts +0 -372
|
@@ -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 {
|
|
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
|
|
15
|
+
private snapshot: KeybindingsSnapshot
|
|
16
16
|
private readonly listeners = new Set<(snapshot: KeybindingsSnapshot) => void>()
|
|
17
17
|
|
|
18
|
-
constructor(filePath =
|
|
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
|
+
}
|
package/src/server/server.ts
CHANGED
|
@@ -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: () =>
|
|
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: () =>
|
|
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
|
|
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: () =>
|
|
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: "
|
|
157
|
-
topic: { type: "
|
|
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: "
|
|
143
|
+
id: "chat-sub-1",
|
|
166
144
|
snapshot: {
|
|
167
|
-
type: "
|
|
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: "
|
|
155
|
+
id: "chat-sub-1",
|
|
210
156
|
})
|
|
211
157
|
)
|
|
212
158
|
|
|
213
|
-
expect(
|
|
214
|
-
expect(ws.sent[2]).toEqual({
|
|
159
|
+
expect(ws.sent[1]).toEqual({
|
|
215
160
|
v: PROTOCOL_VERSION,
|
|
216
161
|
type: "ack",
|
|
217
|
-
id: "
|
|
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
|
})
|
package/src/server/ws-router.ts
CHANGED
|
@@ -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
|
+
})
|
package/src/shared/branding.ts
CHANGED
|
@@ -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
|
|
12
|
-
|
|
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
|
|
16
|
-
return `${
|
|
46
|
+
export function getDataDirDisplay(env: RuntimeEnv = getRuntimeEnv()) {
|
|
47
|
+
return `${getDataRootDirDisplay(env)}/data`
|
|
17
48
|
}
|
|
18
49
|
|
|
19
|
-
export function
|
|
20
|
-
return `${getDataRootDir(homeDir)}/
|
|
50
|
+
export function getKeybindingsFilePath(homeDir: string, env: RuntimeEnv = getRuntimeEnv()) {
|
|
51
|
+
return `${getDataRootDir(homeDir, env)}/keybindings.json`
|
|
21
52
|
}
|
|
22
53
|
|
|
23
|
-
export function
|
|
24
|
-
return
|
|
54
|
+
export function getKeybindingsFilePathDisplay(env: RuntimeEnv = getRuntimeEnv()) {
|
|
55
|
+
return `${getDataRootDirDisplay(env)}/keybindings.json`
|
|
25
56
|
}
|
|
26
57
|
|
|
27
58
|
export function getCliInvocation(arg?: string) {
|
package/src/shared/protocol.ts
CHANGED
|
@@ -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
|
|
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>
|
package/src/shared/types.ts
CHANGED
|
@@ -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 {
|