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.
@@ -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-DPinj1Li.js"></script>
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kanna-code",
3
3
  "type": "module",
4
- "version": "0.6.0",
4
+ "version": "0.6.1",
5
5
  "description": "A beautiful web UI for Claude Code",
6
6
  "license": "MIT",
7
7
  "keywords": [
@@ -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 < data.length) {
250
- const ctrlCIndex = data.indexOf("\x03", cursor)
278
+ while (cursor < filteredData.length) {
279
+ const ctrlCIndex = filteredData.indexOf("\x03", cursor)
251
280
 
252
281
  if (ctrlCIndex === -1) {
253
- session.terminal.write(data.slice(cursor))
282
+ session.terminal.write(filteredData.slice(cursor))
254
283
  return
255
284
  }
256
285
 
257
286
  if (ctrlCIndex > cursor) {
258
- session.terminal.write(data.slice(cursor, ctrlCIndex))
287
+ session.terminal.write(filteredData.slice(cursor, ctrlCIndex))
259
288
  }
260
289
 
261
290
  signalTerminalProcessGroup(session.process, "SIGINT")