kanna-code 0.6.0 → 0.6.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/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
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-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-CAUb9qP0.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/assets/index-C4BaFDD7.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
package/package.json
CHANGED
|
@@ -6,6 +6,8 @@ import { TerminalManager } from "./terminal-manager"
|
|
|
6
6
|
|
|
7
7
|
const SHELL_START_TIMEOUT_MS = 5_000
|
|
8
8
|
const COMMAND_TIMEOUT_MS = 5_000
|
|
9
|
+
const FOCUS_IN_SEQUENCE = "\x1b[I"
|
|
10
|
+
const RAW_READ_HEX_COMMAND = `python3 -c "exec('import os,sys,tty,termios,select\\nfd=sys.stdin.fileno()\\nold=termios.tcgetattr(fd)\\ntty.setraw(fd)\\ntry:\\n sys.stdout.write(\"__RAW_READY__\\\\n\")\\n sys.stdout.flush()\\n r,_,_=select.select([fd],[],[],1)\\n data=os.read(fd,8) if r else b\"\"\\n print(data.hex() or \"__EMPTY__\")\\nfinally:\\n termios.tcsetattr(fd, termios.TCSADRAIN, old)')"\r`
|
|
9
11
|
|
|
10
12
|
const isSupportedPlatform = process.platform !== "win32" && typeof Bun.Terminal === "function"
|
|
11
13
|
const describeIfSupported = isSupportedPlatform ? describe : describe.skip
|
|
@@ -58,6 +60,10 @@ async function createSession(terminalId: string) {
|
|
|
58
60
|
}
|
|
59
61
|
}
|
|
60
62
|
|
|
63
|
+
async function waitForOutputToContain(getOutput: () => string, value: string, timeoutMs = COMMAND_TIMEOUT_MS) {
|
|
64
|
+
await waitFor(() => getOutput().includes(value), timeoutMs)
|
|
65
|
+
}
|
|
66
|
+
|
|
61
67
|
describeIfSupported("TerminalManager", () => {
|
|
62
68
|
test("ctrl+c interrupts the foreground job and keeps the shell alive", async () => {
|
|
63
69
|
const terminalId = "terminal-ctrl-c-foreground"
|
|
@@ -112,4 +118,126 @@ describeIfSupported("TerminalManager", () => {
|
|
|
112
118
|
manager.close(terminalId)
|
|
113
119
|
}
|
|
114
120
|
})
|
|
121
|
+
|
|
122
|
+
test("filters leaked focus reports while focus mode is disabled", async () => {
|
|
123
|
+
const terminalId = "terminal-focus-filtered"
|
|
124
|
+
const { manager, getOutput } = await createSession(terminalId)
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const beforeLength = getOutput().length
|
|
128
|
+
manager.write(terminalId, RAW_READ_HEX_COMMAND)
|
|
129
|
+
await waitForOutputToContain(getOutput, "__RAW_READY__")
|
|
130
|
+
|
|
131
|
+
manager.write(terminalId, FOCUS_IN_SEQUENCE)
|
|
132
|
+
await waitForOutputToContain(getOutput, "__EMPTY__")
|
|
133
|
+
|
|
134
|
+
const interactionOutput = getOutput().slice(beforeLength)
|
|
135
|
+
expect(interactionOutput).toContain("__EMPTY__")
|
|
136
|
+
expect(interactionOutput).not.toContain("1b5b49")
|
|
137
|
+
} finally {
|
|
138
|
+
manager.close(terminalId)
|
|
139
|
+
}
|
|
140
|
+
})
|
|
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
|
+
test("forwards focus reports when the session mode is enabled", () => {
|
|
165
|
+
const manager = new TerminalManager() as unknown as {
|
|
166
|
+
sessions: Map<
|
|
167
|
+
string,
|
|
168
|
+
{
|
|
169
|
+
status: "running" | "exited"
|
|
170
|
+
focusReportingEnabled: boolean
|
|
171
|
+
terminal: { write: (data: string) => void }
|
|
172
|
+
process: Bun.Subprocess | null
|
|
173
|
+
}
|
|
174
|
+
>
|
|
175
|
+
write: (terminalId: string, data: string) => void
|
|
176
|
+
}
|
|
177
|
+
const writes: string[] = []
|
|
178
|
+
|
|
179
|
+
manager.sessions.set("terminal-focus-forwarded", {
|
|
180
|
+
status: "running",
|
|
181
|
+
focusReportingEnabled: true,
|
|
182
|
+
terminal: {
|
|
183
|
+
write(data: string) {
|
|
184
|
+
writes.push(data)
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
process: null,
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
manager.write("terminal-focus-forwarded", FOCUS_IN_SEQUENCE)
|
|
191
|
+
|
|
192
|
+
expect(writes).toEqual([FOCUS_IN_SEQUENCE])
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
test("new sessions reset focus mode back to filtered", async () => {
|
|
196
|
+
const manager = new TerminalManager()
|
|
197
|
+
const firstTerminalId = "terminal-focus-first"
|
|
198
|
+
const secondTerminalId = "terminal-focus-second"
|
|
199
|
+
let outputByTerminalId = new Map<string, string>()
|
|
200
|
+
|
|
201
|
+
manager.onEvent((event) => {
|
|
202
|
+
if (event.type !== "terminal.output") return
|
|
203
|
+
outputByTerminalId.set(event.terminalId, `${outputByTerminalId.get(event.terminalId) ?? ""}${event.data}`)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
const getOutput = (terminalId: string) => outputByTerminalId.get(terminalId) ?? ""
|
|
207
|
+
|
|
208
|
+
const createManagedSession = async (terminalId: string) => {
|
|
209
|
+
manager.createTerminal({
|
|
210
|
+
projectPath: tempProjectPath,
|
|
211
|
+
terminalId,
|
|
212
|
+
cols: 80,
|
|
213
|
+
rows: 24,
|
|
214
|
+
scrollback: 1_000,
|
|
215
|
+
})
|
|
216
|
+
manager.write(terminalId, "printf '__KANNA_READY__\\n'\r")
|
|
217
|
+
await waitForOutputToContain(() => getOutput(terminalId), "__KANNA_READY__", SHELL_START_TIMEOUT_MS)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
await createManagedSession(firstTerminalId)
|
|
222
|
+
const firstBeforeLength = getOutput(firstTerminalId).length
|
|
223
|
+
manager.write(firstTerminalId, "printf '\\033[?1004h'\r")
|
|
224
|
+
await waitFor(() => getOutput(firstTerminalId).length > firstBeforeLength, COMMAND_TIMEOUT_MS)
|
|
225
|
+
manager.close(firstTerminalId)
|
|
226
|
+
|
|
227
|
+
await createManagedSession(secondTerminalId)
|
|
228
|
+
const before = getOutput(secondTerminalId).length
|
|
229
|
+
manager.write(secondTerminalId, "cat -v\r")
|
|
230
|
+
await waitFor(() => getOutput(secondTerminalId).length > before, COMMAND_TIMEOUT_MS)
|
|
231
|
+
manager.write(secondTerminalId, FOCUS_IN_SEQUENCE)
|
|
232
|
+
manager.write(secondTerminalId, "\x03")
|
|
233
|
+
manager.write(secondTerminalId, "printf '__KANNA_FRESH_SESSION__\\n'\r")
|
|
234
|
+
await waitForOutputToContain(() => getOutput(secondTerminalId), "__KANNA_FRESH_SESSION__")
|
|
235
|
+
|
|
236
|
+
const interactionOutput = getOutput(secondTerminalId).slice(before)
|
|
237
|
+
expect(interactionOutput).not.toContain("^[[I")
|
|
238
|
+
} finally {
|
|
239
|
+
manager.close(firstTerminalId)
|
|
240
|
+
manager.close(secondTerminalId)
|
|
241
|
+
}
|
|
242
|
+
})
|
|
115
243
|
})
|
|
@@ -10,6 +10,9 @@ const DEFAULT_ROWS = 24
|
|
|
10
10
|
const DEFAULT_SCROLLBACK = 1_000
|
|
11
11
|
const MIN_SCROLLBACK = 500
|
|
12
12
|
const MAX_SCROLLBACK = 5_000
|
|
13
|
+
const FOCUS_IN_SEQUENCE = "\x1b[I"
|
|
14
|
+
const FOCUS_OUT_SEQUENCE = "\x1b[O"
|
|
15
|
+
const MODE_SEQUENCE_TAIL_LENGTH = 16
|
|
13
16
|
|
|
14
17
|
interface CreateTerminalArgs {
|
|
15
18
|
projectPath: string
|
|
@@ -33,6 +36,8 @@ interface TerminalSession {
|
|
|
33
36
|
terminal: Bun.Terminal
|
|
34
37
|
headless: Terminal
|
|
35
38
|
serializeAddon: SerializeAddon
|
|
39
|
+
focusReportingEnabled: boolean
|
|
40
|
+
modeSequenceTail: string
|
|
36
41
|
}
|
|
37
42
|
|
|
38
43
|
function clampScrollback(value: number) {
|
|
@@ -78,6 +83,25 @@ function createTerminalEnv() {
|
|
|
78
83
|
}
|
|
79
84
|
}
|
|
80
85
|
|
|
86
|
+
function updateFocusReportingState(session: Pick<TerminalSession, "focusReportingEnabled" | "modeSequenceTail">, chunk: string) {
|
|
87
|
+
const combined = session.modeSequenceTail + chunk
|
|
88
|
+
const regex = /\x1b\[\?1004([hl])/g
|
|
89
|
+
|
|
90
|
+
for (const match of combined.matchAll(regex)) {
|
|
91
|
+
session.focusReportingEnabled = match[1] === "h"
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
session.modeSequenceTail = combined.slice(-MODE_SEQUENCE_TAIL_LENGTH)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function filterFocusReportInput(data: string, allowFocusReporting: boolean) {
|
|
98
|
+
if (allowFocusReporting || (!data.includes(FOCUS_IN_SEQUENCE) && !data.includes(FOCUS_OUT_SEQUENCE))) {
|
|
99
|
+
return data
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return data.replaceAll(FOCUS_IN_SEQUENCE, "").replaceAll(FOCUS_OUT_SEQUENCE, "")
|
|
103
|
+
}
|
|
104
|
+
|
|
81
105
|
function killTerminalProcessTree(subprocess: Bun.Subprocess | null) {
|
|
82
106
|
if (!subprocess) return
|
|
83
107
|
|
|
@@ -179,6 +203,7 @@ export class TerminalManager {
|
|
|
179
203
|
name: "xterm-256color",
|
|
180
204
|
data: (_terminal, data) => {
|
|
181
205
|
const chunk = Buffer.from(data).toString("utf8")
|
|
206
|
+
updateFocusReportingState(session, chunk)
|
|
182
207
|
headless.write(chunk)
|
|
183
208
|
this.emit({
|
|
184
209
|
type: "terminal.output",
|
|
@@ -189,6 +214,8 @@ export class TerminalManager {
|
|
|
189
214
|
}),
|
|
190
215
|
headless,
|
|
191
216
|
serializeAddon,
|
|
217
|
+
focusReportingEnabled: false,
|
|
218
|
+
modeSequenceTail: "",
|
|
192
219
|
}
|
|
193
220
|
|
|
194
221
|
try {
|
|
@@ -203,7 +230,6 @@ export class TerminalManager {
|
|
|
203
230
|
session.headless.dispose()
|
|
204
231
|
throw error
|
|
205
232
|
}
|
|
206
|
-
|
|
207
233
|
void session.process.exited.then((exitCode) => {
|
|
208
234
|
const active = this.sessions.get(args.terminalId)
|
|
209
235
|
if (!active) return
|
|
@@ -244,18 +270,21 @@ export class TerminalManager {
|
|
|
244
270
|
const session = this.sessions.get(terminalId)
|
|
245
271
|
if (!session || session.status === "exited") return
|
|
246
272
|
|
|
273
|
+
const filteredData = filterFocusReportInput(data, session.focusReportingEnabled)
|
|
274
|
+
if (!filteredData) return
|
|
275
|
+
|
|
247
276
|
let cursor = 0
|
|
248
277
|
|
|
249
|
-
while (cursor <
|
|
250
|
-
const ctrlCIndex =
|
|
278
|
+
while (cursor < filteredData.length) {
|
|
279
|
+
const ctrlCIndex = filteredData.indexOf("\x03", cursor)
|
|
251
280
|
|
|
252
281
|
if (ctrlCIndex === -1) {
|
|
253
|
-
session.terminal.write(
|
|
282
|
+
session.terminal.write(filteredData.slice(cursor))
|
|
254
283
|
return
|
|
255
284
|
}
|
|
256
285
|
|
|
257
286
|
if (ctrlCIndex > cursor) {
|
|
258
|
-
session.terminal.write(
|
|
287
|
+
session.terminal.write(filteredData.slice(cursor, ctrlCIndex))
|
|
259
288
|
}
|
|
260
289
|
|
|
261
290
|
signalTerminalProcessGroup(session.process, "SIGINT")
|