kanna-code 0.6.1 → 0.7.1
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-BQedIo87.css +32 -0
- package/dist/client/assets/index-BVFjL2m3.js +498 -0
- package/dist/client/index.html +2 -2
- package/package.json +3 -1
- package/src/server/external-open.test.ts +60 -0
- package/src/server/external-open.ts +203 -50
- package/src/server/terminal-manager.test.ts +56 -22
- package/src/server/terminal-manager.ts +2 -0
- package/src/server/ws-router.ts +1 -1
- package/src/shared/protocol.ts +15 -1
- package/dist/client/assets/index-C4BaFDD7.css +0 -32
- package/dist/client/assets/index-CAUb9qP0.js +0 -478
package/dist/client/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
7
7
|
<title>Kanna</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-BVFjL2m3.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BQedIo87.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kanna-code",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.7.1",
|
|
5
5
|
"description": "A beautiful web UI for Claude Code",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"keywords": [
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"bin": {
|
|
23
23
|
"kanna": "./bin/kanna"
|
|
24
24
|
},
|
|
25
|
+
"packageManager": "bun@1.3.5",
|
|
25
26
|
"files": [
|
|
26
27
|
"bin/",
|
|
27
28
|
"src/server/",
|
|
@@ -46,6 +47,7 @@
|
|
|
46
47
|
"@radix-ui/react-context-menu": "^2.2.16",
|
|
47
48
|
"@xterm/addon-fit": "^0.11.0",
|
|
48
49
|
"@xterm/addon-serialize": "^0.14.0",
|
|
50
|
+
"@xterm/addon-web-links": "^0.12.0",
|
|
49
51
|
"@xterm/headless": "^6.0.0",
|
|
50
52
|
"@xterm/xterm": "^6.0.0",
|
|
51
53
|
"default-shell": "^2.2.0",
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { buildEditorCommand, tokenizeCommandTemplate } from "./external-open"
|
|
3
|
+
|
|
4
|
+
describe("tokenizeCommandTemplate", () => {
|
|
5
|
+
test("keeps quoted arguments together", () => {
|
|
6
|
+
expect(tokenizeCommandTemplate('code --reuse-window "{path}"')).toEqual([
|
|
7
|
+
"code",
|
|
8
|
+
"--reuse-window",
|
|
9
|
+
"{path}",
|
|
10
|
+
])
|
|
11
|
+
})
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
describe("buildEditorCommand", () => {
|
|
15
|
+
test("builds a preset goto command for file links", () => {
|
|
16
|
+
expect(
|
|
17
|
+
buildEditorCommand({
|
|
18
|
+
localPath: "/Users/jake/Projects/kanna/src/client/app/App.tsx",
|
|
19
|
+
isDirectory: false,
|
|
20
|
+
line: 12,
|
|
21
|
+
column: 3,
|
|
22
|
+
editor: { preset: "vscode", commandTemplate: "code {path}" },
|
|
23
|
+
platform: "linux",
|
|
24
|
+
})
|
|
25
|
+
).toEqual({
|
|
26
|
+
command: "code",
|
|
27
|
+
args: ["--goto", "/Users/jake/Projects/kanna/src/client/app/App.tsx:12:3"],
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test("builds a preset project command for directory opens", () => {
|
|
32
|
+
expect(
|
|
33
|
+
buildEditorCommand({
|
|
34
|
+
localPath: "/Users/jake/Projects/kanna",
|
|
35
|
+
isDirectory: true,
|
|
36
|
+
editor: { preset: "cursor", commandTemplate: "cursor {path}" },
|
|
37
|
+
platform: "linux",
|
|
38
|
+
})
|
|
39
|
+
).toEqual({
|
|
40
|
+
command: "cursor",
|
|
41
|
+
args: ["/Users/jake/Projects/kanna"],
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test("uses the custom template for editor opens", () => {
|
|
46
|
+
expect(
|
|
47
|
+
buildEditorCommand({
|
|
48
|
+
localPath: "/Users/jake/Projects/kanna/src/client/app/App.tsx",
|
|
49
|
+
isDirectory: false,
|
|
50
|
+
line: 12,
|
|
51
|
+
column: 1,
|
|
52
|
+
editor: { preset: "custom", commandTemplate: 'my-editor "{path}" --line {line}' },
|
|
53
|
+
platform: "linux",
|
|
54
|
+
})
|
|
55
|
+
).toEqual({
|
|
56
|
+
command: "my-editor",
|
|
57
|
+
args: ["/Users/jake/Projects/kanna/src/client/app/App.tsx", "--line", "12"],
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
})
|
|
@@ -1,44 +1,59 @@
|
|
|
1
|
+
import { stat } from "node:fs/promises"
|
|
1
2
|
import process from "node:process"
|
|
2
|
-
import type { ClientCommand } from "../shared/protocol"
|
|
3
|
+
import type { ClientCommand, EditorOpenSettings, EditorPreset } from "../shared/protocol"
|
|
3
4
|
import { resolveLocalPath } from "./paths"
|
|
4
5
|
import { canOpenMacApp, hasCommand, spawnDetached } from "./process-utils"
|
|
5
6
|
|
|
6
|
-
type
|
|
7
|
+
type OpenExternalCommand = Extract<ClientCommand, { type: "system.openExternal" }>
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
interface CommandSpec {
|
|
10
|
+
command: string
|
|
11
|
+
args: string[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const DEFAULT_EDITOR_SETTINGS: EditorOpenSettings = {
|
|
15
|
+
preset: "cursor",
|
|
16
|
+
commandTemplate: "cursor {path}",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function openExternal(command: OpenExternalCommand) {
|
|
20
|
+
const resolvedPath = resolveLocalPath(command.localPath)
|
|
10
21
|
const platform = process.platform
|
|
11
22
|
|
|
23
|
+
if (command.action === "open_editor") {
|
|
24
|
+
const info = await stat(resolvedPath).catch(() => null)
|
|
25
|
+
if (!info) {
|
|
26
|
+
throw new Error(`Path not found: ${resolvedPath}`)
|
|
27
|
+
}
|
|
28
|
+
const editorCommand = buildEditorCommand({
|
|
29
|
+
localPath: resolvedPath,
|
|
30
|
+
isDirectory: info.isDirectory(),
|
|
31
|
+
line: command.line,
|
|
32
|
+
column: command.column,
|
|
33
|
+
editor: command.editor ?? DEFAULT_EDITOR_SETTINGS,
|
|
34
|
+
platform,
|
|
35
|
+
})
|
|
36
|
+
spawnDetached(editorCommand.command, editorCommand.args)
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
12
40
|
if (platform === "darwin") {
|
|
13
|
-
if (action === "open_finder") {
|
|
41
|
+
if (command.action === "open_finder") {
|
|
14
42
|
spawnDetached("open", [resolvedPath])
|
|
15
43
|
return
|
|
16
44
|
}
|
|
17
|
-
if (action === "open_terminal") {
|
|
45
|
+
if (command.action === "open_terminal") {
|
|
18
46
|
spawnDetached("open", ["-a", "Terminal", resolvedPath])
|
|
19
47
|
return
|
|
20
48
|
}
|
|
21
|
-
if (action === "open_editor") {
|
|
22
|
-
if (hasCommand("cursor")) {
|
|
23
|
-
spawnDetached("cursor", [resolvedPath])
|
|
24
|
-
return
|
|
25
|
-
}
|
|
26
|
-
for (const appName of ["Cursor", "Visual Studio Code", "Windsurf"]) {
|
|
27
|
-
if (!canOpenMacApp(appName)) continue
|
|
28
|
-
spawnDetached("open", ["-a", appName, resolvedPath])
|
|
29
|
-
return
|
|
30
|
-
}
|
|
31
|
-
spawnDetached("open", [resolvedPath])
|
|
32
|
-
return
|
|
33
|
-
}
|
|
34
49
|
}
|
|
35
50
|
|
|
36
51
|
if (platform === "win32") {
|
|
37
|
-
if (action === "open_finder") {
|
|
52
|
+
if (command.action === "open_finder") {
|
|
38
53
|
spawnDetached("explorer", [resolvedPath])
|
|
39
54
|
return
|
|
40
55
|
}
|
|
41
|
-
if (action === "open_terminal") {
|
|
56
|
+
if (command.action === "open_terminal") {
|
|
42
57
|
if (hasCommand("wt")) {
|
|
43
58
|
spawnDetached("wt", ["-d", resolvedPath])
|
|
44
59
|
return
|
|
@@ -46,46 +61,184 @@ export function openExternal(localPath: string, action: OpenExternalAction) {
|
|
|
46
61
|
spawnDetached("cmd", ["/c", "start", "", "cmd", "/K", `cd /d ${resolvedPath}`])
|
|
47
62
|
return
|
|
48
63
|
}
|
|
49
|
-
if (action === "open_editor") {
|
|
50
|
-
if (hasCommand("cursor")) {
|
|
51
|
-
spawnDetached("cursor", [resolvedPath])
|
|
52
|
-
return
|
|
53
|
-
}
|
|
54
|
-
if (hasCommand("code")) {
|
|
55
|
-
spawnDetached("code", [resolvedPath])
|
|
56
|
-
return
|
|
57
|
-
}
|
|
58
|
-
spawnDetached("explorer", [resolvedPath])
|
|
59
|
-
return
|
|
60
|
-
}
|
|
61
64
|
}
|
|
62
65
|
|
|
63
|
-
if (action === "open_finder") {
|
|
66
|
+
if (command.action === "open_finder") {
|
|
64
67
|
spawnDetached("xdg-open", [resolvedPath])
|
|
65
68
|
return
|
|
66
69
|
}
|
|
67
|
-
if (action === "open_terminal") {
|
|
68
|
-
for (const
|
|
69
|
-
if (!hasCommand(
|
|
70
|
-
if (
|
|
71
|
-
spawnDetached(
|
|
72
|
-
} else if (
|
|
73
|
-
spawnDetached(
|
|
70
|
+
if (command.action === "open_terminal") {
|
|
71
|
+
for (const terminalCommand of ["x-terminal-emulator", "gnome-terminal", "konsole"]) {
|
|
72
|
+
if (!hasCommand(terminalCommand)) continue
|
|
73
|
+
if (terminalCommand === "gnome-terminal") {
|
|
74
|
+
spawnDetached(terminalCommand, ["--working-directory", resolvedPath])
|
|
75
|
+
} else if (terminalCommand === "konsole") {
|
|
76
|
+
spawnDetached(terminalCommand, ["--workdir", resolvedPath])
|
|
74
77
|
} else {
|
|
75
|
-
spawnDetached(
|
|
78
|
+
spawnDetached(terminalCommand, ["--working-directory", resolvedPath])
|
|
76
79
|
}
|
|
77
80
|
return
|
|
78
81
|
}
|
|
79
82
|
spawnDetached("xdg-open", [resolvedPath])
|
|
80
|
-
return
|
|
81
83
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function buildEditorCommand(args: {
|
|
87
|
+
localPath: string
|
|
88
|
+
isDirectory: boolean
|
|
89
|
+
line?: number
|
|
90
|
+
column?: number
|
|
91
|
+
editor: EditorOpenSettings
|
|
92
|
+
platform: NodeJS.Platform
|
|
93
|
+
}): CommandSpec {
|
|
94
|
+
const editor = normalizeEditorSettings(args.editor)
|
|
95
|
+
if (editor.preset === "custom") {
|
|
96
|
+
return buildCustomEditorCommand({
|
|
97
|
+
commandTemplate: editor.commandTemplate,
|
|
98
|
+
localPath: args.localPath,
|
|
99
|
+
line: args.line,
|
|
100
|
+
column: args.column,
|
|
101
|
+
})
|
|
85
102
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
103
|
+
return buildPresetEditorCommand(args, editor.preset)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function buildPresetEditorCommand(
|
|
107
|
+
args: {
|
|
108
|
+
localPath: string
|
|
109
|
+
isDirectory: boolean
|
|
110
|
+
line?: number
|
|
111
|
+
column?: number
|
|
112
|
+
platform: NodeJS.Platform
|
|
113
|
+
},
|
|
114
|
+
preset: Exclude<EditorPreset, "custom">
|
|
115
|
+
): CommandSpec {
|
|
116
|
+
const gotoTarget = `${args.localPath}:${args.line ?? 1}:${args.column ?? 1}`
|
|
117
|
+
const opener = resolveEditorExecutable(preset, args.platform)
|
|
118
|
+
if (args.isDirectory || !args.line) {
|
|
119
|
+
return { command: opener.command, args: [...opener.args, args.localPath] }
|
|
120
|
+
}
|
|
121
|
+
return { command: opener.command, args: [...opener.args, "--goto", gotoTarget] }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function resolveEditorExecutable(preset: Exclude<EditorPreset, "custom">, platform: NodeJS.Platform) {
|
|
125
|
+
if (preset === "cursor") {
|
|
126
|
+
if (hasCommand("cursor")) return { command: "cursor", args: [] }
|
|
127
|
+
if (platform === "darwin" && canOpenMacApp("Cursor")) return { command: "open", args: ["-a", "Cursor"] }
|
|
128
|
+
}
|
|
129
|
+
if (preset === "vscode") {
|
|
130
|
+
if (hasCommand("code")) return { command: "code", args: [] }
|
|
131
|
+
if (platform === "darwin" && canOpenMacApp("Visual Studio Code")) return { command: "open", args: ["-a", "Visual Studio Code"] }
|
|
132
|
+
}
|
|
133
|
+
if (preset === "windsurf") {
|
|
134
|
+
if (hasCommand("windsurf")) return { command: "windsurf", args: [] }
|
|
135
|
+
if (platform === "darwin" && canOpenMacApp("Windsurf")) return { command: "open", args: ["-a", "Windsurf"] }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (platform === "darwin") {
|
|
139
|
+
switch (preset) {
|
|
140
|
+
case "cursor":
|
|
141
|
+
return { command: "open", args: ["-a", "Cursor"] }
|
|
142
|
+
case "vscode":
|
|
143
|
+
return { command: "open", args: ["-a", "Visual Studio Code"] }
|
|
144
|
+
case "windsurf":
|
|
145
|
+
return { command: "open", args: ["-a", "Windsurf"] }
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return { command: preset === "vscode" ? "code" : preset, args: [] }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function buildCustomEditorCommand(args: {
|
|
153
|
+
commandTemplate: string
|
|
154
|
+
localPath: string
|
|
155
|
+
line?: number
|
|
156
|
+
column?: number
|
|
157
|
+
}): CommandSpec {
|
|
158
|
+
const template = args.commandTemplate.trim()
|
|
159
|
+
if (!template.includes("{path}")) {
|
|
160
|
+
throw new Error("Custom editor command must include {path}")
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const line = String(args.line ?? 1)
|
|
164
|
+
const column = String(args.column ?? 1)
|
|
165
|
+
const replaced = template
|
|
166
|
+
.replaceAll("{path}", args.localPath)
|
|
167
|
+
.replaceAll("{line}", line)
|
|
168
|
+
.replaceAll("{column}", column)
|
|
169
|
+
|
|
170
|
+
const tokens = tokenizeCommandTemplate(replaced)
|
|
171
|
+
const [command, ...commandArgs] = tokens
|
|
172
|
+
if (!command) {
|
|
173
|
+
throw new Error("Custom editor command is empty")
|
|
174
|
+
}
|
|
175
|
+
return { command, args: commandArgs }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function tokenizeCommandTemplate(template: string) {
|
|
179
|
+
const tokens: string[] = []
|
|
180
|
+
let current = ""
|
|
181
|
+
let quote: "'" | "\"" | null = null
|
|
182
|
+
|
|
183
|
+
for (let index = 0; index < template.length; index += 1) {
|
|
184
|
+
const char = template[index]
|
|
185
|
+
|
|
186
|
+
if (char === "\\" && index + 1 < template.length) {
|
|
187
|
+
current += template[index + 1]
|
|
188
|
+
index += 1
|
|
189
|
+
continue
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (quote) {
|
|
193
|
+
if (char === quote) {
|
|
194
|
+
quote = null
|
|
195
|
+
} else {
|
|
196
|
+
current += char
|
|
197
|
+
}
|
|
198
|
+
continue
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (char === "'" || char === "\"") {
|
|
202
|
+
quote = char
|
|
203
|
+
continue
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (/\s/.test(char)) {
|
|
207
|
+
if (current.length > 0) {
|
|
208
|
+
tokens.push(current)
|
|
209
|
+
current = ""
|
|
210
|
+
}
|
|
211
|
+
continue
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
current += char
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (quote) {
|
|
218
|
+
throw new Error("Custom editor command has an unclosed quote")
|
|
219
|
+
}
|
|
220
|
+
if (current.length > 0) {
|
|
221
|
+
tokens.push(current)
|
|
222
|
+
}
|
|
223
|
+
return tokens
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function normalizeEditorSettings(editor: EditorOpenSettings): EditorOpenSettings {
|
|
227
|
+
const preset = normalizeEditorPreset(editor.preset)
|
|
228
|
+
return {
|
|
229
|
+
preset,
|
|
230
|
+
commandTemplate: editor.commandTemplate.trim() || DEFAULT_EDITOR_SETTINGS.commandTemplate,
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function normalizeEditorPreset(preset: EditorPreset): EditorPreset {
|
|
235
|
+
switch (preset) {
|
|
236
|
+
case "vscode":
|
|
237
|
+
case "windsurf":
|
|
238
|
+
case "custom":
|
|
239
|
+
case "cursor":
|
|
240
|
+
return preset
|
|
241
|
+
default:
|
|
242
|
+
return DEFAULT_EDITOR_SETTINGS.preset
|
|
89
243
|
}
|
|
90
|
-
spawnDetached("xdg-open", [resolvedPath])
|
|
91
244
|
}
|
|
@@ -139,28 +139,6 @@ describeIfSupported("TerminalManager", () => {
|
|
|
139
139
|
}
|
|
140
140
|
})
|
|
141
141
|
|
|
142
|
-
test("tracks focus report mode after the PTY enables it", async () => {
|
|
143
|
-
const terminalId = "terminal-focus-enabled"
|
|
144
|
-
const { manager, getOutput } = await createSession(terminalId)
|
|
145
|
-
|
|
146
|
-
try {
|
|
147
|
-
const enableBeforeLength = getOutput().length
|
|
148
|
-
manager.write(terminalId, "printf '\\033[?1004h'\r")
|
|
149
|
-
await waitFor(() => getOutput().length > enableBeforeLength, COMMAND_TIMEOUT_MS)
|
|
150
|
-
await waitFor(
|
|
151
|
-
() =>
|
|
152
|
-
(
|
|
153
|
-
(manager as unknown as {
|
|
154
|
-
sessions: Map<string, { focusReportingEnabled: boolean }>
|
|
155
|
-
}).sessions.get(terminalId)?.focusReportingEnabled
|
|
156
|
-
) === true,
|
|
157
|
-
COMMAND_TIMEOUT_MS
|
|
158
|
-
)
|
|
159
|
-
} finally {
|
|
160
|
-
manager.close(terminalId)
|
|
161
|
-
}
|
|
162
|
-
})
|
|
163
|
-
|
|
164
142
|
test("forwards focus reports when the session mode is enabled", () => {
|
|
165
143
|
const manager = new TerminalManager() as unknown as {
|
|
166
144
|
sessions: Map<
|
|
@@ -192,6 +170,62 @@ describeIfSupported("TerminalManager", () => {
|
|
|
192
170
|
expect(writes).toEqual([FOCUS_IN_SEQUENCE])
|
|
193
171
|
})
|
|
194
172
|
|
|
173
|
+
test("resize signals the shell process group with SIGWINCH", () => {
|
|
174
|
+
const manager = new TerminalManager() as unknown as {
|
|
175
|
+
sessions: Map<
|
|
176
|
+
string,
|
|
177
|
+
{
|
|
178
|
+
cols: number
|
|
179
|
+
rows: number
|
|
180
|
+
headless: { resize: (cols: number, rows: number) => void }
|
|
181
|
+
terminal: { resize: (cols: number, rows: number) => void }
|
|
182
|
+
process: { pid: number } | null
|
|
183
|
+
}
|
|
184
|
+
>
|
|
185
|
+
resize: (terminalId: string, cols: number, rows: number) => void
|
|
186
|
+
}
|
|
187
|
+
const resizeCalls: Array<{ cols: number; rows: number }> = []
|
|
188
|
+
const killCalls: Array<{ pid: number; signal: NodeJS.Signals }> = []
|
|
189
|
+
const originalKill = process.kill
|
|
190
|
+
|
|
191
|
+
;(process as typeof process & {
|
|
192
|
+
kill: (pid: number, signal?: NodeJS.Signals | number) => boolean
|
|
193
|
+
}).kill = ((pid: number, signal?: NodeJS.Signals | number) => {
|
|
194
|
+
if (typeof signal === "string") {
|
|
195
|
+
killCalls.push({ pid, signal })
|
|
196
|
+
}
|
|
197
|
+
return true
|
|
198
|
+
}) as typeof process.kill
|
|
199
|
+
|
|
200
|
+
manager.sessions.set("terminal-resize-sigwinch", {
|
|
201
|
+
cols: 80,
|
|
202
|
+
rows: 24,
|
|
203
|
+
headless: {
|
|
204
|
+
resize(cols, rows) {
|
|
205
|
+
resizeCalls.push({ cols, rows })
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
terminal: {
|
|
209
|
+
resize(cols, rows) {
|
|
210
|
+
resizeCalls.push({ cols, rows })
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
process: { pid: 4321 },
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
manager.resize("terminal-resize-sigwinch", 120, 40)
|
|
218
|
+
} finally {
|
|
219
|
+
process.kill = originalKill
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
expect(resizeCalls).toEqual([
|
|
223
|
+
{ cols: 120, rows: 40 },
|
|
224
|
+
{ cols: 120, rows: 40 },
|
|
225
|
+
])
|
|
226
|
+
expect(killCalls).toContainEqual({ pid: -4321, signal: "SIGWINCH" })
|
|
227
|
+
})
|
|
228
|
+
|
|
195
229
|
test("new sessions reset focus mode back to filtered", async () => {
|
|
196
230
|
const manager = new TerminalManager()
|
|
197
231
|
const firstTerminalId = "terminal-focus-first"
|
|
@@ -174,6 +174,7 @@ export class TerminalManager {
|
|
|
174
174
|
existing.headless.options.scrollback = existing.scrollback
|
|
175
175
|
existing.headless.resize(existing.cols, existing.rows)
|
|
176
176
|
existing.terminal.resize(existing.cols, existing.rows)
|
|
177
|
+
signalTerminalProcessGroup(existing.process, "SIGWINCH")
|
|
177
178
|
return this.snapshotOf(existing)
|
|
178
179
|
}
|
|
179
180
|
|
|
@@ -299,6 +300,7 @@ export class TerminalManager {
|
|
|
299
300
|
session.rows = normalizeTerminalDimension(rows, session.rows)
|
|
300
301
|
session.headless.resize(session.cols, session.rows)
|
|
301
302
|
session.terminal.resize(session.cols, session.rows)
|
|
303
|
+
signalTerminalProcessGroup(session.process, "SIGWINCH")
|
|
302
304
|
}
|
|
303
305
|
|
|
304
306
|
close(terminalId: string) {
|
package/src/server/ws-router.ts
CHANGED
package/src/shared/protocol.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { AgentProvider, ChatSnapshot, LocalProjectsSnapshot, ModelOptions, SidebarData } from "./types"
|
|
2
2
|
|
|
3
|
+
export type EditorPreset = "cursor" | "vscode" | "windsurf" | "custom"
|
|
4
|
+
|
|
5
|
+
export interface EditorOpenSettings {
|
|
6
|
+
preset: EditorPreset
|
|
7
|
+
commandTemplate: string
|
|
8
|
+
}
|
|
9
|
+
|
|
3
10
|
export type SubscriptionTopic =
|
|
4
11
|
| { type: "sidebar" }
|
|
5
12
|
| { type: "local-projects" }
|
|
@@ -29,7 +36,14 @@ export type ClientCommand =
|
|
|
29
36
|
| { type: "project.create"; localPath: string; title: string }
|
|
30
37
|
| { type: "project.remove"; projectId: string }
|
|
31
38
|
| { type: "system.ping" }
|
|
32
|
-
| {
|
|
39
|
+
| {
|
|
40
|
+
type: "system.openExternal"
|
|
41
|
+
localPath: string
|
|
42
|
+
action: "open_finder" | "open_terminal" | "open_editor"
|
|
43
|
+
line?: number
|
|
44
|
+
column?: number
|
|
45
|
+
editor?: EditorOpenSettings
|
|
46
|
+
}
|
|
33
47
|
| { type: "chat.create"; projectId: string }
|
|
34
48
|
| { type: "chat.rename"; chatId: string; title: string }
|
|
35
49
|
| { type: "chat.delete"; chatId: string }
|