kanna-code 0.9.0 → 0.10.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/LICENSE +2 -1
- package/README.md +1 -1
- package/dist/client/assets/index-rNCNxifd.js +533 -0
- package/dist/client/assets/{index-gEOLdGK-.css → index-wYwkX5A2.css} +1 -1
- package/dist/client/index.html +2 -2
- package/package.json +1 -1
- package/src/server/cli-runtime.ts +4 -0
- package/src/server/keybindings.test.ts +113 -0
- package/src/server/keybindings.ts +163 -0
- package/src/server/server.ts +5 -0
- package/src/server/ws-router.test.ts +111 -0
- package/src/server/ws-router.ts +34 -0
- package/src/shared/branding.ts +5 -1
- package/src/shared/protocol.ts +5 -0
- package/src/shared/types.ts +34 -14
- package/dist/client/assets/index-CRDe-Lt2.js +0 -543
|
@@ -0,0 +1,163 @@
|
|
|
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 { getDataRootDir, 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 = createDefaultSnapshot()
|
|
16
|
+
private readonly listeners = new Set<(snapshot: KeybindingsSnapshot) => void>()
|
|
17
|
+
|
|
18
|
+
constructor(filePath = path.join(getDataRootDir(homedir()), "keybindings.json")) {
|
|
19
|
+
this.filePath = filePath
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async initialize() {
|
|
23
|
+
await mkdir(path.dirname(this.filePath), { recursive: true })
|
|
24
|
+
const file = Bun.file(this.filePath)
|
|
25
|
+
if (!(await file.exists())) {
|
|
26
|
+
await writeFile(this.filePath, `${JSON.stringify(DEFAULT_KEYBINDINGS, null, 2)}\n`, "utf8")
|
|
27
|
+
}
|
|
28
|
+
await this.reload()
|
|
29
|
+
this.startWatching()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
dispose() {
|
|
33
|
+
this.watcher?.close()
|
|
34
|
+
this.watcher = null
|
|
35
|
+
this.listeners.clear()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getSnapshot() {
|
|
39
|
+
return this.snapshot
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
onChange(listener: (snapshot: KeybindingsSnapshot) => void) {
|
|
43
|
+
this.listeners.add(listener)
|
|
44
|
+
return () => {
|
|
45
|
+
this.listeners.delete(listener)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async reload() {
|
|
50
|
+
const nextSnapshot = await readKeybindingsSnapshot(this.filePath)
|
|
51
|
+
this.setSnapshot(nextSnapshot)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async write(bindings: Partial<Record<KeybindingAction, string[]>>) {
|
|
55
|
+
const nextSnapshot = normalizeKeybindings(bindings)
|
|
56
|
+
await mkdir(path.dirname(this.filePath), { recursive: true })
|
|
57
|
+
await writeFile(this.filePath, `${JSON.stringify(nextSnapshot.bindings, null, 2)}\n`, "utf8")
|
|
58
|
+
this.setSnapshot(nextSnapshot)
|
|
59
|
+
return nextSnapshot
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private setSnapshot(snapshot: KeybindingsSnapshot) {
|
|
63
|
+
this.snapshot = snapshot
|
|
64
|
+
for (const listener of this.listeners) {
|
|
65
|
+
listener(snapshot)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private startWatching() {
|
|
70
|
+
this.watcher?.close()
|
|
71
|
+
try {
|
|
72
|
+
this.watcher = watch(path.dirname(this.filePath), { persistent: false }, (_eventType, filename) => {
|
|
73
|
+
if (filename && filename !== path.basename(this.filePath)) {
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
void this.reload().catch((error: unknown) => {
|
|
77
|
+
console.warn(`${LOG_PREFIX} Failed to reload keybindings:`, error)
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.warn(`${LOG_PREFIX} Failed to watch keybindings file:`, error)
|
|
82
|
+
this.watcher = null
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function readKeybindingsSnapshot(filePath: string) {
|
|
88
|
+
try {
|
|
89
|
+
const text = await readFile(filePath, "utf8")
|
|
90
|
+
if (!text.trim()) {
|
|
91
|
+
return createDefaultSnapshot("Keybindings file was empty. Using defaults.")
|
|
92
|
+
}
|
|
93
|
+
const parsed = JSON.parse(text) as KeybindingsFile
|
|
94
|
+
return normalizeKeybindings(parsed)
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if ((error as NodeJS.ErrnoException)?.code === "ENOENT") {
|
|
97
|
+
return createDefaultSnapshot()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (error instanceof SyntaxError) {
|
|
101
|
+
return createDefaultSnapshot("Keybindings file is invalid JSON. Using defaults.")
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
throw error
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function normalizeKeybindings(value: KeybindingsFile | null | undefined): KeybindingsSnapshot {
|
|
109
|
+
const warnings: string[] = []
|
|
110
|
+
const source = value && typeof value === "object" && !Array.isArray(value)
|
|
111
|
+
? value
|
|
112
|
+
: null
|
|
113
|
+
|
|
114
|
+
if (!source) {
|
|
115
|
+
return createDefaultSnapshot("Keybindings file must contain a JSON object. Using defaults.")
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const bindings = {} as Record<KeybindingAction, string[]>
|
|
119
|
+
for (const action of KEYBINDING_ACTIONS) {
|
|
120
|
+
const rawValue = source[action]
|
|
121
|
+
if (!Array.isArray(rawValue)) {
|
|
122
|
+
bindings[action] = [...DEFAULT_KEYBINDINGS[action]]
|
|
123
|
+
if (rawValue !== undefined) {
|
|
124
|
+
warnings.push(`${action} must be an array of shortcut strings`)
|
|
125
|
+
}
|
|
126
|
+
continue
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const normalized = rawValue
|
|
130
|
+
.filter((entry): entry is string => typeof entry === "string")
|
|
131
|
+
.map((entry) => entry.trim())
|
|
132
|
+
.map((entry) => entry.toLowerCase())
|
|
133
|
+
.filter(Boolean)
|
|
134
|
+
|
|
135
|
+
if (normalized.length === 0) {
|
|
136
|
+
bindings[action] = [...DEFAULT_KEYBINDINGS[action]]
|
|
137
|
+
if (rawValue.length > 0 || source[action] !== undefined) {
|
|
138
|
+
warnings.push(`${action} did not contain any valid shortcut strings`)
|
|
139
|
+
}
|
|
140
|
+
continue
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
bindings[action] = normalized
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
bindings,
|
|
148
|
+
warning: warnings.length > 0 ? `Some keybindings were reset to defaults: ${warnings.join("; ")}` : null,
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function createDefaultSnapshot(warning: string | null = null): KeybindingsSnapshot {
|
|
153
|
+
return {
|
|
154
|
+
bindings: {
|
|
155
|
+
toggleEmbeddedTerminal: [...DEFAULT_KEYBINDINGS.toggleEmbeddedTerminal],
|
|
156
|
+
toggleRightSidebar: [...DEFAULT_KEYBINDINGS.toggleRightSidebar],
|
|
157
|
+
openInFinder: [...DEFAULT_KEYBINDINGS.openInFinder],
|
|
158
|
+
openInEditor: [...DEFAULT_KEYBINDINGS.openInEditor],
|
|
159
|
+
addSplitTerminal: [...DEFAULT_KEYBINDINGS.addSplitTerminal],
|
|
160
|
+
},
|
|
161
|
+
warning,
|
|
162
|
+
}
|
|
163
|
+
}
|
package/src/server/server.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { EventStore } from "./event-store"
|
|
|
4
4
|
import { AgentCoordinator } from "./agent"
|
|
5
5
|
import { discoverProjects, type DiscoveredProject } from "./discovery"
|
|
6
6
|
import { FileTreeManager } from "./file-tree-manager"
|
|
7
|
+
import { KeybindingsManager } from "./keybindings"
|
|
7
8
|
import { getMachineDisplayName } from "./machine-name"
|
|
8
9
|
import { TerminalManager } from "./terminal-manager"
|
|
9
10
|
import { createWsRouter, type ClientState } from "./ws-router"
|
|
@@ -31,6 +32,8 @@ export async function startKannaServer(options: StartKannaServerOptions = {}) {
|
|
|
31
32
|
let server: ReturnType<typeof Bun.serve<ClientState>>
|
|
32
33
|
let router: ReturnType<typeof createWsRouter>
|
|
33
34
|
const terminals = new TerminalManager()
|
|
35
|
+
const keybindings = new KeybindingsManager()
|
|
36
|
+
await keybindings.initialize()
|
|
34
37
|
const fileTree = new FileTreeManager({
|
|
35
38
|
getProject: (projectId) => store.getProject(projectId),
|
|
36
39
|
})
|
|
@@ -44,6 +47,7 @@ export async function startKannaServer(options: StartKannaServerOptions = {}) {
|
|
|
44
47
|
store,
|
|
45
48
|
agent,
|
|
46
49
|
terminals,
|
|
50
|
+
keybindings,
|
|
47
51
|
fileTree,
|
|
48
52
|
refreshDiscovery,
|
|
49
53
|
getDiscoveredProjects: () => discoveredProjects,
|
|
@@ -107,6 +111,7 @@ export async function startKannaServer(options: StartKannaServerOptions = {}) {
|
|
|
107
111
|
}
|
|
108
112
|
router.dispose()
|
|
109
113
|
fileTree.dispose()
|
|
114
|
+
keybindings.dispose()
|
|
110
115
|
terminals.closeAll()
|
|
111
116
|
await store.compact()
|
|
112
117
|
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"
|
|
@@ -23,6 +24,10 @@ describe("ws-router", () => {
|
|
|
23
24
|
getSnapshot: () => null,
|
|
24
25
|
onEvent: () => () => {},
|
|
25
26
|
} as never,
|
|
27
|
+
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 }),
|
|
29
|
+
onChange: () => () => {},
|
|
30
|
+
} as never,
|
|
26
31
|
fileTree: {
|
|
27
32
|
getSnapshot: () => ({ projectId: "project-1", rootPath: "/tmp/project-1", pageSize: 200, supportsRealtime: true }),
|
|
28
33
|
onInvalidate: () => () => {},
|
|
@@ -62,6 +67,10 @@ describe("ws-router", () => {
|
|
|
62
67
|
onEvent: () => () => {},
|
|
63
68
|
write: () => {},
|
|
64
69
|
} as never,
|
|
70
|
+
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 }),
|
|
72
|
+
onChange: () => () => {},
|
|
73
|
+
} as never,
|
|
65
74
|
fileTree: {
|
|
66
75
|
getSnapshot: () => ({ projectId: "project-1", rootPath: "/tmp/project-1", pageSize: 200, supportsRealtime: true }),
|
|
67
76
|
onInvalidate: () => () => {},
|
|
@@ -128,6 +137,10 @@ describe("ws-router", () => {
|
|
|
128
137
|
getSnapshot: () => null,
|
|
129
138
|
onEvent: () => () => {},
|
|
130
139
|
} as never,
|
|
140
|
+
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 }),
|
|
142
|
+
onChange: () => () => {},
|
|
143
|
+
} as never,
|
|
131
144
|
fileTree: fileTree as never,
|
|
132
145
|
refreshDiscovery: async () => [],
|
|
133
146
|
getDiscoveredProjects: () => [],
|
|
@@ -204,4 +217,102 @@ describe("ws-router", () => {
|
|
|
204
217
|
id: "tree-sub-1",
|
|
205
218
|
})
|
|
206
219
|
})
|
|
220
|
+
|
|
221
|
+
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
|
+
}
|
|
232
|
+
const keybindings = {
|
|
233
|
+
snapshot: initialSnapshot,
|
|
234
|
+
getSnapshot() {
|
|
235
|
+
return this.snapshot
|
|
236
|
+
},
|
|
237
|
+
onChange: () => () => {},
|
|
238
|
+
async write(bindings: KeybindingsSnapshot["bindings"]) {
|
|
239
|
+
this.snapshot = { bindings, warning: null }
|
|
240
|
+
return this.snapshot
|
|
241
|
+
},
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const router = createWsRouter({
|
|
245
|
+
store: { state: createEmptyState() } as never,
|
|
246
|
+
agent: { getActiveStatuses: () => new Map() } as never,
|
|
247
|
+
terminals: {
|
|
248
|
+
getSnapshot: () => null,
|
|
249
|
+
onEvent: () => () => {},
|
|
250
|
+
} as never,
|
|
251
|
+
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
|
+
refreshDiscovery: async () => [],
|
|
257
|
+
getDiscoveredProjects: () => [],
|
|
258
|
+
machineDisplayName: "Local Machine",
|
|
259
|
+
})
|
|
260
|
+
const ws = new FakeWebSocket()
|
|
261
|
+
|
|
262
|
+
router.handleMessage(
|
|
263
|
+
ws as never,
|
|
264
|
+
JSON.stringify({
|
|
265
|
+
v: 1,
|
|
266
|
+
type: "subscribe",
|
|
267
|
+
id: "keybindings-sub-1",
|
|
268
|
+
topic: { type: "keybindings" },
|
|
269
|
+
})
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
expect(ws.sent[0]).toEqual({
|
|
273
|
+
v: PROTOCOL_VERSION,
|
|
274
|
+
type: "snapshot",
|
|
275
|
+
id: "keybindings-sub-1",
|
|
276
|
+
snapshot: {
|
|
277
|
+
type: "keybindings",
|
|
278
|
+
data: keybindings.snapshot,
|
|
279
|
+
},
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
router.handleMessage(
|
|
283
|
+
ws as never,
|
|
284
|
+
JSON.stringify({
|
|
285
|
+
v: 1,
|
|
286
|
+
type: "command",
|
|
287
|
+
id: "keybindings-write-1",
|
|
288
|
+
command: {
|
|
289
|
+
type: "settings.writeKeybindings",
|
|
290
|
+
bindings: {
|
|
291
|
+
toggleEmbeddedTerminal: ["cmd+k"],
|
|
292
|
+
toggleRightSidebar: ["ctrl+shift+b"],
|
|
293
|
+
openInFinder: ["cmd+shift+g"],
|
|
294
|
+
openInEditor: ["cmd+shift+p"],
|
|
295
|
+
addSplitTerminal: ["cmd+alt+j"],
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
})
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
await Promise.resolve()
|
|
302
|
+
expect(ws.sent[1]).toEqual({
|
|
303
|
+
v: PROTOCOL_VERSION,
|
|
304
|
+
type: "ack",
|
|
305
|
+
id: "keybindings-write-1",
|
|
306
|
+
result: {
|
|
307
|
+
bindings: {
|
|
308
|
+
toggleEmbeddedTerminal: ["cmd+k"],
|
|
309
|
+
toggleRightSidebar: ["ctrl+shift+b"],
|
|
310
|
+
openInFinder: ["cmd+shift+g"],
|
|
311
|
+
openInEditor: ["cmd+shift+p"],
|
|
312
|
+
addSplitTerminal: ["cmd+alt+j"],
|
|
313
|
+
},
|
|
314
|
+
warning: null,
|
|
315
|
+
},
|
|
316
|
+
})
|
|
317
|
+
})
|
|
207
318
|
})
|
package/src/server/ws-router.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { DiscoveredProject } from "./discovery"
|
|
|
7
7
|
import { EventStore } from "./event-store"
|
|
8
8
|
import { openExternal } from "./external-open"
|
|
9
9
|
import { FileTreeManager } from "./file-tree-manager"
|
|
10
|
+
import { KeybindingsManager } from "./keybindings"
|
|
10
11
|
import { ensureProjectDirectory } from "./paths"
|
|
11
12
|
import { TerminalManager } from "./terminal-manager"
|
|
12
13
|
import { deriveChatSnapshot, deriveLocalProjectsSnapshot, deriveSidebarData } from "./read-models"
|
|
@@ -19,6 +20,7 @@ interface CreateWsRouterArgs {
|
|
|
19
20
|
store: EventStore
|
|
20
21
|
agent: AgentCoordinator
|
|
21
22
|
terminals: TerminalManager
|
|
23
|
+
keybindings: KeybindingsManager
|
|
22
24
|
fileTree: FileTreeManager
|
|
23
25
|
refreshDiscovery: () => Promise<DiscoveredProject[]>
|
|
24
26
|
getDiscoveredProjects: () => DiscoveredProject[]
|
|
@@ -33,6 +35,7 @@ export function createWsRouter({
|
|
|
33
35
|
store,
|
|
34
36
|
agent,
|
|
35
37
|
terminals,
|
|
38
|
+
keybindings,
|
|
36
39
|
fileTree,
|
|
37
40
|
refreshDiscovery,
|
|
38
41
|
getDiscoveredProjects,
|
|
@@ -68,6 +71,18 @@ export function createWsRouter({
|
|
|
68
71
|
}
|
|
69
72
|
}
|
|
70
73
|
|
|
74
|
+
if (topic.type === "keybindings") {
|
|
75
|
+
return {
|
|
76
|
+
v: PROTOCOL_VERSION,
|
|
77
|
+
type: "snapshot",
|
|
78
|
+
id,
|
|
79
|
+
snapshot: {
|
|
80
|
+
type: "keybindings",
|
|
81
|
+
data: keybindings.getSnapshot(),
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
71
86
|
if (topic.type === "terminal") {
|
|
72
87
|
return {
|
|
73
88
|
v: PROTOCOL_VERSION,
|
|
@@ -156,6 +171,15 @@ export function createWsRouter({
|
|
|
156
171
|
}
|
|
157
172
|
})
|
|
158
173
|
|
|
174
|
+
const disposeKeybindingEvents = keybindings.onChange(() => {
|
|
175
|
+
for (const ws of sockets) {
|
|
176
|
+
for (const [id, topic] of ws.data.subscriptions.entries()) {
|
|
177
|
+
if (topic.type !== "keybindings") continue
|
|
178
|
+
send(ws, createEnvelope(id, topic))
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
|
|
159
183
|
async function handleCommand(ws: ServerWebSocket<ClientState>, message: Extract<ClientEnvelope, { type: "command" }>) {
|
|
160
184
|
const { command, id } = message
|
|
161
185
|
try {
|
|
@@ -164,6 +188,15 @@ export function createWsRouter({
|
|
|
164
188
|
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
165
189
|
return
|
|
166
190
|
}
|
|
191
|
+
case "settings.readKeybindings": {
|
|
192
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: keybindings.getSnapshot() })
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
case "settings.writeKeybindings": {
|
|
196
|
+
const snapshot = await keybindings.write(command.bindings)
|
|
197
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: snapshot })
|
|
198
|
+
return
|
|
199
|
+
}
|
|
167
200
|
case "project.open": {
|
|
168
201
|
await ensureProjectDirectory(command.localPath)
|
|
169
202
|
const project = await store.openProject(command.localPath)
|
|
@@ -329,6 +362,7 @@ export function createWsRouter({
|
|
|
329
362
|
dispose() {
|
|
330
363
|
disposeTerminalEvents()
|
|
331
364
|
disposeFileTreeEvents()
|
|
365
|
+
disposeKeybindingEvents()
|
|
332
366
|
},
|
|
333
367
|
}
|
|
334
368
|
}
|
package/src/shared/branding.ts
CHANGED
|
@@ -12,8 +12,12 @@ export function getDataRootName() {
|
|
|
12
12
|
return DATA_ROOT_NAME
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
export function getDataRootDir(homeDir: string) {
|
|
16
|
+
return `${homeDir}/${DATA_ROOT_NAME}`
|
|
17
|
+
}
|
|
18
|
+
|
|
15
19
|
export function getDataDir(homeDir: string) {
|
|
16
|
-
return `${homeDir}
|
|
20
|
+
return `${getDataRootDir(homeDir)}/data`
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
export function getDataDirDisplay() {
|
package/src/shared/protocol.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
ChatSnapshot,
|
|
4
4
|
FileTreeDirectoryPage,
|
|
5
5
|
FileTreeSnapshot,
|
|
6
|
+
KeybindingsSnapshot,
|
|
6
7
|
LocalProjectsSnapshot,
|
|
7
8
|
ModelOptions,
|
|
8
9
|
SidebarData,
|
|
@@ -18,6 +19,7 @@ export interface EditorOpenSettings {
|
|
|
18
19
|
export type SubscriptionTopic =
|
|
19
20
|
| { type: "sidebar" }
|
|
20
21
|
| { type: "local-projects" }
|
|
22
|
+
| { type: "keybindings" }
|
|
21
23
|
| { type: "file-tree"; projectId: string }
|
|
22
24
|
| { type: "chat"; chatId: string }
|
|
23
25
|
| { type: "terminal"; terminalId: string }
|
|
@@ -45,6 +47,8 @@ export type ClientCommand =
|
|
|
45
47
|
| { type: "project.create"; localPath: string; title: string }
|
|
46
48
|
| { type: "project.remove"; projectId: string }
|
|
47
49
|
| { type: "system.ping" }
|
|
50
|
+
| { type: "settings.readKeybindings" }
|
|
51
|
+
| { type: "settings.writeKeybindings"; bindings: KeybindingsSnapshot["bindings"] }
|
|
48
52
|
| {
|
|
49
53
|
type: "system.openExternal"
|
|
50
54
|
localPath: string
|
|
@@ -89,6 +93,7 @@ export type ClientEnvelope =
|
|
|
89
93
|
export type ServerSnapshot =
|
|
90
94
|
| { type: "sidebar"; data: SidebarData }
|
|
91
95
|
| { type: "local-projects"; data: LocalProjectsSnapshot }
|
|
96
|
+
| { type: "keybindings"; data: KeybindingsSnapshot }
|
|
92
97
|
| { type: "file-tree"; data: FileTreeSnapshot }
|
|
93
98
|
| { type: "chat"; data: ChatSnapshot | null }
|
|
94
99
|
| { type: "terminal"; data: TerminalSnapshot | null }
|
package/src/shared/types.ts
CHANGED
|
@@ -191,6 +191,26 @@ export interface FileTreeSnapshot {
|
|
|
191
191
|
supportsRealtime: true
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
export type KeybindingAction =
|
|
195
|
+
| "toggleEmbeddedTerminal"
|
|
196
|
+
| "toggleRightSidebar"
|
|
197
|
+
| "openInFinder"
|
|
198
|
+
| "openInEditor"
|
|
199
|
+
| "addSplitTerminal"
|
|
200
|
+
|
|
201
|
+
export const DEFAULT_KEYBINDINGS: Record<KeybindingAction, string[]> = {
|
|
202
|
+
toggleEmbeddedTerminal: ["cmd+j", "ctrl+`"],
|
|
203
|
+
toggleRightSidebar: ["cmd+b", "ctrl+b"],
|
|
204
|
+
openInFinder: ["cmd+alt+f", "ctrl+alt+f"],
|
|
205
|
+
openInEditor: ["cmd+shift+o", "ctrl+shift+o"],
|
|
206
|
+
addSplitTerminal: ["cmd+/", "ctrl+/"],
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export interface KeybindingsSnapshot {
|
|
210
|
+
bindings: Record<KeybindingAction, string[]>
|
|
211
|
+
warning: string | null
|
|
212
|
+
}
|
|
213
|
+
|
|
194
214
|
export interface McpServerInfo {
|
|
195
215
|
name: string
|
|
196
216
|
status: string
|
|
@@ -244,46 +264,46 @@ interface ToolCallBase<TKind extends string, TInput> {
|
|
|
244
264
|
}
|
|
245
265
|
|
|
246
266
|
export interface AskUserQuestionToolCall
|
|
247
|
-
extends ToolCallBase<"ask_user_question", { questions: AskUserQuestionItem[] }> {}
|
|
267
|
+
extends ToolCallBase<"ask_user_question", { questions: AskUserQuestionItem[] }> { }
|
|
248
268
|
|
|
249
269
|
export interface ExitPlanModeToolCall
|
|
250
|
-
extends ToolCallBase<"exit_plan_mode", { plan?: string; summary?: string }> {}
|
|
270
|
+
extends ToolCallBase<"exit_plan_mode", { plan?: string; summary?: string }> { }
|
|
251
271
|
|
|
252
272
|
export interface TodoWriteToolCall
|
|
253
|
-
extends ToolCallBase<"todo_write", { todos: TodoItem[] }> {}
|
|
273
|
+
extends ToolCallBase<"todo_write", { todos: TodoItem[] }> { }
|
|
254
274
|
|
|
255
275
|
export interface SkillToolCall
|
|
256
|
-
extends ToolCallBase<"skill", { skill: string }> {}
|
|
276
|
+
extends ToolCallBase<"skill", { skill: string }> { }
|
|
257
277
|
|
|
258
278
|
export interface GlobToolCall
|
|
259
|
-
extends ToolCallBase<"glob", { pattern: string }> {}
|
|
279
|
+
extends ToolCallBase<"glob", { pattern: string }> { }
|
|
260
280
|
|
|
261
281
|
export interface GrepToolCall
|
|
262
|
-
extends ToolCallBase<"grep", { pattern: string; outputMode?: string }> {}
|
|
282
|
+
extends ToolCallBase<"grep", { pattern: string; outputMode?: string }> { }
|
|
263
283
|
|
|
264
284
|
export interface BashToolCall
|
|
265
|
-
extends ToolCallBase<"bash", { command: string; description?: string; timeoutMs?: number; runInBackground?: boolean }> {}
|
|
285
|
+
extends ToolCallBase<"bash", { command: string; description?: string; timeoutMs?: number; runInBackground?: boolean }> { }
|
|
266
286
|
|
|
267
287
|
export interface WebSearchToolCall
|
|
268
|
-
extends ToolCallBase<"web_search", { query: string }> {}
|
|
288
|
+
extends ToolCallBase<"web_search", { query: string }> { }
|
|
269
289
|
|
|
270
290
|
export interface ReadFileToolCall
|
|
271
|
-
extends ToolCallBase<"read_file", { filePath: string }> {}
|
|
291
|
+
extends ToolCallBase<"read_file", { filePath: string }> { }
|
|
272
292
|
|
|
273
293
|
export interface WriteFileToolCall
|
|
274
|
-
extends ToolCallBase<"write_file", { filePath: string; content: string }> {}
|
|
294
|
+
extends ToolCallBase<"write_file", { filePath: string; content: string }> { }
|
|
275
295
|
|
|
276
296
|
export interface EditFileToolCall
|
|
277
|
-
extends ToolCallBase<"edit_file", { filePath: string; oldString: string; newString: string }> {}
|
|
297
|
+
extends ToolCallBase<"edit_file", { filePath: string; oldString: string; newString: string }> { }
|
|
278
298
|
|
|
279
299
|
export interface SubagentTaskToolCall
|
|
280
|
-
extends ToolCallBase<"subagent_task", { subagentType?: string }> {}
|
|
300
|
+
extends ToolCallBase<"subagent_task", { subagentType?: string }> { }
|
|
281
301
|
|
|
282
302
|
export interface McpGenericToolCall
|
|
283
|
-
extends ToolCallBase<"mcp_generic", { server: string; tool: string; payload: Record<string, unknown> }> {}
|
|
303
|
+
extends ToolCallBase<"mcp_generic", { server: string; tool: string; payload: Record<string, unknown> }> { }
|
|
284
304
|
|
|
285
305
|
export interface UnknownToolCall
|
|
286
|
-
extends ToolCallBase<"unknown_tool", { payload: Record<string, unknown> }> {}
|
|
306
|
+
extends ToolCallBase<"unknown_tool", { payload: Record<string, unknown> }> { }
|
|
287
307
|
|
|
288
308
|
export type NormalizedToolCall =
|
|
289
309
|
| AskUserQuestionToolCall
|