kaizenai 0.1.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.
Files changed (74) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +246 -0
  3. package/bin/kaizen +15 -0
  4. package/dist/client/apple-touch-icon.png +0 -0
  5. package/dist/client/assets/index-D-ORCGrq.js +603 -0
  6. package/dist/client/assets/index-r28mcHqz.css +32 -0
  7. package/dist/client/favicon.png +0 -0
  8. package/dist/client/fonts/body-medium.woff2 +0 -0
  9. package/dist/client/fonts/body-regular-italic.woff2 +0 -0
  10. package/dist/client/fonts/body-regular.woff2 +0 -0
  11. package/dist/client/fonts/body-semibold.woff2 +0 -0
  12. package/dist/client/index.html +22 -0
  13. package/dist/client/manifest-dark.webmanifest +24 -0
  14. package/dist/client/manifest.webmanifest +24 -0
  15. package/dist/client/pwa-192.png +0 -0
  16. package/dist/client/pwa-512.png +0 -0
  17. package/dist/client/pwa-icon.svg +15 -0
  18. package/dist/client/pwa-splash.png +0 -0
  19. package/dist/client/pwa-splash.svg +15 -0
  20. package/package.json +103 -0
  21. package/src/server/acp-shared.ts +315 -0
  22. package/src/server/agent.ts +1120 -0
  23. package/src/server/attachments.ts +133 -0
  24. package/src/server/backgrounds.ts +74 -0
  25. package/src/server/cli-runtime.ts +333 -0
  26. package/src/server/cli-supervisor.ts +81 -0
  27. package/src/server/cli.ts +68 -0
  28. package/src/server/codex-app-server-protocol.ts +453 -0
  29. package/src/server/codex-app-server.ts +1350 -0
  30. package/src/server/cursor-acp.ts +819 -0
  31. package/src/server/discovery.ts +322 -0
  32. package/src/server/event-store.ts +1369 -0
  33. package/src/server/events.ts +244 -0
  34. package/src/server/external-open.ts +272 -0
  35. package/src/server/gemini-acp.ts +844 -0
  36. package/src/server/gemini-cli.ts +525 -0
  37. package/src/server/generate-title.ts +36 -0
  38. package/src/server/git-manager.ts +79 -0
  39. package/src/server/git-repository.ts +101 -0
  40. package/src/server/harness-types.ts +20 -0
  41. package/src/server/keybindings.ts +177 -0
  42. package/src/server/machine-name.ts +22 -0
  43. package/src/server/paths.ts +112 -0
  44. package/src/server/process-utils.ts +22 -0
  45. package/src/server/project-icon.ts +344 -0
  46. package/src/server/project-metadata.ts +10 -0
  47. package/src/server/provider-catalog.ts +85 -0
  48. package/src/server/provider-settings.ts +155 -0
  49. package/src/server/quick-response.ts +153 -0
  50. package/src/server/read-models.ts +275 -0
  51. package/src/server/recovery.ts +507 -0
  52. package/src/server/restart.ts +30 -0
  53. package/src/server/server.ts +244 -0
  54. package/src/server/terminal-manager.ts +350 -0
  55. package/src/server/theme-settings.ts +179 -0
  56. package/src/server/update-manager.ts +230 -0
  57. package/src/server/usage/base-provider-usage.ts +57 -0
  58. package/src/server/usage/claude-usage.ts +558 -0
  59. package/src/server/usage/codex-usage.ts +144 -0
  60. package/src/server/usage/cursor-browser.ts +120 -0
  61. package/src/server/usage/cursor-cookies.ts +390 -0
  62. package/src/server/usage/cursor-usage.ts +490 -0
  63. package/src/server/usage/gemini-usage.ts +24 -0
  64. package/src/server/usage/provider-usage.ts +61 -0
  65. package/src/server/usage/test-helpers.ts +9 -0
  66. package/src/server/usage/types.ts +54 -0
  67. package/src/server/usage/utils.ts +325 -0
  68. package/src/server/ws-router.ts +717 -0
  69. package/src/shared/branding.ts +83 -0
  70. package/src/shared/dev-ports.ts +43 -0
  71. package/src/shared/ports.ts +2 -0
  72. package/src/shared/protocol.ts +152 -0
  73. package/src/shared/tools.ts +251 -0
  74. package/src/shared/types.ts +1028 -0
@@ -0,0 +1,244 @@
1
+ import path from "node:path"
2
+ import { APP_NAME, getRuntimeProfile } from "../shared/branding"
3
+ import { EventStore } from "./event-store"
4
+ import { AgentCoordinator } from "./agent"
5
+ import { ATTACHMENTS_ROUTE_PREFIX, resolveAttachmentPath } from "./attachments"
6
+ import { discoverProjects, type DiscoveredProject } from "./discovery"
7
+ import { GitManager } from "./git-manager"
8
+ import { KeybindingsManager } from "./keybindings"
9
+ import { ProviderSettingsManager } from "./provider-settings"
10
+ import { ThemeSettingsManager } from "./theme-settings"
11
+ import { getMachineDisplayName } from "./machine-name"
12
+ import { listProjectDirectories } from "./paths"
13
+ import { TerminalManager } from "./terminal-manager"
14
+ import { UpdateManager } from "./update-manager"
15
+ import type { UpdateInstallAttemptResult } from "./cli-runtime"
16
+ import { importProjectHistory } from "./recovery"
17
+ import { createWsRouter, type ClientState } from "./ws-router"
18
+ import { getSystemBackgrounds, resolveBackgroundPath } from "./backgrounds"
19
+
20
+ export interface StartKaizenServerOptions {
21
+ port?: number
22
+ host?: string
23
+ strictPort?: boolean
24
+ onMigrationProgress?: (message: string) => void
25
+ update?: {
26
+ version: string
27
+ fetchLatestVersion: (packageName: string) => Promise<string>
28
+ installVersion: (packageName: string, version: string) => UpdateInstallAttemptResult
29
+ }
30
+ }
31
+
32
+ export async function startKaizenServer(options: StartKaizenServerOptions = {}) {
33
+ const port = options.port ?? 3210
34
+ const hostname = options.host ?? "127.0.0.1"
35
+ const strictPort = options.strictPort ?? false
36
+ const store = new EventStore()
37
+ const machineDisplayName = getMachineDisplayName()
38
+ await store.initialize()
39
+ await store.migrateLegacyTranscripts(options.onMigrationProgress)
40
+ for (const project of store.listProjects()) {
41
+ await importProjectHistory({
42
+ store,
43
+ projectId: project.id,
44
+ repoKey: project.repoKey,
45
+ localPath: project.localPath,
46
+ worktreePaths: project.worktreePaths,
47
+ })
48
+ }
49
+ let discoveredProjects: DiscoveredProject[] = []
50
+
51
+ async function refreshDiscovery() {
52
+ discoveredProjects = discoverProjects().filter((project) => !store.isProjectHidden(project.repoKey))
53
+ return discoveredProjects
54
+ }
55
+
56
+ await refreshDiscovery()
57
+
58
+ let server: ReturnType<typeof Bun.serve<ClientState>>
59
+ let router: ReturnType<typeof createWsRouter>
60
+ let agentSnapshotBroadcastTimer: ReturnType<typeof setTimeout> | null = null
61
+ const terminals = new TerminalManager()
62
+ const git = new GitManager()
63
+ const keybindings = new KeybindingsManager()
64
+ await keybindings.initialize()
65
+ const providerSettings = new ProviderSettingsManager()
66
+ await providerSettings.initialize()
67
+ const themeSettings = new ThemeSettingsManager()
68
+ await themeSettings.initialize()
69
+ const updateManager = options.update
70
+ ? new UpdateManager({
71
+ currentVersion: options.update.version,
72
+ fetchLatestVersion: options.update.fetchLatestVersion,
73
+ installVersion: options.update.installVersion,
74
+ devMode: getRuntimeProfile() === "dev",
75
+ })
76
+ : null
77
+ const agent = new AgentCoordinator({
78
+ store,
79
+ onStateChange: () => {
80
+ if (agentSnapshotBroadcastTimer) return
81
+ agentSnapshotBroadcastTimer = setTimeout(() => {
82
+ agentSnapshotBroadcastTimer = null
83
+ router.broadcastSnapshots()
84
+ }, 33)
85
+ },
86
+ attachmentsDir: path.join(store.dataDir, "attachments"),
87
+ })
88
+ router = createWsRouter({
89
+ store,
90
+ agent,
91
+ terminals,
92
+ git,
93
+ keybindings,
94
+ providerSettings,
95
+ themeSettings,
96
+ refreshDiscovery,
97
+ getDiscoveredProjects: () => discoveredProjects,
98
+ machineDisplayName,
99
+ updateManager,
100
+ })
101
+
102
+ const distDir = path.join(import.meta.dir, "..", "..", "dist", "client")
103
+
104
+ const MAX_PORT_ATTEMPTS = 20
105
+ let actualPort = port
106
+
107
+ for (let attempt = 0; attempt < MAX_PORT_ATTEMPTS; attempt++) {
108
+ try {
109
+ server = Bun.serve<ClientState>({
110
+ port: actualPort,
111
+ hostname,
112
+ fetch(req, serverInstance) {
113
+ const url = new URL(req.url)
114
+
115
+ if (url.pathname === "/ws") {
116
+ const upgraded = serverInstance.upgrade(req, {
117
+ data: {
118
+ subscriptions: new Map(),
119
+ },
120
+ })
121
+ return upgraded ? undefined : new Response("WebSocket upgrade failed", { status: 400 })
122
+ }
123
+
124
+ if (url.pathname === "/health") {
125
+ return Response.json({ ok: true, port: actualPort })
126
+ }
127
+
128
+ if (url.pathname === "/api/directories") {
129
+ return serveDirectories(url)
130
+ }
131
+
132
+ if (url.pathname === "/api/backgrounds") {
133
+ return getSystemBackgrounds().then(Response.json)
134
+ }
135
+
136
+ if (url.pathname.startsWith("/api/backgrounds/")) {
137
+ const id = url.pathname.slice("/api/backgrounds/".length)
138
+ return resolveBackgroundPath(id).then(filePath => {
139
+ if (filePath) return new Response(Bun.file(filePath))
140
+ return new Response("Not found", { status: 404 })
141
+ })
142
+ }
143
+
144
+ if (url.pathname.startsWith(`${ATTACHMENTS_ROUTE_PREFIX}/`)) {
145
+ return serveAttachment(path.join(store.dataDir, "attachments"), url.pathname)
146
+ }
147
+
148
+ return serveStatic(distDir, url.pathname)
149
+ },
150
+ websocket: {
151
+ open(ws) {
152
+ router.handleOpen(ws)
153
+ },
154
+ message(ws, raw) {
155
+ router.handleMessage(ws, raw)
156
+ },
157
+ close(ws) {
158
+ router.handleClose(ws)
159
+ },
160
+ },
161
+ })
162
+ break
163
+ } catch (err: unknown) {
164
+ const isAddrInUse =
165
+ err instanceof Error && "code" in err && (err as NodeJS.ErrnoException).code === "EADDRINUSE"
166
+ if (!isAddrInUse || strictPort || attempt === MAX_PORT_ATTEMPTS - 1) {
167
+ throw err
168
+ }
169
+ console.log(`Port ${actualPort} is in use, trying ${actualPort + 1}...`)
170
+ actualPort++
171
+ }
172
+ }
173
+
174
+ const shutdown = async () => {
175
+ for (const chatId of [...agent.activeTurns.keys()]) {
176
+ await agent.shutdown(chatId)
177
+ }
178
+ router.dispose()
179
+ keybindings.dispose()
180
+ providerSettings.dispose()
181
+ themeSettings.dispose()
182
+ terminals.closeAll()
183
+ await store.compact()
184
+ server.stop(true)
185
+ }
186
+
187
+ return {
188
+ port: actualPort,
189
+ store,
190
+ updateManager,
191
+ stop: shutdown,
192
+ }
193
+ }
194
+
195
+ async function serveAttachment(attachmentsDir: string, pathname: string) {
196
+ const relativePath = pathname.slice(`${ATTACHMENTS_ROUTE_PREFIX}/`.length)
197
+ const filePath = resolveAttachmentPath(attachmentsDir, decodeURIComponent(relativePath))
198
+ if (!filePath) {
199
+ return new Response("Invalid attachment path", { status: 400 })
200
+ }
201
+
202
+ const file = Bun.file(filePath)
203
+ if (!(await file.exists())) {
204
+ return new Response("Not Found", { status: 404 })
205
+ }
206
+
207
+ return new Response(file)
208
+ }
209
+
210
+ async function serveDirectories(url: URL) {
211
+ try {
212
+ const localPath = url.searchParams.get("path") ?? undefined
213
+ const snapshot = await listProjectDirectories(localPath)
214
+ return Response.json(snapshot)
215
+ } catch (error) {
216
+ const message = error instanceof Error ? error.message : String(error)
217
+ return Response.json({ error: message }, { status: 400 })
218
+ }
219
+ }
220
+
221
+ async function serveStatic(distDir: string, pathname: string) {
222
+ const requestedPath = pathname === "/" ? "/index.html" : pathname
223
+ const filePath = path.join(distDir, requestedPath)
224
+ const indexPath = path.join(distDir, "index.html")
225
+
226
+ const file = Bun.file(filePath)
227
+ if (await file.exists()) {
228
+ return new Response(file)
229
+ }
230
+
231
+ const indexFile = Bun.file(indexPath)
232
+ if (await indexFile.exists()) {
233
+ return new Response(indexFile, {
234
+ headers: {
235
+ "Content-Type": "text/html; charset=utf-8",
236
+ },
237
+ })
238
+ }
239
+
240
+ return new Response(
241
+ `${APP_NAME} client bundle not found. Run \`bun run build\` inside workbench/ first.`,
242
+ { status: 503 }
243
+ )
244
+ }
@@ -0,0 +1,350 @@
1
+ import path from "node:path"
2
+ import process from "node:process"
3
+ import defaultShell, { detectDefaultShell } from "default-shell"
4
+ import { Terminal } from "@xterm/headless"
5
+ import { SerializeAddon } from "@xterm/addon-serialize"
6
+ import type { TerminalEvent, TerminalSnapshot } from "../shared/protocol"
7
+
8
+ const DEFAULT_COLS = 80
9
+ const DEFAULT_ROWS = 24
10
+ const DEFAULT_SCROLLBACK = 1_000
11
+ const MIN_SCROLLBACK = 500
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
16
+
17
+ interface CreateTerminalArgs {
18
+ projectPath: string
19
+ terminalId: string
20
+ cols: number
21
+ rows: number
22
+ scrollback: number
23
+ }
24
+
25
+ interface TerminalSession {
26
+ terminalId: string
27
+ title: string
28
+ cwd: string
29
+ shell: string
30
+ cols: number
31
+ rows: number
32
+ scrollback: number
33
+ status: "running" | "exited"
34
+ exitCode: number | null
35
+ process: Bun.Subprocess | null
36
+ terminal: Bun.Terminal
37
+ headless: Terminal
38
+ serializeAddon: SerializeAddon
39
+ focusReportingEnabled: boolean
40
+ modeSequenceTail: string
41
+ }
42
+
43
+ function clampScrollback(value: number) {
44
+ if (!Number.isFinite(value)) return DEFAULT_SCROLLBACK
45
+ return Math.min(MAX_SCROLLBACK, Math.max(MIN_SCROLLBACK, Math.round(value)))
46
+ }
47
+
48
+ function normalizeTerminalDimension(value: number, fallback: number) {
49
+ if (!Number.isFinite(value)) return fallback
50
+ return Math.max(1, Math.round(value))
51
+ }
52
+
53
+ function resolveShell() {
54
+ try {
55
+ return detectDefaultShell()
56
+ } catch {
57
+ if (defaultShell) return defaultShell
58
+ if (process.platform === "win32") {
59
+ return process.env.ComSpec || "cmd.exe"
60
+ }
61
+ return process.env.SHELL || "/bin/sh"
62
+ }
63
+ }
64
+
65
+ function resolveShellArgs(shellPath: string) {
66
+ if (process.platform === "win32") {
67
+ return []
68
+ }
69
+
70
+ const shellName = path.basename(shellPath)
71
+ if (["bash", "zsh", "fish", "sh", "ksh"].includes(shellName)) {
72
+ return ["-l"]
73
+ }
74
+
75
+ return []
76
+ }
77
+
78
+ function createTerminalEnv() {
79
+ return {
80
+ ...process.env,
81
+ TERM: "xterm-256color",
82
+ COLORTERM: "truecolor",
83
+ }
84
+ }
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) {
99
+ return data
100
+ }
101
+
102
+ return data.replaceAll(FOCUS_IN_SEQUENCE, "").replaceAll(FOCUS_OUT_SEQUENCE, "")
103
+ }
104
+
105
+ function killTerminalProcessTree(subprocess: Bun.Subprocess | null) {
106
+ if (!subprocess) return
107
+
108
+ const pid = subprocess.pid
109
+ if (typeof pid !== "number") return
110
+
111
+ if (process.platform !== "win32") {
112
+ try {
113
+ process.kill(-pid, "SIGKILL")
114
+ return
115
+ } catch {
116
+ // Fall back to killing only the shell process if group termination fails.
117
+ }
118
+ }
119
+
120
+ try {
121
+ subprocess.kill("SIGKILL")
122
+ } catch {
123
+ // Ignore subprocess shutdown errors during disposal.
124
+ }
125
+ }
126
+
127
+ function signalTerminalProcessGroup(subprocess: Bun.Subprocess | null, signal: NodeJS.Signals) {
128
+ if (!subprocess) return false
129
+
130
+ const pid = subprocess.pid
131
+ if (typeof pid !== "number") return false
132
+
133
+ if (process.platform !== "win32") {
134
+ try {
135
+ process.kill(-pid, signal)
136
+ return true
137
+ } catch {
138
+ // Fall back to signaling only the shell if group signaling fails.
139
+ }
140
+ }
141
+
142
+ try {
143
+ subprocess.kill(signal)
144
+ return true
145
+ } catch {
146
+ return false
147
+ }
148
+ }
149
+
150
+ export class TerminalManager {
151
+ private readonly sessions = new Map<string, TerminalSession>()
152
+ private readonly listeners = new Set<(event: TerminalEvent) => void>()
153
+
154
+ onEvent(listener: (event: TerminalEvent) => void) {
155
+ this.listeners.add(listener)
156
+ return () => {
157
+ this.listeners.delete(listener)
158
+ }
159
+ }
160
+
161
+ createTerminal(args: CreateTerminalArgs) {
162
+ if (process.platform === "win32") {
163
+ throw new Error("Embedded terminal is currently supported on macOS/Linux only.")
164
+ }
165
+ if (typeof Bun.Terminal !== "function") {
166
+ throw new Error("Embedded terminal requires Bun 1.3.5+ with Bun.Terminal support.")
167
+ }
168
+
169
+ const existing = this.sessions.get(args.terminalId)
170
+ if (existing) {
171
+ existing.scrollback = clampScrollback(args.scrollback)
172
+ existing.cols = normalizeTerminalDimension(args.cols, existing.cols)
173
+ existing.rows = normalizeTerminalDimension(args.rows, existing.rows)
174
+ existing.headless.options.scrollback = existing.scrollback
175
+ existing.headless.resize(existing.cols, existing.rows)
176
+ existing.terminal.resize(existing.cols, existing.rows)
177
+ signalTerminalProcessGroup(existing.process, "SIGWINCH")
178
+ return this.snapshotOf(existing)
179
+ }
180
+
181
+ const shell = resolveShell()
182
+ const cols = normalizeTerminalDimension(args.cols, DEFAULT_COLS)
183
+ const rows = normalizeTerminalDimension(args.rows, DEFAULT_ROWS)
184
+ const scrollback = clampScrollback(args.scrollback)
185
+ const title = path.basename(shell) || "shell"
186
+ const headless = new Terminal({ cols, rows, scrollback, allowProposedApi: true })
187
+ const serializeAddon = new SerializeAddon()
188
+ headless.loadAddon(serializeAddon)
189
+
190
+ const session: TerminalSession = {
191
+ terminalId: args.terminalId,
192
+ title,
193
+ cwd: args.projectPath,
194
+ shell,
195
+ cols,
196
+ rows,
197
+ scrollback,
198
+ status: "running",
199
+ exitCode: null,
200
+ process: null,
201
+ terminal: new Bun.Terminal({
202
+ cols,
203
+ rows,
204
+ name: "xterm-256color",
205
+ data: (_terminal, data) => {
206
+ const chunk = Buffer.from(data).toString("utf8")
207
+ updateFocusReportingState(session, chunk)
208
+ headless.write(chunk)
209
+ this.emit({
210
+ type: "terminal.output",
211
+ terminalId: args.terminalId,
212
+ data: chunk,
213
+ })
214
+ },
215
+ }),
216
+ headless,
217
+ serializeAddon,
218
+ focusReportingEnabled: false,
219
+ modeSequenceTail: "",
220
+ }
221
+
222
+ try {
223
+ session.process = Bun.spawn([shell, ...resolveShellArgs(shell)], {
224
+ cwd: args.projectPath,
225
+ env: createTerminalEnv(),
226
+ terminal: session.terminal,
227
+ })
228
+ } catch (error) {
229
+ session.terminal.close()
230
+ session.serializeAddon.dispose()
231
+ session.headless.dispose()
232
+ throw error
233
+ }
234
+ void session.process.exited.then((exitCode) => {
235
+ const active = this.sessions.get(args.terminalId)
236
+ if (!active) return
237
+ active.status = "exited"
238
+ active.exitCode = exitCode
239
+ this.emit({
240
+ type: "terminal.exit",
241
+ terminalId: args.terminalId,
242
+ exitCode,
243
+ })
244
+ }).catch((error) => {
245
+ const active = this.sessions.get(args.terminalId)
246
+ if (!active) return
247
+ active.status = "exited"
248
+ active.exitCode = 1
249
+ this.emit({
250
+ type: "terminal.output",
251
+ terminalId: args.terminalId,
252
+ data: `\r\n[terminal error] ${error instanceof Error ? error.message : String(error)}\r\n`,
253
+ })
254
+ this.emit({
255
+ type: "terminal.exit",
256
+ terminalId: args.terminalId,
257
+ exitCode: 1,
258
+ })
259
+ })
260
+
261
+ this.sessions.set(args.terminalId, session)
262
+ return this.snapshotOf(session)
263
+ }
264
+
265
+ getSnapshot(terminalId: string): TerminalSnapshot | null {
266
+ const session = this.sessions.get(terminalId)
267
+ return session ? this.snapshotOf(session) : null
268
+ }
269
+
270
+ write(terminalId: string, data: string) {
271
+ const session = this.sessions.get(terminalId)
272
+ if (!session || session.status === "exited") return
273
+
274
+ const filteredData = filterFocusReportInput(data, session.focusReportingEnabled)
275
+ if (!filteredData) return
276
+
277
+ let cursor = 0
278
+
279
+ while (cursor < filteredData.length) {
280
+ const ctrlCIndex = filteredData.indexOf("\x03", cursor)
281
+
282
+ if (ctrlCIndex === -1) {
283
+ session.terminal.write(filteredData.slice(cursor))
284
+ return
285
+ }
286
+
287
+ if (ctrlCIndex > cursor) {
288
+ session.terminal.write(filteredData.slice(cursor, ctrlCIndex))
289
+ }
290
+
291
+ signalTerminalProcessGroup(session.process, "SIGINT")
292
+ cursor = ctrlCIndex + 1
293
+ }
294
+ }
295
+
296
+ resize(terminalId: string, cols: number, rows: number) {
297
+ const session = this.sessions.get(terminalId)
298
+ if (!session) return
299
+ session.cols = normalizeTerminalDimension(cols, session.cols)
300
+ session.rows = normalizeTerminalDimension(rows, session.rows)
301
+ session.headless.resize(session.cols, session.rows)
302
+ session.terminal.resize(session.cols, session.rows)
303
+ signalTerminalProcessGroup(session.process, "SIGWINCH")
304
+ }
305
+
306
+ close(terminalId: string) {
307
+ const session = this.sessions.get(terminalId)
308
+ if (!session) return
309
+
310
+ this.sessions.delete(terminalId)
311
+ killTerminalProcessTree(session.process)
312
+ session.terminal.close()
313
+ session.serializeAddon.dispose()
314
+ session.headless.dispose()
315
+ }
316
+
317
+ closeByCwd(cwd: string) {
318
+ for (const [terminalId, session] of this.sessions.entries()) {
319
+ if (session.cwd !== cwd) continue
320
+ this.close(terminalId)
321
+ }
322
+ }
323
+
324
+ closeAll() {
325
+ for (const terminalId of this.sessions.keys()) {
326
+ this.close(terminalId)
327
+ }
328
+ }
329
+
330
+ private snapshotOf(session: TerminalSession): TerminalSnapshot {
331
+ return {
332
+ terminalId: session.terminalId,
333
+ title: session.title,
334
+ cwd: session.cwd,
335
+ shell: session.shell,
336
+ cols: session.cols,
337
+ rows: session.rows,
338
+ scrollback: session.scrollback,
339
+ serializedState: session.serializeAddon.serialize({ scrollback: session.scrollback }),
340
+ status: session.status,
341
+ exitCode: session.exitCode,
342
+ }
343
+ }
344
+
345
+ private emit(event: TerminalEvent) {
346
+ for (const listener of this.listeners) {
347
+ listener(event)
348
+ }
349
+ }
350
+ }