| author | |
| committer | |
| log | 0133d9582c7bdf047654e4fee532f21655872e68 |
| tree | e5f53fc773cb02dd1e178bba03089baad9c3996a |
| parent | 16b81f72689f4347753762112f76ccd5828fe2fa |
27 files changed, 2214 insertions(+), 241 deletions(-)
apps/server/package.json+1| ... | ... | @@ -17,6 +17,7 @@ |
| 17 | 17 | "test": "vitest run" |
| 18 | 18 | }, |
| 19 | 19 | "dependencies": { |
| 20 | "node-pty": "^1.1.0", | |
| 20 | 21 | "open": "^10.1.0", |
| 21 | 22 | "ws": "^8.18.0" |
| 22 | 23 | }, |
apps/server/src/git.ts+16-4| ... | ... | @@ -12,10 +12,22 @@ import type { |
| 12 | 12 | GitListBranchesInput, |
| 13 | 13 | GitListBranchesResult, |
| 14 | 14 | GitRemoveWorktreeInput, |
| 15 | TerminalCommandInput, | |
| 16 | TerminalCommandResult, | |
| 17 | 15 | } from "@t3tools/contracts"; |
| 18 | 16 | |
| 17 | export interface TerminalCommandInput { | |
| 18 | command: string; | |
| 19 | cwd: string; | |
| 20 | timeoutMs?: number; | |
| 21 | } | |
| 22 | ||
| 23 | export interface TerminalCommandResult { | |
| 24 | stdout: string; | |
| 25 | stderr: string; | |
| 26 | code: number | null; | |
| 27 | signal: NodeJS.Signals | null; | |
| 28 | timedOut: boolean; | |
| 29 | } | |
| 30 | ||
| 19 | 31 | /** Spawn git directly with an argv array — no shell, no quoting needed. */ |
| 20 | 32 | function runGit(args: string[], cwd: string, timeoutMs = 30_000): Promise<TerminalCommandResult> { |
| 21 | 33 | return new Promise((resolve, reject) => { |
| ... | ... | @@ -138,8 +150,8 @@ export async function listGitBranches(input: GitListBranchesInput): Promise<GitL |
| 138 | 150 | let currentPath: string | null = null; |
| 139 | 151 | for (const line of worktreeList.stdout.split("\n")) { |
| 140 | 152 | if (line.startsWith("worktree ")) { |
| 141 | currentPath = line.slice("worktree ".length); | |
| 142 | if (!fs.existsSync(currentPath)) currentPath = null; | |
| 153 | const candidatePath = line.slice("worktree ".length); | |
| 154 | currentPath = fs.existsSync(candidatePath) ? candidatePath : null; | |
| 143 | 155 | } else if (line.startsWith("branch refs/heads/") && currentPath) { |
| 144 | 156 | worktreeMap.set(line.slice("branch refs/heads/".length), currentPath); |
| 145 | 157 | } else if (line === "") { |
apps/server/src/ptyAdapter.ts created+79| ... | ... | @@ -0,0 +1,79 @@ |
| 1 | import * as nodePty from "node-pty"; | |
| 2 | ||
| 3 | export interface PtyExitEvent { | |
| 4 | exitCode: number; | |
| 5 | signal: number | null; | |
| 6 | } | |
| 7 | ||
| 8 | export interface PtyProcess { | |
| 9 | readonly pid: number; | |
| 10 | write(data: string): void; | |
| 11 | resize(cols: number, rows: number): void; | |
| 12 | kill(signal?: string): void; | |
| 13 | onData(callback: (data: string) => void): () => void; | |
| 14 | onExit(callback: (event: PtyExitEvent) => void): () => void; | |
| 15 | } | |
| 16 | ||
| 17 | export interface PtySpawnInput { | |
| 18 | shell: string; | |
| 19 | cwd: string; | |
| 20 | cols: number; | |
| 21 | rows: number; | |
| 22 | env: NodeJS.ProcessEnv; | |
| 23 | } | |
| 24 | ||
| 25 | export interface PtyAdapter { | |
| 26 | spawn(input: PtySpawnInput): PtyProcess; | |
| 27 | } | |
| 28 | ||
| 29 | class NodePtyProcess implements PtyProcess { | |
| 30 | constructor(private readonly process: nodePty.IPty) {} | |
| 31 | ||
| 32 | get pid(): number { | |
| 33 | return this.process.pid; | |
| 34 | } | |
| 35 | ||
| 36 | write(data: string): void { | |
| 37 | this.process.write(data); | |
| 38 | } | |
| 39 | ||
| 40 | resize(cols: number, rows: number): void { | |
| 41 | this.process.resize(cols, rows); | |
| 42 | } | |
| 43 | ||
| 44 | kill(signal?: string): void { | |
| 45 | this.process.kill(signal); | |
| 46 | } | |
| 47 | ||
| 48 | onData(callback: (data: string) => void): () => void { | |
| 49 | const disposable = this.process.onData(callback); | |
| 50 | return () => { | |
| 51 | disposable.dispose(); | |
| 52 | }; | |
| 53 | } | |
| 54 | ||
| 55 | onExit(callback: (event: PtyExitEvent) => void): () => void { | |
| 56 | const disposable = this.process.onExit((event) => { | |
| 57 | callback({ | |
| 58 | exitCode: event.exitCode, | |
| 59 | signal: event.signal ?? null, | |
| 60 | }); | |
| 61 | }); | |
| 62 | return () => { | |
| 63 | disposable.dispose(); | |
| 64 | }; | |
| 65 | } | |
| 66 | } | |
| 67 | ||
| 68 | export class NodePtyAdapter implements PtyAdapter { | |
| 69 | spawn(input: PtySpawnInput): PtyProcess { | |
| 70 | const ptyProcess = nodePty.spawn(input.shell, [], { | |
| 71 | cwd: input.cwd, | |
| 72 | cols: input.cols, | |
| 73 | rows: input.rows, | |
| 74 | env: input.env, | |
| 75 | name: globalThis.process.platform === "win32" ? "xterm-color" : "xterm-256color", | |
| 76 | }); | |
| 77 | return new NodePtyProcess(ptyProcess); | |
| 78 | } | |
| 79 | } |
apps/server/src/terminalManager.test.ts created+247| ... | ... | @@ -0,0 +1,247 @@ |
| 1 | import fs from "node:fs"; | |
| 2 | import os from "node:os"; | |
| 3 | import path from "node:path"; | |
| 4 | ||
| 5 | import type { | |
| 6 | TerminalEvent, | |
| 7 | TerminalOpenInput, | |
| 8 | } from "@t3tools/contracts"; | |
| 9 | import { afterEach, describe, expect, it } from "vitest"; | |
| 10 | ||
| 11 | import type { PtyAdapter, PtyExitEvent, PtyProcess, PtySpawnInput } from "./ptyAdapter"; | |
| 12 | import { TerminalManager } from "./terminalManager"; | |
| 13 | ||
| 14 | class FakePtyProcess implements PtyProcess { | |
| 15 | readonly writes: string[] = []; | |
| 16 | readonly resizeCalls: Array<{ cols: number; rows: number }> = []; | |
| 17 | private readonly dataListeners = new Set<(data: string) => void>(); | |
| 18 | private readonly exitListeners = new Set<(event: PtyExitEvent) => void>(); | |
| 19 | killed = false; | |
| 20 | ||
| 21 | constructor(readonly pid: number) {} | |
| 22 | ||
| 23 | write(data: string): void { | |
| 24 | this.writes.push(data); | |
| 25 | } | |
| 26 | ||
| 27 | resize(cols: number, rows: number): void { | |
| 28 | this.resizeCalls.push({ cols, rows }); | |
| 29 | } | |
| 30 | ||
| 31 | kill(): void { | |
| 32 | this.killed = true; | |
| 33 | } | |
| 34 | ||
| 35 | onData(callback: (data: string) => void): () => void { | |
| 36 | this.dataListeners.add(callback); | |
| 37 | return () => { | |
| 38 | this.dataListeners.delete(callback); | |
| 39 | }; | |
| 40 | } | |
| 41 | ||
| 42 | onExit(callback: (event: PtyExitEvent) => void): () => void { | |
| 43 | this.exitListeners.add(callback); | |
| 44 | return () => { | |
| 45 | this.exitListeners.delete(callback); | |
| 46 | }; | |
| 47 | } | |
| 48 | ||
| 49 | emitData(data: string): void { | |
| 50 | for (const listener of this.dataListeners) { | |
| 51 | listener(data); | |
| 52 | } | |
| 53 | } | |
| 54 | ||
| 55 | emitExit(event: PtyExitEvent): void { | |
| 56 | for (const listener of this.exitListeners) { | |
| 57 | listener(event); | |
| 58 | } | |
| 59 | } | |
| 60 | } | |
| 61 | ||
| 62 | class FakePtyAdapter implements PtyAdapter { | |
| 63 | readonly spawnInputs: PtySpawnInput[] = []; | |
| 64 | readonly processes: FakePtyProcess[] = []; | |
| 65 | private nextPid = 9000; | |
| 66 | ||
| 67 | spawn(input: PtySpawnInput): PtyProcess { | |
| 68 | this.spawnInputs.push(input); | |
| 69 | const process = new FakePtyProcess(this.nextPid++); | |
| 70 | this.processes.push(process); | |
| 71 | return process; | |
| 72 | } | |
| 73 | } | |
| 74 | ||
| 75 | function waitFor(predicate: () => boolean, timeoutMs = 800): Promise<void> { | |
| 76 | const started = Date.now(); | |
| 77 | return new Promise((resolve, reject) => { | |
| 78 | const poll = () => { | |
| 79 | if (predicate()) { | |
| 80 | resolve(); | |
| 81 | return; | |
| 82 | } | |
| 83 | if (Date.now() - started > timeoutMs) { | |
| 84 | reject(new Error("Timed out waiting for condition")); | |
| 85 | return; | |
| 86 | } | |
| 87 | setTimeout(poll, 15); | |
| 88 | }; | |
| 89 | poll(); | |
| 90 | }); | |
| 91 | } | |
| 92 | ||
| 93 | function openInput(overrides: Partial<TerminalOpenInput> = {}): TerminalOpenInput { | |
| 94 | return { | |
| 95 | threadId: "thread-1", | |
| 96 | cwd: process.cwd(), | |
| 97 | cols: 100, | |
| 98 | rows: 24, | |
| 99 | ...overrides, | |
| 100 | }; | |
| 101 | } | |
| 102 | ||
| 103 | describe("TerminalManager", () => { | |
| 104 | const tempDirs: string[] = []; | |
| 105 | ||
| 106 | afterEach(() => { | |
| 107 | for (const dir of tempDirs.splice(0, tempDirs.length)) { | |
| 108 | fs.rmSync(dir, { recursive: true, force: true }); | |
| 109 | } | |
| 110 | }); | |
| 111 | ||
| 112 | function makeManager(historyLineLimit = 5) { | |
| 113 | const logsDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3code-terminal-")); | |
| 114 | tempDirs.push(logsDir); | |
| 115 | const ptyAdapter = new FakePtyAdapter(); | |
| 116 | const manager = new TerminalManager({ | |
| 117 | logsDir, | |
| 118 | ptyAdapter, | |
| 119 | historyLineLimit, | |
| 120 | shellResolver: () => "/bin/bash", | |
| 121 | }); | |
| 122 | return { logsDir, ptyAdapter, manager }; | |
| 123 | } | |
| 124 | ||
| 125 | it("spawns lazily and reuses running terminal per thread", async () => { | |
| 126 | const { manager, ptyAdapter } = makeManager(); | |
| 127 | const first = await manager.open(openInput()); | |
| 128 | const second = await manager.open(openInput()); | |
| 129 | ||
| 130 | expect(first.threadId).toBe("thread-1"); | |
| 131 | expect(second.threadId).toBe("thread-1"); | |
| 132 | expect(ptyAdapter.spawnInputs).toHaveLength(1); | |
| 133 | ||
| 134 | manager.dispose(); | |
| 135 | }); | |
| 136 | ||
| 137 | it("forwards write and resize to active pty process", async () => { | |
| 138 | const { manager, ptyAdapter } = makeManager(); | |
| 139 | await manager.open(openInput()); | |
| 140 | const process = ptyAdapter.processes[0]; | |
| 141 | expect(process).toBeDefined(); | |
| 142 | if (!process) return; | |
| 143 | ||
| 144 | await manager.write({ threadId: "thread-1", data: "ls\n" }); | |
| 145 | await manager.resize({ threadId: "thread-1", cols: 120, rows: 30 }); | |
| 146 | ||
| 147 | expect(process.writes).toEqual(["ls\n"]); | |
| 148 | expect(process.resizeCalls).toEqual([{ cols: 120, rows: 30 }]); | |
| 149 | ||
| 150 | manager.dispose(); | |
| 151 | }); | |
| 152 | ||
| 153 | it("clears transcript and emits cleared event", async () => { | |
| 154 | const { manager, ptyAdapter, logsDir } = makeManager(); | |
| 155 | const events: TerminalEvent[] = []; | |
| 156 | manager.on("event", (event) => { | |
| 157 | events.push(event); | |
| 158 | }); | |
| 159 | await manager.open(openInput()); | |
| 160 | const process = ptyAdapter.processes[0]; | |
| 161 | expect(process).toBeDefined(); | |
| 162 | if (!process) return; | |
| 163 | ||
| 164 | process.emitData("hello\n"); | |
| 165 | await waitFor(() => fs.existsSync(path.join(logsDir, "thread-1.log"))); | |
| 166 | await manager.clear({ threadId: "thread-1" }); | |
| 167 | await waitFor(() => fs.readFileSync(path.join(logsDir, "thread-1.log"), "utf8") === ""); | |
| 168 | ||
| 169 | expect(events.some((event) => event.type === "cleared")).toBe(true); | |
| 170 | ||
| 171 | manager.dispose(); | |
| 172 | }); | |
| 173 | ||
| 174 | it("restarts terminal with empty transcript and respawns pty", async () => { | |
| 175 | const { manager, ptyAdapter, logsDir } = makeManager(); | |
| 176 | await manager.open(openInput()); | |
| 177 | const firstProcess = ptyAdapter.processes[0]; | |
| 178 | expect(firstProcess).toBeDefined(); | |
| 179 | if (!firstProcess) return; | |
| 180 | firstProcess.emitData("before restart\n"); | |
| 181 | await waitFor(() => fs.existsSync(path.join(logsDir, "thread-1.log"))); | |
| 182 | ||
| 183 | const snapshot = await manager.restart(openInput()); | |
| 184 | expect(snapshot.history).toBe(""); | |
| 185 | expect(snapshot.status).toBe("running"); | |
| 186 | expect(ptyAdapter.spawnInputs).toHaveLength(2); | |
| 187 | await waitFor(() => fs.readFileSync(path.join(logsDir, "thread-1.log"), "utf8") === ""); | |
| 188 | ||
| 189 | manager.dispose(); | |
| 190 | }); | |
| 191 | ||
| 192 | it("emits exited event and reopens with clean transcript after exit", async () => { | |
| 193 | const { manager, ptyAdapter, logsDir } = makeManager(); | |
| 194 | const events: TerminalEvent[] = []; | |
| 195 | manager.on("event", (event) => { | |
| 196 | events.push(event); | |
| 197 | }); | |
| 198 | await manager.open(openInput()); | |
| 199 | const process = ptyAdapter.processes[0]; | |
| 200 | expect(process).toBeDefined(); | |
| 201 | if (!process) return; | |
| 202 | process.emitData("old data\n"); | |
| 203 | await waitFor(() => fs.existsSync(path.join(logsDir, "thread-1.log"))); | |
| 204 | process.emitExit({ exitCode: 0, signal: 0 }); | |
| 205 | ||
| 206 | await waitFor(() => events.some((event) => event.type === "exited")); | |
| 207 | const reopened = await manager.open(openInput()); | |
| 208 | ||
| 209 | expect(reopened.history).toBe(""); | |
| 210 | expect(ptyAdapter.spawnInputs).toHaveLength(2); | |
| 211 | expect(fs.readFileSync(path.join(logsDir, "thread-1.log"), "utf8")).toBe(""); | |
| 212 | ||
| 213 | manager.dispose(); | |
| 214 | }); | |
| 215 | ||
| 216 | it("caps persisted history to configured line limit", async () => { | |
| 217 | const { manager, ptyAdapter } = makeManager(3); | |
| 218 | await manager.open(openInput()); | |
| 219 | const process = ptyAdapter.processes[0]; | |
| 220 | expect(process).toBeDefined(); | |
| 221 | if (!process) return; | |
| 222 | ||
| 223 | process.emitData("line1\nline2\nline3\nline4\n"); | |
| 224 | await manager.close({ threadId: "thread-1" }); | |
| 225 | ||
| 226 | const reopened = await manager.open(openInput()); | |
| 227 | const nonEmptyLines = reopened.history.split("\n").filter((line) => line.length > 0); | |
| 228 | expect(nonEmptyLines).toEqual(["line2", "line3", "line4"]); | |
| 229 | ||
| 230 | manager.dispose(); | |
| 231 | }); | |
| 232 | ||
| 233 | it("deletes history file when close(deleteHistory=true)", async () => { | |
| 234 | const { manager, ptyAdapter, logsDir } = makeManager(); | |
| 235 | await manager.open(openInput()); | |
| 236 | const process = ptyAdapter.processes[0]; | |
| 237 | expect(process).toBeDefined(); | |
| 238 | if (!process) return; | |
| 239 | process.emitData("bye\n"); | |
| 240 | await waitFor(() => fs.existsSync(path.join(logsDir, "thread-1.log"))); | |
| 241 | ||
| 242 | await manager.close({ threadId: "thread-1", deleteHistory: true }); | |
| 243 | expect(fs.existsSync(path.join(logsDir, "thread-1.log"))).toBe(false); | |
| 244 | ||
| 245 | manager.dispose(); | |
| 246 | }); | |
| 247 | }); |
apps/server/src/terminalManager.ts created+452| ... | ... | @@ -0,0 +1,452 @@ |
| 1 | import { EventEmitter } from "node:events"; | |
| 2 | import fs from "node:fs"; | |
| 3 | import path from "node:path"; | |
| 4 | ||
| 5 | import { | |
| 6 | type TerminalCloseInput, | |
| 7 | type TerminalEvent, | |
| 8 | type TerminalOpenInput, | |
| 9 | type TerminalSessionSnapshot, | |
| 10 | type TerminalSessionStatus, | |
| 11 | type TerminalThreadInput, | |
| 12 | type TerminalWriteInput, | |
| 13 | terminalCloseInputSchema, | |
| 14 | terminalOpenInputSchema, | |
| 15 | terminalResizeInputSchema, | |
| 16 | terminalThreadInputSchema, | |
| 17 | terminalWriteInputSchema, | |
| 18 | } from "@t3tools/contracts"; | |
| 19 | ||
| 20 | import { createLogger } from "./logger"; | |
| 21 | import { NodePtyAdapter, type PtyAdapter, type PtyExitEvent, type PtyProcess } from "./ptyAdapter"; | |
| 22 | ||
| 23 | const DEFAULT_HISTORY_LINE_LIMIT = 5_000; | |
| 24 | ||
| 25 | export interface TerminalManagerEvents { | |
| 26 | event: [event: TerminalEvent]; | |
| 27 | } | |
| 28 | ||
| 29 | export interface TerminalManagerOptions { | |
| 30 | logsDir?: string; | |
| 31 | historyLineLimit?: number; | |
| 32 | ptyAdapter?: PtyAdapter; | |
| 33 | shellResolver?: () => string; | |
| 34 | } | |
| 35 | ||
| 36 | interface TerminalSessionState { | |
| 37 | threadId: string; | |
| 38 | cwd: string; | |
| 39 | status: TerminalSessionStatus; | |
| 40 | pid: number | null; | |
| 41 | history: string; | |
| 42 | exitCode: number | null; | |
| 43 | exitSignal: number | null; | |
| 44 | updatedAt: string; | |
| 45 | cols: number; | |
| 46 | rows: number; | |
| 47 | process: PtyProcess | null; | |
| 48 | unsubscribeData: (() => void) | null; | |
| 49 | unsubscribeExit: (() => void) | null; | |
| 50 | } | |
| 51 | ||
| 52 | function defaultShellResolver(): string { | |
| 53 | if (process.platform === "win32") { | |
| 54 | return process.env.ComSpec ?? "cmd.exe"; | |
| 55 | } | |
| 56 | return process.env.SHELL ?? "bash"; | |
| 57 | } | |
| 58 | ||
| 59 | function capHistory(history: string, maxLines: number): string { | |
| 60 | if (history.length === 0) return history; | |
| 61 | const hasTrailingNewline = history.endsWith("\n"); | |
| 62 | const lines = history.split("\n"); | |
| 63 | if (hasTrailingNewline) { | |
| 64 | lines.pop(); | |
| 65 | } | |
| 66 | if (lines.length <= maxLines) return history; | |
| 67 | const capped = lines.slice(lines.length - maxLines).join("\n"); | |
| 68 | return hasTrailingNewline ? `${capped}\n` : capped; | |
| 69 | } | |
| 70 | ||
| 71 | function toSafeThreadId(threadId: string): string { | |
| 72 | return threadId.replace(/[^a-zA-Z0-9._-]/g, "_"); | |
| 73 | } | |
| 74 | ||
| 75 | export class TerminalManager extends EventEmitter<TerminalManagerEvents> { | |
| 76 | private readonly sessions = new Map<string, TerminalSessionState>(); | |
| 77 | private readonly logsDir: string; | |
| 78 | private readonly historyLineLimit: number; | |
| 79 | private readonly ptyAdapter: PtyAdapter; | |
| 80 | private readonly shellResolver: () => string; | |
| 81 | private readonly persistQueues = new Map<string, Promise<void>>(); | |
| 82 | private readonly logger = createLogger("terminal"); | |
| 83 | ||
| 84 | constructor(options: TerminalManagerOptions = {}) { | |
| 85 | super(); | |
| 86 | this.logsDir = options.logsDir ?? path.resolve(process.cwd(), ".logs", "terminals"); | |
| 87 | this.historyLineLimit = options.historyLineLimit ?? DEFAULT_HISTORY_LINE_LIMIT; | |
| 88 | this.ptyAdapter = options.ptyAdapter ?? new NodePtyAdapter(); | |
| 89 | this.shellResolver = options.shellResolver ?? defaultShellResolver; | |
| 90 | fs.mkdirSync(this.logsDir, { recursive: true }); | |
| 91 | } | |
| 92 | ||
| 93 | async open(raw: TerminalOpenInput): Promise<TerminalSessionSnapshot> { | |
| 94 | const input = terminalOpenInputSchema.parse(raw); | |
| 95 | await this.assertValidCwd(input.cwd); | |
| 96 | await this.flushPersistQueue(input.threadId); | |
| 97 | ||
| 98 | const existing = this.sessions.get(input.threadId); | |
| 99 | if (!existing) { | |
| 100 | const history = await this.readHistory(input.threadId); | |
| 101 | const session: TerminalSessionState = { | |
| 102 | threadId: input.threadId, | |
| 103 | cwd: input.cwd, | |
| 104 | status: "starting", | |
| 105 | pid: null, | |
| 106 | history, | |
| 107 | exitCode: null, | |
| 108 | exitSignal: null, | |
| 109 | updatedAt: new Date().toISOString(), | |
| 110 | cols: input.cols, | |
| 111 | rows: input.rows, | |
| 112 | process: null, | |
| 113 | unsubscribeData: null, | |
| 114 | unsubscribeExit: null, | |
| 115 | }; | |
| 116 | this.sessions.set(input.threadId, session); | |
| 117 | this.startSession(session, input, "started"); | |
| 118 | return this.snapshot(session); | |
| 119 | } | |
| 120 | ||
| 121 | if (existing.cwd !== input.cwd) { | |
| 122 | this.stopProcess(existing); | |
| 123 | existing.cwd = input.cwd; | |
| 124 | existing.history = ""; | |
| 125 | await this.persistHistory(existing.threadId, existing.history); | |
| 126 | } else if (existing.status === "exited" || existing.status === "error") { | |
| 127 | existing.history = ""; | |
| 128 | await this.persistHistory(existing.threadId, existing.history); | |
| 129 | } | |
| 130 | ||
| 131 | if (!existing.process) { | |
| 132 | this.startSession(existing, input, "started"); | |
| 133 | return this.snapshot(existing); | |
| 134 | } | |
| 135 | ||
| 136 | if (existing.cols !== input.cols || existing.rows !== input.rows) { | |
| 137 | existing.cols = input.cols; | |
| 138 | existing.rows = input.rows; | |
| 139 | existing.process.resize(input.cols, input.rows); | |
| 140 | existing.updatedAt = new Date().toISOString(); | |
| 141 | } | |
| 142 | ||
| 143 | return this.snapshot(existing); | |
| 144 | } | |
| 145 | ||
| 146 | async write(raw: TerminalWriteInput): Promise<void> { | |
| 147 | const input = terminalWriteInputSchema.parse(raw); | |
| 148 | const session = this.requireSession(input.threadId); | |
| 149 | if (!session.process || session.status !== "running") { | |
| 150 | throw new Error(`Terminal is not running for thread: ${input.threadId}`); | |
| 151 | } | |
| 152 | session.process.write(input.data); | |
| 153 | } | |
| 154 | ||
| 155 | async resize(raw: TerminalThreadInput & { cols: number; rows: number }): Promise<void> { | |
| 156 | const input = terminalResizeInputSchema.parse(raw); | |
| 157 | const session = this.requireSession(input.threadId); | |
| 158 | if (!session.process || session.status !== "running") { | |
| 159 | throw new Error(`Terminal is not running for thread: ${input.threadId}`); | |
| 160 | } | |
| 161 | session.cols = input.cols; | |
| 162 | session.rows = input.rows; | |
| 163 | session.updatedAt = new Date().toISOString(); | |
| 164 | session.process.resize(input.cols, input.rows); | |
| 165 | } | |
| 166 | ||
| 167 | async clear(raw: TerminalThreadInput): Promise<void> { | |
| 168 | const input = terminalThreadInputSchema.parse(raw); | |
| 169 | const session = this.requireSession(input.threadId); | |
| 170 | session.history = ""; | |
| 171 | session.updatedAt = new Date().toISOString(); | |
| 172 | await this.persistHistory(input.threadId, session.history); | |
| 173 | this.emitEvent({ | |
| 174 | type: "cleared", | |
| 175 | threadId: input.threadId, | |
| 176 | createdAt: new Date().toISOString(), | |
| 177 | }); | |
| 178 | } | |
| 179 | ||
| 180 | async restart(raw: TerminalOpenInput): Promise<TerminalSessionSnapshot> { | |
| 181 | const input = terminalOpenInputSchema.parse(raw); | |
| 182 | await this.assertValidCwd(input.cwd); | |
| 183 | ||
| 184 | let session = this.sessions.get(input.threadId); | |
| 185 | if (!session) { | |
| 186 | session = { | |
| 187 | threadId: input.threadId, | |
| 188 | cwd: input.cwd, | |
| 189 | status: "starting", | |
| 190 | pid: null, | |
| 191 | history: "", | |
| 192 | exitCode: null, | |
| 193 | exitSignal: null, | |
| 194 | updatedAt: new Date().toISOString(), | |
| 195 | cols: input.cols, | |
| 196 | rows: input.rows, | |
| 197 | process: null, | |
| 198 | unsubscribeData: null, | |
| 199 | unsubscribeExit: null, | |
| 200 | }; | |
| 201 | this.sessions.set(input.threadId, session); | |
| 202 | } else { | |
| 203 | this.stopProcess(session); | |
| 204 | session.cwd = input.cwd; | |
| 205 | } | |
| 206 | ||
| 207 | session.history = ""; | |
| 208 | await this.persistHistory(input.threadId, session.history); | |
| 209 | this.startSession(session, input, "restarted"); | |
| 210 | return this.snapshot(session); | |
| 211 | } | |
| 212 | ||
| 213 | async close(raw: TerminalCloseInput): Promise<void> { | |
| 214 | const input = terminalCloseInputSchema.parse(raw); | |
| 215 | await this.flushPersistQueue(input.threadId); | |
| 216 | const session = this.sessions.get(input.threadId); | |
| 217 | if (session) { | |
| 218 | this.stopProcess(session); | |
| 219 | this.sessions.delete(input.threadId); | |
| 220 | } | |
| 221 | if (input.deleteHistory) { | |
| 222 | await this.deleteHistory(input.threadId); | |
| 223 | } | |
| 224 | } | |
| 225 | ||
| 226 | dispose(): void { | |
| 227 | for (const session of this.sessions.values()) { | |
| 228 | this.stopProcess(session); | |
| 229 | } | |
| 230 | this.sessions.clear(); | |
| 231 | this.persistQueues.clear(); | |
| 232 | } | |
| 233 | ||
| 234 | private startSession( | |
| 235 | session: TerminalSessionState, | |
| 236 | input: TerminalOpenInput, | |
| 237 | eventType: "started" | "restarted", | |
| 238 | ): void { | |
| 239 | this.stopProcess(session); | |
| 240 | ||
| 241 | session.status = "starting"; | |
| 242 | session.cwd = input.cwd; | |
| 243 | session.cols = input.cols; | |
| 244 | session.rows = input.rows; | |
| 245 | session.exitCode = null; | |
| 246 | session.exitSignal = null; | |
| 247 | session.updatedAt = new Date().toISOString(); | |
| 248 | ||
| 249 | const shell = this.shellResolver(); | |
| 250 | try { | |
| 251 | const ptyProcess = this.ptyAdapter.spawn({ | |
| 252 | shell, | |
| 253 | cwd: session.cwd, | |
| 254 | cols: session.cols, | |
| 255 | rows: session.rows, | |
| 256 | env: process.env, | |
| 257 | }); | |
| 258 | session.process = ptyProcess; | |
| 259 | session.pid = ptyProcess.pid; | |
| 260 | session.status = "running"; | |
| 261 | session.updatedAt = new Date().toISOString(); | |
| 262 | session.unsubscribeData = ptyProcess.onData((data) => { | |
| 263 | this.onProcessData(session, data); | |
| 264 | }); | |
| 265 | session.unsubscribeExit = ptyProcess.onExit((event) => { | |
| 266 | this.onProcessExit(session, event); | |
| 267 | }); | |
| 268 | this.emitEvent({ | |
| 269 | type: eventType, | |
| 270 | threadId: session.threadId, | |
| 271 | createdAt: new Date().toISOString(), | |
| 272 | snapshot: this.snapshot(session), | |
| 273 | }); | |
| 274 | } catch (error) { | |
| 275 | session.status = "error"; | |
| 276 | session.pid = null; | |
| 277 | session.process = null; | |
| 278 | session.updatedAt = new Date().toISOString(); | |
| 279 | const message = error instanceof Error ? error.message : "Terminal start failed"; | |
| 280 | this.emitEvent({ | |
| 281 | type: "error", | |
| 282 | threadId: session.threadId, | |
| 283 | createdAt: new Date().toISOString(), | |
| 284 | message, | |
| 285 | }); | |
| 286 | this.logger.error("failed to start terminal", { | |
| 287 | threadId: session.threadId, | |
| 288 | error: message, | |
| 289 | }); | |
| 290 | } | |
| 291 | } | |
| 292 | ||
| 293 | private onProcessData(session: TerminalSessionState, data: string): void { | |
| 294 | session.history = capHistory(`${session.history}${data}`, this.historyLineLimit); | |
| 295 | session.updatedAt = new Date().toISOString(); | |
| 296 | this.queuePersist(session.threadId, session.history); | |
| 297 | this.emitEvent({ | |
| 298 | type: "output", | |
| 299 | threadId: session.threadId, | |
| 300 | createdAt: new Date().toISOString(), | |
| 301 | data, | |
| 302 | }); | |
| 303 | } | |
| 304 | ||
| 305 | private onProcessExit(session: TerminalSessionState, event: PtyExitEvent): void { | |
| 306 | this.cleanupProcessHandles(session); | |
| 307 | session.process = null; | |
| 308 | session.pid = null; | |
| 309 | session.status = "exited"; | |
| 310 | session.exitCode = Number.isInteger(event.exitCode) ? event.exitCode : null; | |
| 311 | session.exitSignal = Number.isInteger(event.signal) ? event.signal : null; | |
| 312 | session.updatedAt = new Date().toISOString(); | |
| 313 | this.emitEvent({ | |
| 314 | type: "exited", | |
| 315 | threadId: session.threadId, | |
| 316 | createdAt: new Date().toISOString(), | |
| 317 | exitCode: session.exitCode, | |
| 318 | exitSignal: session.exitSignal, | |
| 319 | }); | |
| 320 | } | |
| 321 | ||
| 322 | private stopProcess(session: TerminalSessionState): void { | |
| 323 | const process = session.process; | |
| 324 | if (!process) return; | |
| 325 | this.cleanupProcessHandles(session); | |
| 326 | session.process = null; | |
| 327 | session.pid = null; | |
| 328 | session.status = "exited"; | |
| 329 | session.updatedAt = new Date().toISOString(); | |
| 330 | try { | |
| 331 | process.kill(); | |
| 332 | } catch (error) { | |
| 333 | const message = error instanceof Error ? error.message : String(error); | |
| 334 | this.logger.warn("failed to kill terminal process", { | |
| 335 | threadId: session.threadId, | |
| 336 | error: message, | |
| 337 | }); | |
| 338 | } | |
| 339 | } | |
| 340 | ||
| 341 | private cleanupProcessHandles(session: TerminalSessionState): void { | |
| 342 | session.unsubscribeData?.(); | |
| 343 | session.unsubscribeData = null; | |
| 344 | session.unsubscribeExit?.(); | |
| 345 | session.unsubscribeExit = null; | |
| 346 | } | |
| 347 | ||
| 348 | private queuePersist(threadId: string, history: string): void { | |
| 349 | const task = async () => { | |
| 350 | await fs.promises.writeFile(this.historyPath(threadId), history, "utf8"); | |
| 351 | }; | |
| 352 | const previous = this.persistQueues.get(threadId) ?? Promise.resolve(); | |
| 353 | const next = previous.catch(() => undefined).then(task); | |
| 354 | this.persistQueues.set(threadId, next); | |
| 355 | void next.finally(() => { | |
| 356 | if (this.persistQueues.get(threadId) === next) { | |
| 357 | this.persistQueues.delete(threadId); | |
| 358 | } | |
| 359 | }); | |
| 360 | } | |
| 361 | ||
| 362 | private async persistHistory(threadId: string, history: string): Promise<void> { | |
| 363 | const task = async () => { | |
| 364 | await fs.promises.writeFile(this.historyPath(threadId), history, "utf8"); | |
| 365 | }; | |
| 366 | const previous = this.persistQueues.get(threadId) ?? Promise.resolve(); | |
| 367 | const next = previous.catch(() => undefined).then(task); | |
| 368 | this.persistQueues.set(threadId, next); | |
| 369 | await next.finally(() => { | |
| 370 | if (this.persistQueues.get(threadId) === next) { | |
| 371 | this.persistQueues.delete(threadId); | |
| 372 | } | |
| 373 | }); | |
| 374 | } | |
| 375 | ||
| 376 | private async readHistory(threadId: string): Promise<string> { | |
| 377 | try { | |
| 378 | const raw = await fs.promises.readFile(this.historyPath(threadId), "utf8"); | |
| 379 | const capped = capHistory(raw, this.historyLineLimit); | |
| 380 | if (capped !== raw) { | |
| 381 | await fs.promises.writeFile(this.historyPath(threadId), capped, "utf8"); | |
| 382 | } | |
| 383 | return capped; | |
| 384 | } catch (error) { | |
| 385 | if ((error as NodeJS.ErrnoException).code === "ENOENT") { | |
| 386 | return ""; | |
| 387 | } | |
| 388 | throw error; | |
| 389 | } | |
| 390 | } | |
| 391 | ||
| 392 | private async deleteHistory(threadId: string): Promise<void> { | |
| 393 | try { | |
| 394 | await fs.promises.rm(this.historyPath(threadId), { force: true }); | |
| 395 | } catch (error) { | |
| 396 | this.logger.warn("failed to delete terminal history", { | |
| 397 | threadId, | |
| 398 | error: error instanceof Error ? error.message : String(error), | |
| 399 | }); | |
| 400 | } | |
| 401 | } | |
| 402 | ||
| 403 | private async flushPersistQueue(threadId: string): Promise<void> { | |
| 404 | const pending = this.persistQueues.get(threadId); | |
| 405 | if (!pending) return; | |
| 406 | await pending.catch(() => undefined); | |
| 407 | } | |
| 408 | ||
| 409 | private async assertValidCwd(cwd: string): Promise<void> { | |
| 410 | let stats: fs.Stats; | |
| 411 | try { | |
| 412 | stats = await fs.promises.stat(cwd); | |
| 413 | } catch (error) { | |
| 414 | if ((error as NodeJS.ErrnoException).code === "ENOENT") { | |
| 415 | throw new Error(`Terminal cwd does not exist: ${cwd}`, { cause: error }); | |
| 416 | } | |
| 417 | throw error; | |
| 418 | } | |
| 419 | if (!stats.isDirectory()) { | |
| 420 | throw new Error(`Terminal cwd is not a directory: ${cwd}`); | |
| 421 | } | |
| 422 | } | |
| 423 | ||
| 424 | private requireSession(threadId: string): TerminalSessionState { | |
| 425 | const session = this.sessions.get(threadId); | |
| 426 | if (!session) { | |
| 427 | throw new Error(`Unknown terminal thread: ${threadId}`); | |
| 428 | } | |
| 429 | return session; | |
| 430 | } | |
| 431 | ||
| 432 | private snapshot(session: TerminalSessionState): TerminalSessionSnapshot { | |
| 433 | return { | |
| 434 | threadId: session.threadId, | |
| 435 | cwd: session.cwd, | |
| 436 | status: session.status, | |
| 437 | pid: session.pid, | |
| 438 | history: session.history, | |
| 439 | exitCode: session.exitCode, | |
| 440 | exitSignal: session.exitSignal, | |
| 441 | updatedAt: session.updatedAt, | |
| 442 | }; | |
| 443 | } | |
| 444 | ||
| 445 | private emitEvent(event: TerminalEvent): void { | |
| 446 | this.emit("event", event); | |
| 447 | } | |
| 448 | ||
| 449 | private historyPath(threadId: string): string { | |
| 450 | return path.join(this.logsDir, `${toSafeThreadId(threadId)}.log`); | |
| 451 | } | |
| 452 | } |
apps/server/src/wsServer.test.ts+188| ... | ... | @@ -1,6 +1,7 @@ |
| 1 | 1 | import fs from "node:fs"; |
| 2 | 2 | import os from "node:os"; |
| 3 | 3 | import path from "node:path"; |
| 4 | import { EventEmitter } from "node:events"; | |
| 4 | 5 | |
| 5 | 6 | import { describe, expect, it, afterEach, vi } from "vitest"; |
| 6 | 7 | import { createServer } from "./wsServer"; |
| ... | ... | @@ -8,6 +9,15 @@ import WebSocket from "ws"; |
| 8 | 9 | |
| 9 | 10 | import { WS_CHANNELS, WS_METHODS, type WsPush, type WsResponse } from "@t3tools/contracts"; |
| 10 | 11 | import { ProjectRegistry } from "./projectRegistry"; |
| 12 | import type { | |
| 13 | TerminalCloseInput, | |
| 14 | TerminalEvent, | |
| 15 | TerminalOpenInput, | |
| 16 | TerminalSessionSnapshot, | |
| 17 | TerminalThreadInput, | |
| 18 | TerminalWriteInput, | |
| 19 | } from "@t3tools/contracts"; | |
| 20 | import type { TerminalManager } from "./terminalManager"; | |
| 11 | 21 | |
| 12 | 22 | interface PendingMessages { |
| 13 | 23 | queue: unknown[]; |
| ... | ... | @@ -16,6 +26,91 @@ interface PendingMessages { |
| 16 | 26 | |
| 17 | 27 | const pendingBySocket = new WeakMap<WebSocket, PendingMessages>(); |
| 18 | 28 | |
| 29 | class MockTerminalManager extends EventEmitter<{ event: [event: TerminalEvent] }> { | |
| 30 | private readonly sessions = new Map<string, TerminalSessionSnapshot>(); | |
| 31 | ||
| 32 | async open(input: TerminalOpenInput): Promise<TerminalSessionSnapshot> { | |
| 33 | const now = new Date().toISOString(); | |
| 34 | const snapshot: TerminalSessionSnapshot = { | |
| 35 | threadId: input.threadId, | |
| 36 | cwd: input.cwd, | |
| 37 | status: "running", | |
| 38 | pid: 4242, | |
| 39 | history: "", | |
| 40 | exitCode: null, | |
| 41 | exitSignal: null, | |
| 42 | updatedAt: now, | |
| 43 | }; | |
| 44 | this.sessions.set(input.threadId, snapshot); | |
| 45 | queueMicrotask(() => { | |
| 46 | this.emit("event", { | |
| 47 | type: "started", | |
| 48 | threadId: input.threadId, | |
| 49 | createdAt: now, | |
| 50 | snapshot, | |
| 51 | }); | |
| 52 | }); | |
| 53 | return snapshot; | |
| 54 | } | |
| 55 | ||
| 56 | async write(input: TerminalWriteInput): Promise<void> { | |
| 57 | const existing = this.sessions.get(input.threadId); | |
| 58 | if (!existing) { | |
| 59 | throw new Error(`Unknown terminal thread: ${input.threadId}`); | |
| 60 | } | |
| 61 | queueMicrotask(() => { | |
| 62 | this.emit("event", { | |
| 63 | type: "output", | |
| 64 | threadId: input.threadId, | |
| 65 | createdAt: new Date().toISOString(), | |
| 66 | data: input.data, | |
| 67 | }); | |
| 68 | }); | |
| 69 | } | |
| 70 | ||
| 71 | async resize(_input: TerminalThreadInput & { cols: number; rows: number }): Promise<void> {} | |
| 72 | ||
| 73 | async clear(input: TerminalThreadInput): Promise<void> { | |
| 74 | queueMicrotask(() => { | |
| 75 | this.emit("event", { | |
| 76 | type: "cleared", | |
| 77 | threadId: input.threadId, | |
| 78 | createdAt: new Date().toISOString(), | |
| 79 | }); | |
| 80 | }); | |
| 81 | } | |
| 82 | ||
| 83 | async restart(input: TerminalOpenInput): Promise<TerminalSessionSnapshot> { | |
| 84 | const now = new Date().toISOString(); | |
| 85 | const snapshot: TerminalSessionSnapshot = { | |
| 86 | threadId: input.threadId, | |
| 87 | cwd: input.cwd, | |
| 88 | status: "running", | |
| 89 | pid: 5252, | |
| 90 | history: "", | |
| 91 | exitCode: null, | |
| 92 | exitSignal: null, | |
| 93 | updatedAt: now, | |
| 94 | }; | |
| 95 | this.sessions.set(input.threadId, snapshot); | |
| 96 | queueMicrotask(() => { | |
| 97 | this.emit("event", { | |
| 98 | type: "restarted", | |
| 99 | threadId: input.threadId, | |
| 100 | createdAt: now, | |
| 101 | snapshot, | |
| 102 | }); | |
| 103 | }); | |
| 104 | return snapshot; | |
| 105 | } | |
| 106 | ||
| 107 | async close(input: TerminalCloseInput): Promise<void> { | |
| 108 | this.sessions.delete(input.threadId); | |
| 109 | } | |
| 110 | ||
| 111 | dispose(): void {} | |
| 112 | } | |
| 113 | ||
| 19 | 114 | function connectWs(port: number, token?: string): Promise<WebSocket> { |
| 20 | 115 | return new Promise((resolve, reject) => { |
| 21 | 116 | const query = token ? `?token=${encodeURIComponent(token)}` : ""; |
| ... | ... | @@ -85,6 +180,7 @@ describe("WebSocket Server", () => { |
| 85 | 180 | devUrl?: string; |
| 86 | 181 | authToken?: string; |
| 87 | 182 | stateDir?: string; |
| 183 | terminalManager?: TerminalManager; | |
| 88 | 184 | } = {}, |
| 89 | 185 | ): ReturnType<typeof createServer> { |
| 90 | 186 | const stateDir = options.stateDir ?? makeTempDir("t3code-ws-state-"); |
| ... | ... | @@ -94,6 +190,9 @@ describe("WebSocket Server", () => { |
| 94 | 190 | ...(options.devUrl ? { devUrl: options.devUrl } : {}), |
| 95 | 191 | ...(options.authToken ? { authToken: options.authToken } : {}), |
| 96 | 192 | projectRegistry: new ProjectRegistry(stateDir), |
| 193 | ...(options.terminalManager | |
| 194 | ? { terminalManager: options.terminalManager } | |
| 195 | : {}), | |
| 97 | 196 | }); |
| 98 | 197 | } |
| 99 | 198 | |
| ... | ... | @@ -213,6 +312,95 @@ describe("WebSocket Server", () => { |
| 213 | 312 | expect(response.result).toEqual([]); |
| 214 | 313 | }); |
| 215 | 314 | |
| 315 | it("routes terminal RPC methods and broadcasts terminal events", async () => { | |
| 316 | const cwd = makeTempDir("t3code-ws-terminal-cwd-"); | |
| 317 | const terminalManager = new MockTerminalManager(); | |
| 318 | server = createTestServer({ | |
| 319 | cwd: "/test", | |
| 320 | terminalManager: terminalManager as unknown as TerminalManager, | |
| 321 | }); | |
| 322 | await server.start(); | |
| 323 | const addr = server.httpServer.address(); | |
| 324 | const port = typeof addr === "object" && addr !== null ? addr.port : 0; | |
| 325 | ||
| 326 | const ws = await connectWs(port); | |
| 327 | connections.push(ws); | |
| 328 | await waitForMessage(ws); | |
| 329 | ||
| 330 | const open = await sendRequest(ws, WS_METHODS.terminalOpen, { | |
| 331 | threadId: "thread-1", | |
| 332 | cwd, | |
| 333 | cols: 100, | |
| 334 | rows: 24, | |
| 335 | }); | |
| 336 | expect(open.error).toBeUndefined(); | |
| 337 | expect((open.result as TerminalSessionSnapshot).threadId).toBe("thread-1"); | |
| 338 | ||
| 339 | const write = await sendRequest(ws, WS_METHODS.terminalWrite, { | |
| 340 | threadId: "thread-1", | |
| 341 | data: "echo hello\n", | |
| 342 | }); | |
| 343 | expect(write.error).toBeUndefined(); | |
| 344 | ||
| 345 | const resize = await sendRequest(ws, WS_METHODS.terminalResize, { | |
| 346 | threadId: "thread-1", | |
| 347 | cols: 120, | |
| 348 | rows: 30, | |
| 349 | }); | |
| 350 | expect(resize.error).toBeUndefined(); | |
| 351 | ||
| 352 | const clear = await sendRequest(ws, WS_METHODS.terminalClear, { | |
| 353 | threadId: "thread-1", | |
| 354 | }); | |
| 355 | expect(clear.error).toBeUndefined(); | |
| 356 | ||
| 357 | const restart = await sendRequest(ws, WS_METHODS.terminalRestart, { | |
| 358 | threadId: "thread-1", | |
| 359 | cwd, | |
| 360 | cols: 120, | |
| 361 | rows: 30, | |
| 362 | }); | |
| 363 | expect(restart.error).toBeUndefined(); | |
| 364 | ||
| 365 | const close = await sendRequest(ws, WS_METHODS.terminalClose, { | |
| 366 | threadId: "thread-1", | |
| 367 | deleteHistory: true, | |
| 368 | }); | |
| 369 | expect(close.error).toBeUndefined(); | |
| 370 | ||
| 371 | const manualEvent: TerminalEvent = { | |
| 372 | type: "output", | |
| 373 | threadId: "thread-1", | |
| 374 | createdAt: new Date().toISOString(), | |
| 375 | data: "manual test output\n", | |
| 376 | }; | |
| 377 | terminalManager.emit("event", manualEvent); | |
| 378 | ||
| 379 | const push = (await waitForMessage(ws)) as WsPush; | |
| 380 | expect(push.type).toBe("push"); | |
| 381 | expect(push.channel).toBe(WS_CHANNELS.terminalEvent); | |
| 382 | expect((push.data as TerminalEvent).type).toBe("output"); | |
| 383 | }); | |
| 384 | ||
| 385 | it("returns validation errors for invalid terminal open params", async () => { | |
| 386 | server = createTestServer({ cwd: "/test" }); | |
| 387 | await server.start(); | |
| 388 | const addr = server.httpServer.address(); | |
| 389 | const port = typeof addr === "object" && addr !== null ? addr.port : 0; | |
| 390 | ||
| 391 | const ws = await connectWs(port); | |
| 392 | connections.push(ws); | |
| 393 | await waitForMessage(ws); | |
| 394 | ||
| 395 | const response = await sendRequest(ws, WS_METHODS.terminalOpen, { | |
| 396 | threadId: "", | |
| 397 | cwd: "", | |
| 398 | cols: 1, | |
| 399 | rows: 1, | |
| 400 | }); | |
| 401 | expect(response.error).toBeDefined(); | |
| 402 | }); | |
| 403 | ||
| 216 | 404 | it("handles invalid JSON gracefully", async () => { |
| 217 | 405 | server = createTestServer({ cwd: "/test" }); |
| 218 | 406 | await server.start(); |
apps/server/src/wsServer.ts+44| ... | ... | @@ -27,6 +27,7 @@ import { |
| 27 | 27 | listGitBranches, |
| 28 | 28 | removeGitWorktree, |
| 29 | 29 | } from "./git"; |
| 30 | import { TerminalManager } from "./terminalManager"; | |
| 30 | 31 | |
| 31 | 32 | const MIME_TYPES: Record<string, string> = { |
| 32 | 33 | ".html": "text/html; charset=utf-8", |
| ... | ... | @@ -51,6 +52,7 @@ export interface ServerOptions { |
| 51 | 52 | devUrl?: string | undefined; |
| 52 | 53 | logWebSocketEvents?: boolean | undefined; |
| 53 | 54 | projectRegistry?: ProjectRegistry | undefined; |
| 55 | terminalManager?: TerminalManager | undefined; | |
| 54 | 56 | authToken?: string | undefined; |
| 55 | 57 | } |
| 56 | 58 | |
| ... | ... | @@ -71,9 +73,11 @@ export function createServer(options: ServerOptions) { |
| 71 | 73 | devUrl, |
| 72 | 74 | logWebSocketEvents: explicitLogWsEvents, |
| 73 | 75 | projectRegistry: providedRegistry, |
| 76 | terminalManager: providedTerminalManager, | |
| 74 | 77 | authToken, |
| 75 | 78 | } = options; |
| 76 | 79 | const providerManager = new ProviderManager(); |
| 80 | const terminalManager = providedTerminalManager ?? new TerminalManager(); | |
| 77 | 81 | const projectRegistry = |
| 78 | 82 | providedRegistry ?? new ProjectRegistry(path.join(os.homedir(), ".t3", "userdata")); |
| 79 | 83 | const clients = new Set<WebSocket>(); |
| ... | ... | @@ -108,6 +112,23 @@ export function createServer(options: ServerOptions) { |
| 108 | 112 | logOutgoingPush(push, recipients); |
| 109 | 113 | }); |
| 110 | 114 | |
| 115 | terminalManager.on("event", (event) => { | |
| 116 | const push: WsPush = { | |
| 117 | type: "push", | |
| 118 | channel: WS_CHANNELS.terminalEvent, | |
| 119 | data: event, | |
| 120 | }; | |
| 121 | const message = JSON.stringify(push); | |
| 122 | let recipients = 0; | |
| 123 | for (const client of clients) { | |
| 124 | if (client.readyState === client.OPEN) { | |
| 125 | client.send(message); | |
| 126 | recipients += 1; | |
| 127 | } | |
| 128 | } | |
| 129 | logOutgoingPush(push, recipients); | |
| 130 | }); | |
| 131 | ||
| 111 | 132 | // HTTP server — serves static files or redirects to Vite dev server |
| 112 | 133 | const httpServer = http.createServer((req, res) => { |
| 113 | 134 | // In dev mode, redirect to Vite dev server |
| ... | ... | @@ -330,6 +351,28 @@ export function createServer(options: ServerOptions) { |
| 330 | 351 | case WS_METHODS.gitInit: |
| 331 | 352 | return initGitRepo(request.params as never); |
| 332 | 353 | |
| 354 | case WS_METHODS.terminalOpen: | |
| 355 | return terminalManager.open(request.params as never); | |
| 356 | ||
| 357 | case WS_METHODS.terminalWrite: | |
| 358 | await terminalManager.write(request.params as never); | |
| 359 | return undefined; | |
| 360 | ||
| 361 | case WS_METHODS.terminalResize: | |
| 362 | await terminalManager.resize(request.params as never); | |
| 363 | return undefined; | |
| 364 | ||
| 365 | case WS_METHODS.terminalClear: | |
| 366 | await terminalManager.clear(request.params as never); | |
| 367 | return undefined; | |
| 368 | ||
| 369 | case WS_METHODS.terminalRestart: | |
| 370 | return terminalManager.restart(request.params as never); | |
| 371 | ||
| 372 | case WS_METHODS.terminalClose: | |
| 373 | await terminalManager.close(request.params as never); | |
| 374 | return undefined; | |
| 375 | ||
| 333 | 376 | case WS_METHODS.serverGetConfig: |
| 334 | 377 | return { cwd }; |
| 335 | 378 | |
| ... | ... | @@ -360,6 +403,7 @@ export function createServer(options: ServerOptions) { |
| 360 | 403 | async function stop(): Promise<void> { |
| 361 | 404 | providerManager.stopAll(); |
| 362 | 405 | providerManager.dispose(); |
| 406 | terminalManager.dispose(); | |
| 363 | 407 | |
| 364 | 408 | for (const client of clients) { |
| 365 | 409 | client.close(); |
apps/web/package.json+2| ... | ... | @@ -11,6 +11,8 @@ |
| 11 | 11 | "test": "vitest run --passWithNoTests" |
| 12 | 12 | }, |
| 13 | 13 | "dependencies": { |
| 14 | "@xterm/addon-fit": "^0.11.0", | |
| 15 | "@xterm/xterm": "^6.0.0", | |
| 14 | 16 | "@t3tools/contracts": "workspace:*", |
| 15 | 17 | "@tanstack/react-query": "^5.90.0", |
| 16 | 18 | "highlight.js": "^11.11.1", |
apps/web/src/App.tsx+15| ... | ... | @@ -8,6 +8,7 @@ import { isElectron } from "./env"; |
| 8 | 8 | import { DEFAULT_MODEL } from "./model-logic"; |
| 9 | 9 | import { readNativeApi } from "./session-logic"; |
| 10 | 10 | import { StoreProvider, useStore } from "./store"; |
| 11 | import { DEFAULT_THREAD_TERMINAL_HEIGHT } from "./types"; | |
| 11 | 12 | import { onServerWelcome } from "./wsNativeApi"; |
| 12 | 13 | |
| 13 | 14 | function EventRouter() { |
| ... | ... | @@ -26,6 +27,18 @@ function EventRouter() { |
| 26 | 27 | }); |
| 27 | 28 | }, [api, dispatch]); |
| 28 | 29 | |
| 30 | useEffect(() => { | |
| 31 | if (!api) return; | |
| 32 | return api.terminal.onEvent((event) => { | |
| 33 | if (event.type !== "exited") return; | |
| 34 | dispatch({ | |
| 35 | type: "SET_THREAD_TERMINAL_OPEN", | |
| 36 | threadId: event.threadId, | |
| 37 | open: false, | |
| 38 | }); | |
| 39 | }); | |
| 40 | }, [api, dispatch]); | |
| 41 | ||
| 29 | 42 | return null; |
| 30 | 43 | } |
| 31 | 44 | |
| ... | ... | @@ -78,6 +91,8 @@ function AutoProjectBootstrap() { |
| 78 | 91 | projectId, |
| 79 | 92 | title: "New thread", |
| 80 | 93 | model: DEFAULT_MODEL, |
| 94 | terminalOpen: false, | |
| 95 | terminalHeight: DEFAULT_THREAD_TERMINAL_HEIGHT, | |
| 81 | 96 | session: null, |
| 82 | 97 | messages: [], |
| 83 | 98 | events: [], |
apps/web/src/components/ChatView.tsx+93| ... | ... | @@ -7,6 +7,7 @@ import { |
| 7 | 7 | type FormEvent, |
| 8 | 8 | Fragment, |
| 9 | 9 | type KeyboardEvent, |
| 10 | useCallback, | |
| 10 | 11 | useEffect, |
| 11 | 12 | useMemo, |
| 12 | 13 | useRef, |
| ... | ... | @@ -34,7 +35,9 @@ import { |
| 34 | 35 | } from "../session-logic"; |
| 35 | 36 | import { useStore } from "../store"; |
| 36 | 37 | import BranchToolbar from "./BranchToolbar"; |
| 38 | import { isTerminalToggleShortcut } from "../terminal-shortcuts"; | |
| 37 | 39 | import ChatMarkdown from "./ChatMarkdown"; |
| 40 | import ThreadTerminalDrawer from "./ThreadTerminalDrawer"; | |
| 38 | 41 | |
| 39 | 42 | function formatMessageMeta(createdAt: string, duration: string | null): string { |
| 40 | 43 | if (!duration) return formatTimestamp(createdAt); |
| ... | ... | @@ -145,12 +148,15 @@ export default function ChatView() { |
| 145 | 148 | const [respondingRequestIds, setRespondingRequestIds] = useState<string[]>([]); |
| 146 | 149 | const [expandedWorkGroups, setExpandedWorkGroups] = useState<Record<string, boolean>>({}); |
| 147 | 150 | const [nowTick, setNowTick] = useState(() => Date.now()); |
| 151 | const [terminalFocusRequestId, setTerminalFocusRequestId] = useState(0); | |
| 148 | 152 | const messagesEndRef = useRef<HTMLDivElement>(null); |
| 149 | 153 | const textareaRef = useRef<HTMLTextAreaElement>(null); |
| 150 | 154 | const modelMenuRef = useRef<HTMLDivElement>(null); |
| 151 | 155 | const editorMenuRef = useRef<HTMLDivElement>(null); |
| 156 | const terminalOpenByThreadRef = useRef<Record<string, boolean>>({}); | |
| 152 | 157 | |
| 153 | 158 | const activeThread = state.threads.find((t) => t.id === state.activeThreadId); |
| 159 | const activeThreadId = activeThread?.id ?? null; | |
| 154 | 160 | const activeProject = state.projects.find((p) => p.id === activeThread?.projectId); |
| 155 | 161 | const selectedModel = resolveModelSlug( |
| 156 | 162 | activeThread?.model ?? activeProject?.model ?? DEFAULT_MODEL, |
| ... | ... | @@ -255,6 +261,25 @@ export default function ChatView() { |
| 255 | 261 | (activeThread.messages.length > 0 || |
| 256 | 262 | (activeThread.session !== null && activeThread.session.status !== "closed")), |
| 257 | 263 | ); |
| 264 | const terminalShortcutHint = navigator.platform.includes("Mac") | |
| 265 | ? "\u2318J" | |
| 266 | : "Ctrl+J"; | |
| 267 | const focusComposer = useCallback(() => { | |
| 268 | const textarea = textareaRef.current; | |
| 269 | if (!textarea) return; | |
| 270 | textarea.focus(); | |
| 271 | const cursor = textarea.value.length; | |
| 272 | textarea.setSelectionRange(cursor, cursor); | |
| 273 | }, []); | |
| 274 | const toggleTerminalVisibility = useCallback(() => { | |
| 275 | if (!activeThreadId) return; | |
| 276 | const isOpen = Boolean(activeThread?.terminalOpen); | |
| 277 | dispatch({ | |
| 278 | type: "SET_THREAD_TERMINAL_OPEN", | |
| 279 | threadId: activeThreadId, | |
| 280 | open: !isOpen, | |
| 281 | }); | |
| 282 | }, [activeThread?.terminalOpen, activeThreadId, dispatch]); | |
| 258 | 283 | |
| 259 | 284 | const handleRuntimeModeChange = async (mode: "approval-required" | "full-access") => { |
| 260 | 285 | if (mode === state.runtimeMode) return; |
| ... | ... | @@ -364,6 +389,37 @@ export default function ChatView() { |
| 364 | 389 | return () => window.removeEventListener("keydown", handler); |
| 365 | 390 | }, [api, activeProject, activeThread, lastEditor]); |
| 366 | 391 | |
| 392 | useEffect(() => { | |
| 393 | if (!activeThreadId) return; | |
| 394 | const previous = terminalOpenByThreadRef.current[activeThreadId] ?? false; | |
| 395 | const current = Boolean(activeThread?.terminalOpen); | |
| 396 | ||
| 397 | if (!previous && current) { | |
| 398 | setTerminalFocusRequestId((value) => value + 1); | |
| 399 | } else if (previous && !current) { | |
| 400 | terminalOpenByThreadRef.current[activeThreadId] = current; | |
| 401 | const frame = window.requestAnimationFrame(() => { | |
| 402 | focusComposer(); | |
| 403 | }); | |
| 404 | return () => { | |
| 405 | window.cancelAnimationFrame(frame); | |
| 406 | }; | |
| 407 | } | |
| 408 | ||
| 409 | terminalOpenByThreadRef.current[activeThreadId] = current; | |
| 410 | }, [activeThread?.terminalOpen, activeThreadId, focusComposer]); | |
| 411 | ||
| 412 | useEffect(() => { | |
| 413 | const handler = (event: globalThis.KeyboardEvent) => { | |
| 414 | if (!activeThreadId) return; | |
| 415 | if (!isTerminalToggleShortcut(event)) return; | |
| 416 | event.preventDefault(); | |
| 417 | toggleTerminalVisibility(); | |
| 418 | }; | |
| 419 | window.addEventListener("keydown", handler); | |
| 420 | return () => window.removeEventListener("keydown", handler); | |
| 421 | }, [activeThreadId, toggleTerminalVisibility]); | |
| 422 | ||
| 367 | 423 | const openInEditor = (editorId: EditorId) => { |
| 368 | 424 | if (!api || !activeProject) return; |
| 369 | 425 | const cwd = activeThread?.worktreePath ?? activeProject.cwd; |
| ... | ... | @@ -636,6 +692,17 @@ export default function ChatView() { |
| 636 | 692 | </div> |
| 637 | 693 | )} |
| 638 | 694 | {/* Diff toggle */} |
| 695 | <button | |
| 696 | type="button" | |
| 697 | className={`rounded-md px-2 py-1 text-[10px] transition-colors duration-150 ${ | |
| 698 | activeThread.terminalOpen | |
| 699 | ? "bg-accent text-foreground" | |
| 700 | : "text-muted-foreground/40 hover:text-muted-foreground/60" | |
| 701 | }`} | |
| 702 | onClick={toggleTerminalVisibility} | |
| 703 | > | |
| 704 | Terminal <span className="text-muted-foreground/50">{terminalShortcutHint}</span> | |
| 705 | </button> | |
| 639 | 706 | <button |
| 640 | 707 | type="button" |
| 641 | 708 | className={`rounded-md px-2 py-1 text-[10px] transition-colors duration-150 ${ |
| ... | ... | @@ -1141,6 +1208,32 @@ export default function ChatView() { |
| 1141 | 1208 | </div> |
| 1142 | 1209 | |
| 1143 | 1210 | <BranchToolbar envMode={envMode} onEnvModeChange={setEnvMode} envLocked={envLocked} /> |
| 1211 | <BranchToolbar envMode={envMode} onEnvModeChange={setEnvMode} envLocked={envLocked} /> | |
| 1212 | ||
| 1213 | {activeThread.terminalOpen && api && activeProject && ( | |
| 1214 | <ThreadTerminalDrawer | |
| 1215 | key={activeThread.id} | |
| 1216 | api={api} | |
| 1217 | threadId={activeThread.id} | |
| 1218 | cwd={activeProject.cwd} | |
| 1219 | height={activeThread.terminalHeight} | |
| 1220 | focusRequestId={terminalFocusRequestId} | |
| 1221 | onHeightChange={(height) => | |
| 1222 | dispatch({ | |
| 1223 | type: "SET_THREAD_TERMINAL_HEIGHT", | |
| 1224 | threadId: activeThread.id, | |
| 1225 | height, | |
| 1226 | }) | |
| 1227 | } | |
| 1228 | onThreadExited={() => | |
| 1229 | dispatch({ | |
| 1230 | type: "SET_THREAD_TERMINAL_OPEN", | |
| 1231 | threadId: activeThread.id, | |
| 1232 | open: false, | |
| 1233 | }) | |
| 1234 | } | |
| 1235 | /> | |
| 1236 | )} | |
| 1144 | 1237 | </div> |
| 1145 | 1238 | ); |
| 1146 | 1239 | } |
apps/web/src/components/Sidebar.tsx+12-1| ... | ... | @@ -5,7 +5,7 @@ import { useTheme } from "../hooks/useTheme"; |
| 5 | 5 | import { DEFAULT_MODEL } from "../model-logic"; |
| 6 | 6 | import { readNativeApi } from "../session-logic"; |
| 7 | 7 | import { useStore } from "../store"; |
| 8 | import type { Project } from "../types"; | |
| 8 | import { DEFAULT_THREAD_TERMINAL_HEIGHT, type Project } from "../types"; | |
| 9 | 9 | |
| 10 | 10 | const THEME_CYCLE = { system: "light", light: "dark", dark: "system" } as const; |
| 11 | 11 | function formatRelativeTime(iso: string): string { |
| ... | ... | @@ -52,6 +52,8 @@ export default function Sidebar() { |
| 52 | 52 | projectId, |
| 53 | 53 | title: "New thread", |
| 54 | 54 | model: state.projects.find((p) => p.id === projectId)?.model ?? DEFAULT_MODEL, |
| 55 | terminalOpen: false, | |
| 56 | terminalHeight: DEFAULT_THREAD_TERMINAL_HEIGHT, | |
| 55 | 57 | session: null, |
| 56 | 58 | messages: [], |
| 57 | 59 | events: [], |
| ... | ... | @@ -182,6 +184,15 @@ export default function Sidebar() { |
| 182 | 184 | } |
| 183 | 185 | } |
| 184 | 186 | |
| 187 | try { | |
| 188 | await api.terminal.close({ | |
| 189 | threadId, | |
| 190 | deleteHistory: true, | |
| 191 | }); | |
| 192 | } catch { | |
| 193 | // Terminal may already be closed | |
| 194 | } | |
| 195 | ||
| 185 | 196 | dispatch({ type: "DELETE_THREAD", threadId }); |
| 186 | 197 | }, |
| 187 | 198 | [api, dispatch, state.threads], |
apps/web/src/components/ThreadTerminalDrawer.tsx created+435| ... | ... | @@ -0,0 +1,435 @@ |
| 1 | import { FitAddon } from "@xterm/addon-fit"; | |
| 2 | import { type NativeApi } from "@t3tools/contracts"; | |
| 3 | import { Terminal, type ITheme } from "@xterm/xterm"; | |
| 4 | import { | |
| 5 | type PointerEvent as ReactPointerEvent, | |
| 6 | useCallback, | |
| 7 | useEffect, | |
| 8 | useRef, | |
| 9 | useState, | |
| 10 | } from "react"; | |
| 11 | import { isTerminalClearShortcut } from "../terminal-shortcuts"; | |
| 12 | import { DEFAULT_THREAD_TERMINAL_HEIGHT } from "../types"; | |
| 13 | ||
| 14 | const MIN_DRAWER_HEIGHT = 180; | |
| 15 | const MAX_DRAWER_HEIGHT_RATIO = 0.75; | |
| 16 | ||
| 17 | function maxDrawerHeight(): number { | |
| 18 | if (typeof window === "undefined") return DEFAULT_THREAD_TERMINAL_HEIGHT; | |
| 19 | return Math.max( | |
| 20 | MIN_DRAWER_HEIGHT, | |
| 21 | Math.floor(window.innerHeight * MAX_DRAWER_HEIGHT_RATIO), | |
| 22 | ); | |
| 23 | } | |
| 24 | ||
| 25 | function clampDrawerHeight(height: number): number { | |
| 26 | const safeHeight = Number.isFinite(height) | |
| 27 | ? height | |
| 28 | : DEFAULT_THREAD_TERMINAL_HEIGHT; | |
| 29 | const maxHeight = maxDrawerHeight(); | |
| 30 | return Math.min(Math.max(Math.round(safeHeight), MIN_DRAWER_HEIGHT), maxHeight); | |
| 31 | } | |
| 32 | ||
| 33 | function writeSystemMessage(terminal: Terminal, message: string): void { | |
| 34 | terminal.write(`\r\n[terminal] ${message}\r\n`); | |
| 35 | } | |
| 36 | ||
| 37 | function terminalThemeFromApp(): ITheme { | |
| 38 | const isDark = document.documentElement.classList.contains("dark"); | |
| 39 | const bodyStyles = getComputedStyle(document.body); | |
| 40 | const background = | |
| 41 | bodyStyles.backgroundColor || (isDark ? "rgb(14, 18, 24)" : "rgb(255, 255, 255)"); | |
| 42 | const foreground = | |
| 43 | bodyStyles.color || (isDark ? "rgb(237, 241, 247)" : "rgb(28, 33, 41)"); | |
| 44 | ||
| 45 | if (isDark) { | |
| 46 | return { | |
| 47 | background, | |
| 48 | foreground, | |
| 49 | cursor: "rgb(180, 203, 255)", | |
| 50 | selectionBackground: "rgba(180, 203, 255, 0.25)", | |
| 51 | scrollbarSliderBackground: "rgba(255, 255, 255, 0.1)", | |
| 52 | scrollbarSliderHoverBackground: "rgba(255, 255, 255, 0.18)", | |
| 53 | scrollbarSliderActiveBackground: "rgba(255, 255, 255, 0.22)", | |
| 54 | black: "rgb(24, 30, 38)", | |
| 55 | red: "rgb(255, 122, 142)", | |
| 56 | green: "rgb(134, 231, 149)", | |
| 57 | yellow: "rgb(244, 205, 114)", | |
| 58 | blue: "rgb(137, 190, 255)", | |
| 59 | magenta: "rgb(208, 176, 255)", | |
| 60 | cyan: "rgb(124, 232, 237)", | |
| 61 | white: "rgb(210, 218, 230)", | |
| 62 | brightBlack: "rgb(110, 120, 136)", | |
| 63 | brightRed: "rgb(255, 168, 180)", | |
| 64 | brightGreen: "rgb(176, 245, 186)", | |
| 65 | brightYellow: "rgb(255, 224, 149)", | |
| 66 | brightBlue: "rgb(174, 210, 255)", | |
| 67 | brightMagenta: "rgb(229, 203, 255)", | |
| 68 | brightCyan: "rgb(167, 244, 247)", | |
| 69 | brightWhite: "rgb(244, 247, 252)", | |
| 70 | }; | |
| 71 | } | |
| 72 | ||
| 73 | return { | |
| 74 | background, | |
| 75 | foreground, | |
| 76 | cursor: "rgb(38, 56, 78)", | |
| 77 | selectionBackground: "rgba(37, 63, 99, 0.2)", | |
| 78 | scrollbarSliderBackground: "rgba(0, 0, 0, 0.15)", | |
| 79 | scrollbarSliderHoverBackground: "rgba(0, 0, 0, 0.25)", | |
| 80 | scrollbarSliderActiveBackground: "rgba(0, 0, 0, 0.3)", | |
| 81 | black: "rgb(44, 53, 66)", | |
| 82 | red: "rgb(191, 70, 87)", | |
| 83 | green: "rgb(60, 126, 86)", | |
| 84 | yellow: "rgb(146, 112, 35)", | |
| 85 | blue: "rgb(72, 102, 163)", | |
| 86 | magenta: "rgb(132, 86, 149)", | |
| 87 | cyan: "rgb(53, 127, 141)", | |
| 88 | white: "rgb(210, 215, 223)", | |
| 89 | brightBlack: "rgb(112, 123, 140)", | |
| 90 | brightRed: "rgb(212, 95, 112)", | |
| 91 | brightGreen: "rgb(85, 148, 111)", | |
| 92 | brightYellow: "rgb(173, 133, 45)", | |
| 93 | brightBlue: "rgb(91, 124, 194)", | |
| 94 | brightMagenta: "rgb(153, 107, 172)", | |
| 95 | brightCyan: "rgb(70, 149, 164)", | |
| 96 | brightWhite: "rgb(236, 240, 246)", | |
| 97 | }; | |
| 98 | } | |
| 99 | ||
| 100 | interface ThreadTerminalDrawerProps { | |
| 101 | api: NativeApi; | |
| 102 | threadId: string; | |
| 103 | cwd: string; | |
| 104 | height: number; | |
| 105 | focusRequestId: number; | |
| 106 | onHeightChange: (height: number) => void; | |
| 107 | onThreadExited: () => void; | |
| 108 | } | |
| 109 | ||
| 110 | export default function ThreadTerminalDrawer({ | |
| 111 | api, | |
| 112 | threadId, | |
| 113 | cwd, | |
| 114 | height, | |
| 115 | focusRequestId, | |
| 116 | onHeightChange, | |
| 117 | onThreadExited, | |
| 118 | }: ThreadTerminalDrawerProps) { | |
| 119 | const [drawerHeight, setDrawerHeight] = useState(() => clampDrawerHeight(height)); | |
| 120 | const containerRef = useRef<HTMLDivElement>(null); | |
| 121 | const terminalRef = useRef<Terminal | null>(null); | |
| 122 | const fitAddonRef = useRef<FitAddon | null>(null); | |
| 123 | const drawerHeightRef = useRef(drawerHeight); | |
| 124 | const lastSyncedHeightRef = useRef(clampDrawerHeight(height)); | |
| 125 | const onHeightChangeRef = useRef(onHeightChange); | |
| 126 | const onThreadExitedRef = useRef(onThreadExited); | |
| 127 | const resizeStateRef = useRef<{ | |
| 128 | pointerId: number; | |
| 129 | startY: number; | |
| 130 | startHeight: number; | |
| 131 | } | null>(null); | |
| 132 | ||
| 133 | useEffect(() => { | |
| 134 | onHeightChangeRef.current = onHeightChange; | |
| 135 | }, [onHeightChange]); | |
| 136 | ||
| 137 | useEffect(() => { | |
| 138 | onThreadExitedRef.current = onThreadExited; | |
| 139 | }, [onThreadExited]); | |
| 140 | ||
| 141 | useEffect(() => { | |
| 142 | drawerHeightRef.current = drawerHeight; | |
| 143 | }, [drawerHeight]); | |
| 144 | ||
| 145 | const syncHeight = useCallback( | |
| 146 | (nextHeight: number) => { | |
| 147 | const clampedHeight = clampDrawerHeight(nextHeight); | |
| 148 | if (lastSyncedHeightRef.current === clampedHeight) return; | |
| 149 | lastSyncedHeightRef.current = clampedHeight; | |
| 150 | onHeightChangeRef.current(clampedHeight); | |
| 151 | }, | |
| 152 | [], | |
| 153 | ); | |
| 154 | ||
| 155 | useEffect(() => { | |
| 156 | const clampedHeight = clampDrawerHeight(height); | |
| 157 | setDrawerHeight(clampedHeight); | |
| 158 | lastSyncedHeightRef.current = clampedHeight; | |
| 159 | }, [height, threadId]); | |
| 160 | ||
| 161 | const handleResizePointerDown = useCallback( | |
| 162 | (event: ReactPointerEvent<HTMLDivElement>) => { | |
| 163 | if (event.button !== 0) return; | |
| 164 | event.preventDefault(); | |
| 165 | event.currentTarget.setPointerCapture(event.pointerId); | |
| 166 | resizeStateRef.current = { | |
| 167 | pointerId: event.pointerId, | |
| 168 | startY: event.clientY, | |
| 169 | startHeight: drawerHeight, | |
| 170 | }; | |
| 171 | }, | |
| 172 | [drawerHeight], | |
| 173 | ); | |
| 174 | ||
| 175 | const handleResizePointerMove = useCallback( | |
| 176 | (event: ReactPointerEvent<HTMLDivElement>) => { | |
| 177 | const resizeState = resizeStateRef.current; | |
| 178 | if (!resizeState || resizeState.pointerId !== event.pointerId) return; | |
| 179 | event.preventDefault(); | |
| 180 | const clampedHeight = clampDrawerHeight( | |
| 181 | resizeState.startHeight + (resizeState.startY - event.clientY), | |
| 182 | ); | |
| 183 | setDrawerHeight(clampedHeight); | |
| 184 | }, | |
| 185 | [], | |
| 186 | ); | |
| 187 | ||
| 188 | const handleResizePointerEnd = useCallback( | |
| 189 | (event: ReactPointerEvent<HTMLDivElement>) => { | |
| 190 | const resizeState = resizeStateRef.current; | |
| 191 | if (!resizeState || resizeState.pointerId !== event.pointerId) return; | |
| 192 | resizeStateRef.current = null; | |
| 193 | if (event.currentTarget.hasPointerCapture(event.pointerId)) { | |
| 194 | event.currentTarget.releasePointerCapture(event.pointerId); | |
| 195 | } | |
| 196 | syncHeight(drawerHeightRef.current); | |
| 197 | }, | |
| 198 | [syncHeight], | |
| 199 | ); | |
| 200 | ||
| 201 | useEffect(() => { | |
| 202 | const onWindowResize = () => { | |
| 203 | const clampedHeight = clampDrawerHeight(drawerHeightRef.current); | |
| 204 | if (clampedHeight !== drawerHeightRef.current) { | |
| 205 | setDrawerHeight(clampedHeight); | |
| 206 | } | |
| 207 | if (!resizeStateRef.current) { | |
| 208 | syncHeight(clampedHeight); | |
| 209 | } | |
| 210 | }; | |
| 211 | window.addEventListener("resize", onWindowResize); | |
| 212 | return () => { | |
| 213 | window.removeEventListener("resize", onWindowResize); | |
| 214 | }; | |
| 215 | }, [syncHeight]); | |
| 216 | ||
| 217 | useEffect(() => { | |
| 218 | return () => { | |
| 219 | syncHeight(drawerHeightRef.current); | |
| 220 | }; | |
| 221 | }, [syncHeight]); | |
| 222 | ||
| 223 | useEffect(() => { | |
| 224 | const mount = containerRef.current; | |
| 225 | if (!mount) return; | |
| 226 | ||
| 227 | let disposed = false; | |
| 228 | ||
| 229 | const fitAddon = new FitAddon(); | |
| 230 | const terminal = new Terminal({ | |
| 231 | cursorBlink: true, | |
| 232 | lineHeight: 1.2, | |
| 233 | fontSize: 12, | |
| 234 | scrollback: 5_000, | |
| 235 | fontFamily: | |
| 236 | '"SF Mono", "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace', | |
| 237 | theme: terminalThemeFromApp(), | |
| 238 | }); | |
| 239 | terminal.loadAddon(fitAddon); | |
| 240 | terminal.open(mount); | |
| 241 | fitAddon.fit(); | |
| 242 | ||
| 243 | terminalRef.current = terminal; | |
| 244 | fitAddonRef.current = fitAddon; | |
| 245 | ||
| 246 | const sendClearShortcut = async () => { | |
| 247 | const activeTerminal = terminalRef.current; | |
| 248 | if (!activeTerminal) return; | |
| 249 | try { | |
| 250 | await api.terminal.write({ threadId, data: "\u000c" }); | |
| 251 | } catch (error) { | |
| 252 | writeSystemMessage( | |
| 253 | activeTerminal, | |
| 254 | error instanceof Error ? error.message : "Failed to clear terminal", | |
| 255 | ); | |
| 256 | } | |
| 257 | }; | |
| 258 | ||
| 259 | terminal.attachCustomKeyEventHandler((event) => { | |
| 260 | if (!isTerminalClearShortcut(event)) return true; | |
| 261 | event.preventDefault(); | |
| 262 | event.stopPropagation(); | |
| 263 | void sendClearShortcut(); | |
| 264 | return false; | |
| 265 | }); | |
| 266 | ||
| 267 | const resizeAndSync = () => { | |
| 268 | const activeTerminal = terminalRef.current; | |
| 269 | const activeFitAddon = fitAddonRef.current; | |
| 270 | if (!activeTerminal || !activeFitAddon) return; | |
| 271 | activeFitAddon.fit(); | |
| 272 | void api.terminal | |
| 273 | .resize({ | |
| 274 | threadId, | |
| 275 | cols: activeTerminal.cols, | |
| 276 | rows: activeTerminal.rows, | |
| 277 | }) | |
| 278 | .catch(() => undefined); | |
| 279 | }; | |
| 280 | ||
| 281 | const inputDisposable = terminal.onData((data) => { | |
| 282 | void api.terminal | |
| 283 | .write({ threadId, data }) | |
| 284 | .catch((err) => | |
| 285 | writeSystemMessage( | |
| 286 | terminal, | |
| 287 | err instanceof Error ? err.message : "Terminal write failed", | |
| 288 | ), | |
| 289 | ); | |
| 290 | }); | |
| 291 | ||
| 292 | const themeObserver = new MutationObserver(() => { | |
| 293 | const activeTerminal = terminalRef.current; | |
| 294 | if (!activeTerminal) return; | |
| 295 | activeTerminal.options.theme = terminalThemeFromApp(); | |
| 296 | activeTerminal.refresh(0, activeTerminal.rows - 1); | |
| 297 | }); | |
| 298 | themeObserver.observe(document.documentElement, { | |
| 299 | attributes: true, | |
| 300 | attributeFilter: ["class", "style"], | |
| 301 | }); | |
| 302 | ||
| 303 | const openTerminal = async () => { | |
| 304 | try { | |
| 305 | const activeTerminal = terminalRef.current; | |
| 306 | const activeFitAddon = fitAddonRef.current; | |
| 307 | if (!activeTerminal || !activeFitAddon) return; | |
| 308 | activeFitAddon.fit(); | |
| 309 | const snapshot = await api.terminal.open({ | |
| 310 | threadId, | |
| 311 | cwd, | |
| 312 | cols: activeTerminal.cols, | |
| 313 | rows: activeTerminal.rows, | |
| 314 | }); | |
| 315 | if (disposed) return; | |
| 316 | activeTerminal.write("\u001bc"); | |
| 317 | if (snapshot.history.length > 0) { | |
| 318 | activeTerminal.write(snapshot.history); | |
| 319 | } | |
| 320 | window.requestAnimationFrame(() => { | |
| 321 | activeTerminal.focus(); | |
| 322 | }); | |
| 323 | } catch (err) { | |
| 324 | if (disposed) return; | |
| 325 | writeSystemMessage( | |
| 326 | terminal, | |
| 327 | err instanceof Error ? err.message : "Failed to open terminal", | |
| 328 | ); | |
| 329 | } | |
| 330 | }; | |
| 331 | ||
| 332 | const unsubscribe = api.terminal.onEvent((event) => { | |
| 333 | if (event.threadId !== threadId) return; | |
| 334 | const activeTerminal = terminalRef.current; | |
| 335 | if (!activeTerminal) return; | |
| 336 | ||
| 337 | if (event.type === "output") { | |
| 338 | activeTerminal.write(event.data); | |
| 339 | return; | |
| 340 | } | |
| 341 | ||
| 342 | if (event.type === "started" || event.type === "restarted") { | |
| 343 | activeTerminal.write("\u001bc"); | |
| 344 | if (event.snapshot.history.length > 0) { | |
| 345 | activeTerminal.write(event.snapshot.history); | |
| 346 | } | |
| 347 | return; | |
| 348 | } | |
| 349 | ||
| 350 | if (event.type === "cleared") { | |
| 351 | activeTerminal.clear(); | |
| 352 | activeTerminal.write("\u001bc"); | |
| 353 | return; | |
| 354 | } | |
| 355 | ||
| 356 | if (event.type === "error") { | |
| 357 | writeSystemMessage(activeTerminal, event.message); | |
| 358 | return; | |
| 359 | } | |
| 360 | ||
| 361 | if (event.type === "exited") { | |
| 362 | onThreadExitedRef.current(); | |
| 363 | } | |
| 364 | }); | |
| 365 | ||
| 366 | window.addEventListener("resize", resizeAndSync); | |
| 367 | const fitTimer = window.setTimeout(resizeAndSync, 30); | |
| 368 | void openTerminal(); | |
| 369 | ||
| 370 | return () => { | |
| 371 | disposed = true; | |
| 372 | window.clearTimeout(fitTimer); | |
| 373 | window.removeEventListener("resize", resizeAndSync); | |
| 374 | unsubscribe(); | |
| 375 | inputDisposable.dispose(); | |
| 376 | themeObserver.disconnect(); | |
| 377 | terminalRef.current = null; | |
| 378 | fitAddonRef.current = null; | |
| 379 | terminal.dispose(); | |
| 380 | }; | |
| 381 | }, [api, cwd, threadId]); | |
| 382 | ||
| 383 | useEffect(() => { | |
| 384 | const terminal = terminalRef.current; | |
| 385 | if (!terminal) return; | |
| 386 | const frame = window.requestAnimationFrame(() => { | |
| 387 | terminal.focus(); | |
| 388 | }); | |
| 389 | return () => { | |
| 390 | window.cancelAnimationFrame(frame); | |
| 391 | }; | |
| 392 | }, [focusRequestId]); | |
| 393 | ||
| 394 | useEffect(() => { | |
| 395 | const terminal = terminalRef.current; | |
| 396 | const fitAddon = fitAddonRef.current; | |
| 397 | if (!terminal || !fitAddon) return; | |
| 398 | const frame = window.requestAnimationFrame(() => { | |
| 399 | fitAddon.fit(); | |
| 400 | void api.terminal | |
| 401 | .resize({ | |
| 402 | threadId, | |
| 403 | cols: terminal.cols, | |
| 404 | rows: terminal.rows, | |
| 405 | }) | |
| 406 | .catch(() => undefined); | |
| 407 | }); | |
| 408 | return () => { | |
| 409 | window.cancelAnimationFrame(frame); | |
| 410 | }; | |
| 411 | }, [api, drawerHeight, threadId]); | |
| 412 | ||
| 413 | return ( | |
| 414 | <aside | |
| 415 | className="thread-terminal-drawer flex shrink-0 flex-col border-t border-border/80 bg-background" | |
| 416 | style={{ height: `${drawerHeight}px` }} | |
| 417 | > | |
| 418 | <div | |
| 419 | className="flex h-2 cursor-row-resize items-center justify-center" | |
| 420 | onPointerDown={handleResizePointerDown} | |
| 421 | onPointerMove={handleResizePointerMove} | |
| 422 | onPointerUp={handleResizePointerEnd} | |
| 423 | onPointerCancel={handleResizePointerEnd} | |
| 424 | > | |
| 425 | <div className="h-px w-10 rounded-full bg-border/80" /> | |
| 426 | </div> | |
| 427 | <div className="min-h-0 w-full flex-1 px-1.5 pb-1"> | |
| 428 | <div | |
| 429 | ref={containerRef} | |
| 430 | className="h-full w-full overflow-hidden rounded-[4px]" | |
| 431 | /> | |
| 432 | </div> | |
| 433 | </aside> | |
| 434 | ); | |
| 435 | } |
apps/web/src/index.css+14| ... | ... | @@ -229,6 +229,20 @@ input { |
| 229 | 229 | background: rgba(255, 255, 255, 0.18); |
| 230 | 230 | } |
| 231 | 231 | |
| 232 | /* Terminal drawer scrollbar parity with chat */ | |
| 233 | .thread-terminal-drawer .xterm .xterm-scrollable-element > .scrollbar.vertical { | |
| 234 | width: 6px !important; | |
| 235 | } | |
| 236 | ||
| 237 | .thread-terminal-drawer .xterm .xterm-scrollable-element > .scrollbar > .slider { | |
| 238 | border-radius: 3px; | |
| 239 | } | |
| 240 | ||
| 241 | .thread-terminal-drawer .xterm .xterm-scrollable-element > .scrollbar.vertical > .slider { | |
| 242 | width: 6px !important; | |
| 243 | left: 0 !important; | |
| 244 | } | |
| 245 | ||
| 232 | 246 | /* Reasoning select -- clickable label surface */ |
| 233 | 247 | label:has(> select#reasoning-effort) { |
| 234 | 248 | position: relative; |
apps/web/src/main.tsx+1| ... | ... | @@ -1,5 +1,6 @@ |
| 1 | 1 | import React from "react"; |
| 2 | 2 | import ReactDOM from "react-dom/client"; |
| 3 | import "@xterm/xterm/css/xterm.css"; | |
| 3 | 4 | import "highlight.js/styles/github-dark.css"; |
| 4 | 5 | |
| 5 | 6 | import App from "./App"; |
apps/web/src/persistenceSchema.test.ts+77-2| ... | ... | @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"; |
| 2 | 2 | |
| 3 | 3 | import { DEFAULT_MODEL } from "./model-logic"; |
| 4 | 4 | import { hydratePersistedState, toPersistedState } from "./persistenceSchema"; |
| 5 | import { DEFAULT_THREAD_TERMINAL_HEIGHT } from "./types"; | |
| 5 | 6 | import type { Thread } from "./types"; |
| 6 | 7 | |
| 7 | 8 | describe("hydratePersistedState", () => { |
| ... | ... | @@ -47,6 +48,8 @@ describe("hydratePersistedState", () => { |
| 47 | 48 | expect(hydrated?.projects[0]?.model).toBe(DEFAULT_MODEL); |
| 48 | 49 | expect(hydrated?.threads[0]?.model).toBe(DEFAULT_MODEL); |
| 49 | 50 | expect(hydrated?.threads[0]?.codexThreadId).toBeNull(); |
| 51 | expect(hydrated?.threads[0]?.terminalOpen).toBe(false); | |
| 52 | expect(hydrated?.threads[0]?.terminalHeight).toBe(DEFAULT_THREAD_TERMINAL_HEIGHT); | |
| 50 | 53 | expect(hydrated?.threads[0]?.messages[0]?.streaming).toBe(false); |
| 51 | 54 | expect(hydrated?.runtimeMode).toBe("full-access"); |
| 52 | 55 | }); |
| ... | ... | @@ -87,6 +90,8 @@ describe("hydratePersistedState", () => { |
| 87 | 90 | expect(hydrated).not.toBeNull(); |
| 88 | 91 | expect(hydrated?.threads.map((thread) => thread.id)).toEqual(["t-1"]); |
| 89 | 92 | expect(hydrated?.threads[0]?.codexThreadId).toBeNull(); |
| 93 | expect(hydrated?.threads[0]?.terminalOpen).toBe(false); | |
| 94 | expect(hydrated?.threads[0]?.terminalHeight).toBe(DEFAULT_THREAD_TERMINAL_HEIGHT); | |
| 90 | 95 | expect(hydrated?.activeThreadId).toBe("t-1"); |
| 91 | 96 | expect(hydrated?.runtimeMode).toBe("full-access"); |
| 92 | 97 | }); |
| ... | ... | @@ -111,16 +116,84 @@ describe("hydratePersistedState", () => { |
| 111 | 116 | const hydrated = hydratePersistedState(payload, false); |
| 112 | 117 | expect(hydrated?.runtimeMode).toBe("approval-required"); |
| 113 | 118 | }); |
| 119 | ||
| 120 | it("hydrates terminal fields from v6 payload", () => { | |
| 121 | const payload = JSON.stringify({ | |
| 122 | version: 6, | |
| 123 | runtimeMode: "full-access", | |
| 124 | projects: [ | |
| 125 | { | |
| 126 | id: "p-1", | |
| 127 | name: "Project", | |
| 128 | cwd: "/tmp/project", | |
| 129 | model: "gpt-5.3-codex", | |
| 130 | expanded: true, | |
| 131 | }, | |
| 132 | ], | |
| 133 | threads: [ | |
| 134 | { | |
| 135 | id: "t-1", | |
| 136 | codexThreadId: null, | |
| 137 | projectId: "p-1", | |
| 138 | title: "Thread", | |
| 139 | model: "gpt-5.3-codex", | |
| 140 | terminalOpen: true, | |
| 141 | terminalHeight: 360, | |
| 142 | messages: [], | |
| 143 | createdAt: "2026-02-08T10:00:00.000Z", | |
| 144 | }, | |
| 145 | ], | |
| 146 | activeThreadId: "t-1", | |
| 147 | }); | |
| 148 | ||
| 149 | const hydrated = hydratePersistedState(payload, false); | |
| 150 | expect(hydrated?.threads[0]?.terminalOpen).toBe(true); | |
| 151 | expect(hydrated?.threads[0]?.terminalHeight).toBe(360); | |
| 152 | }); | |
| 153 | ||
| 154 | it("defaults terminalHeight when hydrating v5 payloads", () => { | |
| 155 | const payload = JSON.stringify({ | |
| 156 | version: 5, | |
| 157 | runtimeMode: "full-access", | |
| 158 | projects: [ | |
| 159 | { | |
| 160 | id: "p-1", | |
| 161 | name: "Project", | |
| 162 | cwd: "/tmp/project", | |
| 163 | model: "gpt-5.3-codex", | |
| 164 | expanded: true, | |
| 165 | }, | |
| 166 | ], | |
| 167 | threads: [ | |
| 168 | { | |
| 169 | id: "t-1", | |
| 170 | codexThreadId: null, | |
| 171 | projectId: "p-1", | |
| 172 | title: "Thread", | |
| 173 | model: "gpt-5.3-codex", | |
| 174 | terminalOpen: true, | |
| 175 | messages: [], | |
| 176 | createdAt: "2026-02-08T10:00:00.000Z", | |
| 177 | }, | |
| 178 | ], | |
| 179 | activeThreadId: "t-1", | |
| 180 | }); | |
| 181 | ||
| 182 | const hydrated = hydratePersistedState(payload, false); | |
| 183 | expect(hydrated?.threads[0]?.terminalHeight).toBe(DEFAULT_THREAD_TERMINAL_HEIGHT); | |
| 184 | }); | |
| 114 | 185 | }); |
| 115 | 186 | |
| 116 | 187 | describe("toPersistedState", () => { |
| 117 | it("writes v4 payload and strips non-persisted thread fields", () => { | |
| 188 | it("writes v6 payload and strips non-persisted thread fields", () => { | |
| 118 | 189 | const thread: Thread = { |
| 119 | 190 | id: "t-1", |
| 120 | 191 | codexThreadId: "thr_1", |
| 121 | 192 | projectId: "p-1", |
| 122 | 193 | title: "Thread", |
| 123 | 194 | model: "gpt-5.3-codex", |
| 195 | terminalOpen: true, | |
| 196 | terminalHeight: 320, | |
| 124 | 197 | session: null, |
| 125 | 198 | messages: [ |
| 126 | 199 | { |
| ... | ... | @@ -153,7 +226,7 @@ describe("toPersistedState", () => { |
| 153 | 226 | runtimeMode: "full-access", |
| 154 | 227 | }); |
| 155 | 228 | |
| 156 | expect(persisted.version).toBe(4); | |
| 229 | expect(persisted.version).toBe(6); | |
| 157 | 230 | expect(persisted.runtimeMode).toBe("full-access"); |
| 158 | 231 | expect(persisted.threads[0]).toEqual({ |
| 159 | 232 | id: "t-1", |
| ... | ... | @@ -161,6 +234,8 @@ describe("toPersistedState", () => { |
| 161 | 234 | projectId: "p-1", |
| 162 | 235 | title: "Thread", |
| 163 | 236 | model: "gpt-5.3-codex", |
| 237 | terminalOpen: true, | |
| 238 | terminalHeight: 320, | |
| 164 | 239 | messages: thread.messages, |
| 165 | 240 | createdAt: thread.createdAt, |
| 166 | 241 | branch: null, |
apps/web/src/persistenceSchema.ts+29-3| ... | ... | @@ -1,7 +1,13 @@ |
| 1 | 1 | import { z } from "zod"; |
| 2 | 2 | |
| 3 | 3 | import { DEFAULT_MODEL, resolveModelSlug } from "./model-logic"; |
| 4 | import { DEFAULT_RUNTIME_MODE, type Project, type RuntimeMode, type Thread } from "./types"; | |
| 4 | import { | |
| 5 | DEFAULT_THREAD_TERMINAL_HEIGHT, | |
| 6 | DEFAULT_RUNTIME_MODE, | |
| 7 | type Project, | |
| 8 | type RuntimeMode, | |
| 9 | type Thread, | |
| 10 | } from "./types"; | |
| 5 | 11 | |
| 6 | 12 | const LEGACY_DEFAULT_MODEL = "gpt-5.2-codex"; |
| 7 | 13 | |
| ... | ... | @@ -27,6 +33,10 @@ const persistedThreadSchema = z.object({ |
| 27 | 33 | projectId: z.string().min(1), |
| 28 | 34 | title: z.string().min(1), |
| 29 | 35 | model: z.string().min(1), |
| 36 | terminalOpen: z.boolean().default(false), | |
| 37 | terminalHeight: z.number().int().min(120).max(4_096).default( | |
| 38 | DEFAULT_THREAD_TERMINAL_HEIGHT, | |
| 39 | ), | |
| 30 | 40 | messages: z.array(persistedMessageSchema), |
| 31 | 41 | createdAt: z.string().min(1), |
| 32 | 42 | branch: z.string().min(1).nullable().optional(), |
| ... | ... | @@ -59,7 +69,19 @@ export const persistedStateV4Schema = persistedStateBodySchema.extend({ |
| 59 | 69 | version: z.literal(4).optional(), |
| 60 | 70 | }); |
| 61 | 71 | |
| 72 | export const persistedStateV6Schema = persistedStateBodySchema.extend({ | |
| 73 | runtimeMode: runtimeModeSchema.default(DEFAULT_RUNTIME_MODE), | |
| 74 | version: z.literal(6).optional(), | |
| 75 | }); | |
| 76 | ||
| 77 | export const persistedStateV5Schema = persistedStateBodySchema.extend({ | |
| 78 | runtimeMode: runtimeModeSchema.default(DEFAULT_RUNTIME_MODE), | |
| 79 | version: z.literal(5).optional(), | |
| 80 | }); | |
| 81 | ||
| 62 | 82 | const persistedStateSchema = z.union([ |
| 83 | persistedStateV6Schema, | |
| 84 | persistedStateV5Schema, | |
| 63 | 85 | persistedStateV4Schema, |
| 64 | 86 | persistedStateV3Schema, |
| 65 | 87 | persistedStateV2Schema, |
| ... | ... | @@ -101,6 +123,8 @@ function hydrateThread( |
| 101 | 123 | projectId: thread.projectId, |
| 102 | 124 | title: thread.title, |
| 103 | 125 | model: resolveModelSlug(maybeMigrateLegacyModel(thread.model, isLegacyPayload)), |
| 126 | terminalOpen: thread.terminalOpen ?? false, | |
| 127 | terminalHeight: thread.terminalHeight ?? DEFAULT_THREAD_TERMINAL_HEIGHT, | |
| 104 | 128 | session: null, |
| 105 | 129 | messages: thread.messages.map((message) => ({ |
| 106 | 130 | ...message, |
| ... | ... | @@ -153,9 +177,9 @@ export function hydratePersistedState( |
| 153 | 177 | |
| 154 | 178 | export function toPersistedState( |
| 155 | 179 | state: PersistedStoreSnapshot, |
| 156 | ): z.infer<typeof persistedStateV4Schema> { | |
| 180 | ): z.infer<typeof persistedStateV6Schema> { | |
| 157 | 181 | return { |
| 158 | version: 4, | |
| 182 | version: 6, | |
| 159 | 183 | projects: state.projects, |
| 160 | 184 | threads: state.threads.map((thread) => ({ |
| 161 | 185 | id: thread.id, |
| ... | ... | @@ -163,6 +187,8 @@ export function toPersistedState( |
| 163 | 187 | projectId: thread.projectId, |
| 164 | 188 | title: thread.title, |
| 165 | 189 | model: thread.model, |
| 190 | terminalOpen: thread.terminalOpen, | |
| 191 | terminalHeight: thread.terminalHeight, | |
| 166 | 192 | messages: thread.messages, |
| 167 | 193 | createdAt: thread.createdAt, |
| 168 | 194 | branch: thread.branch, |
apps/web/src/store.test.ts+32| ... | ... | @@ -2,6 +2,7 @@ import type { ProviderEvent, ProviderSession } from "@t3tools/contracts"; |
| 2 | 2 | import { describe, expect, it } from "vitest"; |
| 3 | 3 | |
| 4 | 4 | import { type AppState, reducer } from "./store"; |
| 5 | import { DEFAULT_THREAD_TERMINAL_HEIGHT } from "./types"; | |
| 5 | 6 | import type { Thread } from "./types"; |
| 6 | 7 | |
| 7 | 8 | function makeSession(overrides: Partial<ProviderSession> = {}): ProviderSession { |
| ... | ... | @@ -34,6 +35,8 @@ function makeThread(overrides: Partial<Thread> = {}): Thread { |
| 34 | 35 | projectId: "project-1", |
| 35 | 36 | title: "Thread", |
| 36 | 37 | model: "gpt-5.3-codex", |
| 38 | terminalOpen: false, | |
| 39 | terminalHeight: DEFAULT_THREAD_TERMINAL_HEIGHT, | |
| 37 | 40 | session: makeSession(), |
| 38 | 41 | messages: [], |
| 39 | 42 | events: [], |
| ... | ... | @@ -79,6 +82,35 @@ describe("store reducer thread continuity", () => { |
| 79 | 82 | expect(next.threads[0]?.codexThreadId).toBe("thr_123"); |
| 80 | 83 | }); |
| 81 | 84 | |
| 85 | it("toggles terminal open state per thread", () => { | |
| 86 | const state = makeState(makeThread({ terminalOpen: false })); | |
| 87 | const next = reducer(state, { | |
| 88 | type: "TOGGLE_THREAD_TERMINAL", | |
| 89 | threadId: "thread-local-1", | |
| 90 | }); | |
| 91 | expect(next.threads[0]?.terminalOpen).toBe(true); | |
| 92 | }); | |
| 93 | ||
| 94 | it("sets terminal open state per thread", () => { | |
| 95 | const state = makeState(makeThread({ terminalOpen: true })); | |
| 96 | const next = reducer(state, { | |
| 97 | type: "SET_THREAD_TERMINAL_OPEN", | |
| 98 | threadId: "thread-local-1", | |
| 99 | open: false, | |
| 100 | }); | |
| 101 | expect(next.threads[0]?.terminalOpen).toBe(false); | |
| 102 | }); | |
| 103 | ||
| 104 | it("sets terminal height per thread", () => { | |
| 105 | const state = makeState(makeThread({ terminalHeight: 280 })); | |
| 106 | const next = reducer(state, { | |
| 107 | type: "SET_THREAD_TERMINAL_HEIGHT", | |
| 108 | threadId: "thread-local-1", | |
| 109 | height: 360, | |
| 110 | }); | |
| 111 | expect(next.threads[0]?.terminalHeight).toBe(360); | |
| 112 | }); | |
| 113 | ||
| 82 | 114 | it("backfills codexThreadId from routed provider events", () => { |
| 83 | 115 | const state = makeState(makeThread({ codexThreadId: null })); |
| 84 | 116 | const next = reducer(state, { |
apps/web/src/store.ts+46-8| ... | ... | @@ -12,7 +12,12 @@ import type { ProviderEvent, ProviderSession } from "@t3tools/contracts"; |
| 12 | 12 | import { resolveModelSlug } from "./model-logic"; |
| 13 | 13 | import { hydratePersistedState, toPersistedState } from "./persistenceSchema"; |
| 14 | 14 | import { applyEventToMessages, asObject, asString, evolveSession } from "./session-logic"; |
| 15 | import { DEFAULT_RUNTIME_MODE, type Project, type RuntimeMode, type Thread } from "./types"; | |
| 15 | import { | |
| 16 | DEFAULT_RUNTIME_MODE, | |
| 17 | type Project, | |
| 18 | type RuntimeMode, | |
| 19 | type Thread, | |
| 20 | } from "./types"; | |
| 16 | 21 | |
| 17 | 22 | // ── Actions ────────────────────────────────────────────────────────── |
| 18 | 23 | |
| ... | ... | @@ -22,6 +27,9 @@ type Action = |
| 22 | 27 | | { type: "TOGGLE_PROJECT"; projectId: string } |
| 23 | 28 | | { type: "ADD_THREAD"; thread: Thread } |
| 24 | 29 | | { type: "SET_ACTIVE_THREAD"; threadId: string } |
| 30 | | { type: "TOGGLE_THREAD_TERMINAL"; threadId: string } | |
| 31 | | { type: "SET_THREAD_TERMINAL_OPEN"; threadId: string; open: boolean } | |
| 32 | | { type: "SET_THREAD_TERMINAL_HEIGHT"; threadId: string; height: number } | |
| 25 | 33 | | { type: "TOGGLE_DIFF" } |
| 26 | 34 | | { |
| 27 | 35 | type: "APPLY_EVENT"; |
| ... | ... | @@ -52,8 +60,10 @@ export interface AppState { |
| 52 | 60 | diffOpen: boolean; |
| 53 | 61 | } |
| 54 | 62 | |
| 55 | const PERSISTED_STATE_KEY = "t3code:renderer-state:v4"; | |
| 63 | const PERSISTED_STATE_KEY = "t3code:renderer-state:v6"; | |
| 56 | 64 | const LEGACY_PERSISTED_STATE_KEYS = [ |
| 65 | "t3code:renderer-state:v5", | |
| 66 | "t3code:renderer-state:v4", | |
| 57 | 67 | "t3code:renderer-state:v3", |
| 58 | 68 | "codething:renderer-state:v4", |
| 59 | 69 | "codething:renderer-state:v3", |
| ... | ... | @@ -76,15 +86,16 @@ function readPersistedState(): AppState { |
| 76 | 86 | |
| 77 | 87 | try { |
| 78 | 88 | const rawCurrent = window.localStorage.getItem(PERSISTED_STATE_KEY); |
| 79 | const [legacyV3Key, legacyV2Key, legacyV1Key] = LEGACY_PERSISTED_STATE_KEYS; | |
| 80 | const rawLegacyV3 = window.localStorage.getItem(legacyV3Key); | |
| 81 | const rawLegacyV2 = window.localStorage.getItem(legacyV2Key); | |
| 82 | const rawLegacyV1 = window.localStorage.getItem(legacyV1Key); | |
| 83 | const raw = rawCurrent ?? rawLegacyV3 ?? rawLegacyV2 ?? rawLegacyV1; | |
| 89 | const legacyValues = LEGACY_PERSISTED_STATE_KEYS.map((key) => | |
| 90 | window.localStorage.getItem(key), | |
| 91 | ); | |
| 92 | const rawLegacy = legacyValues.find((value) => value !== null) ?? null; | |
| 93 | const raw = rawCurrent ?? rawLegacy; | |
| 84 | 94 | if (!raw) return initialState; |
| 95 | const rawCodethingV1 = window.localStorage.getItem("codething:renderer-state:v1"); | |
| 85 | 96 | const hydrated = hydratePersistedState( |
| 86 | 97 | raw, |
| 87 | !rawCurrent && !rawLegacyV3 && !rawLegacyV2 && Boolean(rawLegacyV1), | |
| 98 | !rawCurrent && raw === rawCodethingV1, | |
| 88 | 99 | ); |
| 89 | 100 | if (!hydrated) return initialState; |
| 90 | 101 | |
| ... | ... | @@ -283,6 +294,33 @@ export function reducer(state: AppState, action: Action): AppState { |
| 283 | 294 | case "SET_ACTIVE_THREAD": |
| 284 | 295 | return { ...state, activeThreadId: action.threadId }; |
| 285 | 296 | |
| 297 | case "TOGGLE_THREAD_TERMINAL": | |
| 298 | return { | |
| 299 | ...state, | |
| 300 | threads: updateThread(state.threads, action.threadId, (t) => ({ | |
| 301 | ...t, | |
| 302 | terminalOpen: !t.terminalOpen, | |
| 303 | })), | |
| 304 | }; | |
| 305 | ||
| 306 | case "SET_THREAD_TERMINAL_OPEN": | |
| 307 | return { | |
| 308 | ...state, | |
| 309 | threads: updateThread(state.threads, action.threadId, (t) => ({ | |
| 310 | ...t, | |
| 311 | terminalOpen: action.open, | |
| 312 | })), | |
| 313 | }; | |
| 314 | ||
| 315 | case "SET_THREAD_TERMINAL_HEIGHT": | |
| 316 | return { | |
| 317 | ...state, | |
| 318 | threads: updateThread(state.threads, action.threadId, (t) => ({ | |
| 319 | ...t, | |
| 320 | terminalHeight: action.height, | |
| 321 | })), | |
| 322 | }; | |
| 323 | ||
| 286 | 324 | case "TOGGLE_DIFF": |
| 287 | 325 | return { ...state, diffOpen: !state.diffOpen }; |
| 288 | 326 |
apps/web/src/terminal-shortcuts.test.ts created+88| ... | ... | @@ -0,0 +1,88 @@ |
| 1 | import { describe, expect, it } from "vitest"; | |
| 2 | ||
| 3 | import { | |
| 4 | isTerminalClearShortcut, | |
| 5 | isTerminalToggleShortcut, | |
| 6 | type ShortcutEventLike, | |
| 7 | } from "./terminal-shortcuts"; | |
| 8 | ||
| 9 | function event(overrides: Partial<ShortcutEventLike> = {}): ShortcutEventLike { | |
| 10 | return { | |
| 11 | key: "j", | |
| 12 | metaKey: false, | |
| 13 | ctrlKey: false, | |
| 14 | shiftKey: false, | |
| 15 | altKey: false, | |
| 16 | ...overrides, | |
| 17 | }; | |
| 18 | } | |
| 19 | ||
| 20 | describe("isTerminalToggleShortcut", () => { | |
| 21 | it("matches Cmd+J on macOS", () => { | |
| 22 | expect( | |
| 23 | isTerminalToggleShortcut(event({ metaKey: true }), "MacIntel"), | |
| 24 | ).toBe(true); | |
| 25 | }); | |
| 26 | ||
| 27 | it("matches Ctrl+J on non-macOS", () => { | |
| 28 | expect( | |
| 29 | isTerminalToggleShortcut(event({ ctrlKey: true }), "Win32"), | |
| 30 | ).toBe(true); | |
| 31 | }); | |
| 32 | ||
| 33 | it("rejects wrong modifiers", () => { | |
| 34 | expect( | |
| 35 | isTerminalToggleShortcut( | |
| 36 | event({ ctrlKey: true, shiftKey: true }), | |
| 37 | "Win32", | |
| 38 | ), | |
| 39 | ).toBe(false); | |
| 40 | expect( | |
| 41 | isTerminalToggleShortcut(event({ metaKey: true, altKey: true }), "MacIntel"), | |
| 42 | ).toBe(false); | |
| 43 | }); | |
| 44 | ||
| 45 | it("rejects non-j keys", () => { | |
| 46 | expect( | |
| 47 | isTerminalToggleShortcut(event({ key: "k", ctrlKey: true }), "Linux"), | |
| 48 | ).toBe(false); | |
| 49 | }); | |
| 50 | }); | |
| 51 | ||
| 52 | describe("isTerminalClearShortcut", () => { | |
| 53 | it("matches Ctrl+L on all platforms", () => { | |
| 54 | expect( | |
| 55 | isTerminalClearShortcut(event({ key: "l", ctrlKey: true }), "Linux"), | |
| 56 | ).toBe(true); | |
| 57 | expect( | |
| 58 | isTerminalClearShortcut(event({ key: "l", ctrlKey: true }), "MacIntel"), | |
| 59 | ).toBe(true); | |
| 60 | }); | |
| 61 | ||
| 62 | it("matches Cmd+K on macOS", () => { | |
| 63 | expect( | |
| 64 | isTerminalClearShortcut(event({ key: "k", metaKey: true }), "MacIntel"), | |
| 65 | ).toBe(true); | |
| 66 | }); | |
| 67 | ||
| 68 | it("rejects Cmd+K on non-macOS", () => { | |
| 69 | expect( | |
| 70 | isTerminalClearShortcut(event({ key: "k", metaKey: true }), "Win32"), | |
| 71 | ).toBe(false); | |
| 72 | }); | |
| 73 | ||
| 74 | it("rejects wrong modifiers", () => { | |
| 75 | expect( | |
| 76 | isTerminalClearShortcut( | |
| 77 | event({ key: "l", ctrlKey: true, shiftKey: true }), | |
| 78 | "Linux", | |
| 79 | ), | |
| 80 | ).toBe(false); | |
| 81 | expect( | |
| 82 | isTerminalClearShortcut( | |
| 83 | event({ key: "k", metaKey: true, altKey: true }), | |
| 84 | "MacIntel", | |
| 85 | ), | |
| 86 | ).toBe(false); | |
| 87 | }); | |
| 88 | }); |
apps/web/src/terminal-shortcuts.ts created+52| ... | ... | @@ -0,0 +1,52 @@ |
| 1 | export interface ShortcutEventLike { | |
| 2 | key: string; | |
| 3 | metaKey: boolean; | |
| 4 | ctrlKey: boolean; | |
| 5 | shiftKey: boolean; | |
| 6 | altKey: boolean; | |
| 7 | } | |
| 8 | ||
| 9 | function isMacPlatform(platform: string): boolean { | |
| 10 | return /mac|iphone|ipad|ipod/i.test(platform); | |
| 11 | } | |
| 12 | ||
| 13 | export function isTerminalToggleShortcut( | |
| 14 | event: ShortcutEventLike, | |
| 15 | platform = navigator.platform, | |
| 16 | ): boolean { | |
| 17 | const key = event.key.toLowerCase(); | |
| 18 | if (key !== "j") return false; | |
| 19 | if (event.shiftKey || event.altKey) return false; | |
| 20 | ||
| 21 | if (isMacPlatform(platform)) { | |
| 22 | return event.metaKey && !event.ctrlKey; | |
| 23 | } | |
| 24 | ||
| 25 | return event.ctrlKey && !event.metaKey; | |
| 26 | } | |
| 27 | ||
| 28 | export function isTerminalClearShortcut( | |
| 29 | event: ShortcutEventLike, | |
| 30 | platform = navigator.platform, | |
| 31 | ): boolean { | |
| 32 | const key = event.key.toLowerCase(); | |
| 33 | ||
| 34 | if ( | |
| 35 | key === "l" && | |
| 36 | event.ctrlKey && | |
| 37 | !event.metaKey && | |
| 38 | !event.altKey && | |
| 39 | !event.shiftKey | |
| 40 | ) { | |
| 41 | return true; | |
| 42 | } | |
| 43 | ||
| 44 | return ( | |
| 45 | isMacPlatform(platform) && | |
| 46 | key === "k" && | |
| 47 | event.metaKey && | |
| 48 | !event.ctrlKey && | |
| 49 | !event.altKey && | |
| 50 | !event.shiftKey | |
| 51 | ); | |
| 52 | } |
apps/web/src/types.ts+3| ... | ... | @@ -3,6 +3,7 @@ import type { ProviderEvent, ProviderSession } from "@t3tools/contracts"; |
| 3 | 3 | export type SessionPhase = "disconnected" | "connecting" | "ready" | "running"; |
| 4 | 4 | export type RuntimeMode = "approval-required" | "full-access"; |
| 5 | 5 | export const DEFAULT_RUNTIME_MODE: RuntimeMode = "full-access"; |
| 6 | export const DEFAULT_THREAD_TERMINAL_HEIGHT = 280; | |
| 6 | 7 | |
| 7 | 8 | export interface ChatMessage { |
| 8 | 9 | id: string; |
| ... | ... | @@ -26,6 +27,8 @@ export interface Thread { |
| 26 | 27 | projectId: string; |
| 27 | 28 | title: string; |
| 28 | 29 | model: string; |
| 30 | terminalOpen: boolean; | |
| 31 | terminalHeight: number; | |
| 29 | 32 | session: ProviderSession | null; |
| 30 | 33 | messages: ChatMessage[]; |
| 31 | 34 | events: ProviderEvent[]; |
apps/web/src/wsNativeApi.ts+8-7| ... | ... | @@ -62,13 +62,14 @@ export function createWsNativeApi(): NativeApi { |
| 62 | 62 | }, |
| 63 | 63 | }, |
| 64 | 64 | terminal: { |
| 65 | run: async () => ({ | |
| 66 | stdout: "", | |
| 67 | stderr: "Terminal not available in web mode", | |
| 68 | code: 1, | |
| 69 | signal: null, | |
| 70 | timedOut: false, | |
| 71 | }), | |
| 65 | open: (input) => transport.request(WS_METHODS.terminalOpen, input), | |
| 66 | write: (input) => transport.request(WS_METHODS.terminalWrite, input), | |
| 67 | resize: (input) => transport.request(WS_METHODS.terminalResize, input), | |
| 68 | clear: (input) => transport.request(WS_METHODS.terminalClear, input), | |
| 69 | restart: (input) => transport.request(WS_METHODS.terminalRestart, input), | |
| 70 | close: (input) => transport.request(WS_METHODS.terminalClose, input), | |
| 71 | onEvent: (callback) => | |
| 72 | transport.subscribe(WS_CHANNELS.terminalEvent, callback as (data: unknown) => void), | |
| 72 | 73 | }, |
| 73 | 74 | agent: { |
| 74 | 75 | spawn: async () => "", |
bun.lock+58-191| ... | ... | @@ -3,7 +3,7 @@ |
| 3 | 3 | "configVersion": 1, |
| 4 | 4 | "workspaces": { |
| 5 | 5 | "": { |
| 6 | "name": "ct-round-5", | |
| 6 | "name": "@t3tools/monorepo", | |
| 7 | 7 | "devDependencies": { |
| 8 | 8 | "oxfmt": "^0.28.0", |
| 9 | 9 | "oxlint": "^1.43.0", |
| ... | ... | @@ -31,9 +31,10 @@ |
| 31 | 31 | "name": "t3", |
| 32 | 32 | "version": "0.1.0", |
| 33 | 33 | "bin": { |
| 34 | "t3": "./dist/index.js", | |
| 34 | "t3": "./dist/index.mjs", | |
| 35 | 35 | }, |
| 36 | 36 | "dependencies": { |
| 37 | "node-pty": "^1.1.0", | |
| 37 | 38 | "open": "^10.1.0", |
| 38 | 39 | "ws": "^8.18.0", |
| 39 | 40 | }, |
| ... | ... | @@ -52,6 +53,8 @@ |
| 52 | 53 | "dependencies": { |
| 53 | 54 | "@t3tools/contracts": "workspace:*", |
| 54 | 55 | "@tanstack/react-query": "^5.90.0", |
| 56 | "@xterm/addon-fit": "^0.11.0", | |
| 57 | "@xterm/xterm": "^6.0.0", | |
| 55 | 58 | "highlight.js": "^11.11.1", |
| 56 | 59 | "lucide-react": "^0.563.0", |
| 57 | 60 | "react": "^19.0.0", |
| ... | ... | @@ -191,7 +194,7 @@ |
| 191 | 194 | |
| 192 | 195 | "@hapi/pinpoint": ["@hapi/pinpoint@2.0.1", "", {}, "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q=="], |
| 193 | 196 | |
| 194 | "@hapi/tlds": ["@hapi/tlds@1.1.4", "", {}, "sha512-Fq+20dxsxLaUn5jSSWrdtSRcIUba2JquuorF9UW1wIJS5cSUwxIsO2GIhaWynPRflvxSzFN+gxKte2HEW1OuoA=="], | |
| 197 | "@hapi/tlds": ["@hapi/tlds@1.1.5", "", {}, "sha512-Vq/1gnIIsvFUpKlDdfrPd/ssHDpAyBP/baVukh3u2KSG2xoNjsnRNjQiPmuyPPGqsn1cqVWWhtZHfOBaLizFRQ=="], | |
| 195 | 198 | |
| 196 | 199 | "@hapi/topo": ["@hapi/topo@6.0.2", "", { "dependencies": { "@hapi/hoek": "^11.0.2" } }, "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg=="], |
| 197 | 200 | |
| ... | ... | @@ -227,21 +230,43 @@ |
| 227 | 230 | |
| 228 | 231 | "@oxfmt/win32-x64": ["@oxfmt/win32-x64@0.28.0", "", { "os": "win32", "cpu": "x64" }, "sha512-4+S2j4OxOIyo8dz5osm5dZuL0yVmxXvtmNdHB5xyGwAWVvyWNvf7tCaQD7w2fdSsAXQLOvK7KFQrHFe33nJUCA=="], |
| 229 | 232 | |
| 230 | "@oxlint/darwin-arm64": ["@oxlint/darwin-arm64@1.43.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-C/GhObv/pQZg34NOzB6Mk8x0wc9AKj8fXzJF8ZRKTsBPyHusC6AZ6bba0QG0TUufw1KWuD0j++oebQfWeiFXNw=="], | |
| 233 | "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.46.0", "", { "os": "android", "cpu": "arm" }, "sha512-vLPcE+HcZ/W/0cVA1KLuAnoUSejGougDH/fDjBFf0Q+rbBIyBNLevOhgx3AnBNAt3hcIGY7U05ISbJCKZeVa3w=="], | |
| 231 | 234 | |
| 232 | "@oxlint/darwin-x64": ["@oxlint/darwin-x64@1.43.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-4NjfUtEEH8ewRQ2KlZGmm6DyrvypMdHwBnQT92vD0dLScNOQzr0V9O8Ua4IWXdeCNl/XMVhAV3h4/3YEYern5A=="], | |
| 235 | "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.46.0", "", { "os": "android", "cpu": "arm64" }, "sha512-b8IqCczUsirdtJ3R/be4cRm64I5pMPafMO/9xyTAZvc+R/FxZHMQuhw0iNT9hQwRn+Uo5rNAoA8QS7QurG2QeA=="], | |
| 233 | 236 | |
| 234 | "@oxlint/linux-arm64-gnu": ["@oxlint/linux-arm64-gnu@1.43.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-75tf1HvwdZ3ebk83yMbSB+moAEWK98mYqpXiaFAi6Zshie7r+Cx5PLXZFUEqkscenoZ+fcNXakHxfn94V6nf1g=="], | |
| 237 | "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.46.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-CfC/KGnNMhI01dkfCMjquKnW4zby3kqD5o/9XA7+pgo9I4b+Nipm+JVFyZPWMNwKqLXNmi35GTLWjs9svPxlew=="], | |
| 235 | 238 | |
| 236 | "@oxlint/linux-arm64-musl": ["@oxlint/linux-arm64-musl@1.43.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-BHV4fb36T2p/7bpA9fiJ5ayt7oJbiYX10nklW5arYp4l9/9yG/FQC5J4G1evzbJ/YbipF9UH0vYBAm5xbqGrvw=="], | |
| 239 | "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.46.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-m38mKPsV3rBdWOJ4TAGZiUjWU8RGrBxsmdSeMQ0bPr/8O6CUOm/RJkPBf0GAfPms2WRVcbkfEXvIiPshAeFkeA=="], | |
| 237 | 240 | |
| 238 | "@oxlint/linux-x64-gnu": ["@oxlint/linux-x64-gnu@1.43.0", "", { "os": "linux", "cpu": "x64" }, "sha512-1l3nvnzWWse1YHibzZ4HQXdF/ibfbKZhp9IguElni3bBqEyPEyurzZ0ikWynDxKGXqZa+UNXTFuU1NRVX1RJ3g=="], | |
| 241 | "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.46.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-YaFRKslSAfuMwn7ejS1/wo9jENqQigpGBjjThX+mrpmEROLYGky+zIC5xSVGRng28U92VEDVbSNJ/sguz3dUAA=="], | |
| 239 | 242 | |
| 240 | "@oxlint/linux-x64-musl": ["@oxlint/linux-x64-musl@1.43.0", "", { "os": "linux", "cpu": "x64" }, "sha512-+jNYgLGRFTJxJuaSOZJBwlYo5M0TWRw0+3y5MHOL4ArrIdHyCthg6r4RbVWrsR1qUfUE1VSSHQ2bfbC99RXqMg=="], | |
| 243 | "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.46.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Nlw+5mSZQtkg1Oj0N8ulxzG8ATpmSDz5V2DNaGhaYAVlcdR8NYSm/xTOnweOXc/UOOv3LwkPPYzqcfPhu2lEkA=="], | |
| 241 | 244 | |
| 242 | "@oxlint/win32-arm64": ["@oxlint/win32-arm64@1.43.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-dvs1C/HCjCyGTURMagiHprsOvVTT3omDiSzi5Qw0D4QFJ1pEaNlfBhVnOUYgUfS6O7Mcmj4+G+sidRsQcWQ/kA=="], | |
| 245 | "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.46.0", "", { "os": "linux", "cpu": "arm" }, "sha512-d3Y5y4ukMqAGnWLMKpwqj8ftNUaac7pA0NrId4AZ77JvHzezmxEcm2gswaBw2HW8y1pnq6KDB0vEPPvpTfDLrA=="], | |
| 243 | 246 | |
| 244 | "@oxlint/win32-x64": ["@oxlint/win32-x64@1.43.0", "", { "os": "win32", "cpu": "x64" }, "sha512-bSuItSU8mTSDsvmmLTepTdCL2FkJI6dwt9tot/k0EmiYF+ArRzmsl4lXVLssJNRV5lJEc5IViyTrh7oiwrjUqA=="], | |
| 247 | "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.46.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jkjx+XSOPuFR+C458prQmehO+v0VK19/3Hj2mOYDF4hHUf3CzmtA4fTmQUtkITZiGHnky7Oao6JeJX24mrX7WQ=="], | |
| 248 | ||
| 249 | "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.46.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-X/aPB1rpJUdykjWSeeGIbjk6qbD8VDulgLuTSMWgr/t6m1ljcAjqHb1g49pVG9bZl55zjECgzvlpPLWnfb4FMQ=="], | |
| 250 | ||
| 251 | "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.46.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-AymyOxGWwKY2KJa8b+h8iLrYJZbWKYCjqctSc2q6uIAkYPrCsxcWlge1JP6XZ14Sa80DVMwI/QvktbytSV+xVw=="], | |
| 252 | ||
| 253 | "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.46.0", "", { "os": "linux", "cpu": "none" }, "sha512-PkeVdPKCDA59rlMuucsel2LjlNEpslQN5AhkMMorIJZItbbqi/0JSuACCzaiIcXYv0oNfbeQ8rbOBikv+aT6cg=="], | |
| 254 | ||
| 255 | "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.46.0", "", { "os": "linux", "cpu": "none" }, "sha512-snQaRLO/X+Ry/CxX1px1g8GUbmXzymdRs+/RkP2bySHWZFhFDtbLm2hA1ujX/jKlTLMJDZn4hYzFGLDwG/Rh2w=="], | |
| 256 | ||
| 257 | "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.46.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-kZhDMwUe/sgDTluGao9c0Dqc1JzV6wPzfGo0l/FLQdh5Zmp39Yg1FbBsCgsJfVKmKl1fNqsHyFLTShWMOlOEhA=="], | |
| 258 | ||
| 259 | "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.46.0", "", { "os": "linux", "cpu": "x64" }, "sha512-n5a7VtQTxHZ13cNAKQc3ziARv5bE1Fx868v/tnhZNVUjaRNYe5uiUrRJ/LZghdAzOxVuQGarjjq/q4QM2+9OPA=="], | |
| 260 | ||
| 261 | "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.46.0", "", { "os": "linux", "cpu": "x64" }, "sha512-KpsDU/BhdVn3iKCLxMXAOZIpO8fS0jEA5iluRoK1rhHPwKtpzEm/OCwERsu/vboMSZm66qnoTUVXRPJ8M+iKVQ=="], | |
| 262 | ||
| 263 | "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.46.0", "", { "os": "none", "cpu": "arm64" }, "sha512-jtbqUyEXlsDlRmMtTZqNbw49+1V/WxqNAR5l0S3OEkdat9diI5I+eqq9IT+jb5cSDdszTGcXpn7S3+gUYSydxQ=="], | |
| 264 | ||
| 265 | "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.46.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-EE8NjpqEZPwHQVigNvdyJ11dZwWIfsfn4VeBAuiJeAdrnY4HFX27mIjJINJgP5ZdBYEFV1OWH/eb9fURCYel8w=="], | |
| 266 | ||
| 267 | "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.46.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-BHyk3H/HRdXs+uImGZ/2+qCET+B8lwGHOm7m54JiJEEUWf3zYCFX/Df1SPqtozWWmnBvioxoTG1J3mPRAr8KUA=="], | |
| 268 | ||
| 269 | "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.46.0", "", { "os": "win32", "cpu": "x64" }, "sha512-DJbQsSJUr4KSi9uU0QqOgI7PX2C+fKGZX+YDprt3vM2sC0dWZsgVTLoN2vtkNyEWJSY2mnvRFUshWXT3bmo0Ug=="], | |
| 245 | 270 | |
| 246 | 271 | "@quansync/fs": ["@quansync/fs@1.0.0", "", { "dependencies": { "quansync": "^1.0.0" } }, "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ=="], |
| 247 | 272 | |
| ... | ... | @@ -403,9 +428,9 @@ |
| 403 | 428 | |
| 404 | 429 | "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], |
| 405 | 430 | |
| 406 | "@types/node": ["@types/node@22.19.10", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-tF5VOugLS/EuDlTBijk0MqABfP8UxgYazTLo3uIn3b4yJgg26QRbVYJYsDtHrjdDUIRfP70+VfhTTc+CE1yskw=="], | |
| 431 | "@types/node": ["@types/node@22.19.11", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w=="], | |
| 407 | 432 | |
| 408 | "@types/react": ["@types/react@19.2.13", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ=="], | |
| 433 | "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], | |
| 409 | 434 | |
| 410 | 435 | "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], |
| 411 | 436 | |
| ... | ... | @@ -435,6 +460,10 @@ |
| 435 | 460 | |
| 436 | 461 | "@vitest/utils": ["@vitest/utils@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "tinyrainbow": "^3.0.3" } }, "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA=="], |
| 437 | 462 | |
| 463 | "@xterm/addon-fit": ["@xterm/addon-fit@0.11.0", "", {}, "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g=="], | |
| 464 | ||
| 465 | "@xterm/xterm": ["@xterm/xterm@6.0.0", "", {}, "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg=="], | |
| 466 | ||
| 438 | 467 | "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], |
| 439 | 468 | |
| 440 | 469 | "ansis": ["ansis@4.2.0", "", {}, "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig=="], |
| ... | ... | @@ -667,7 +696,7 @@ |
| 667 | 696 | |
| 668 | 697 | "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], |
| 669 | 698 | |
| 670 | "jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], | |
| 699 | "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], | |
| 671 | 700 | |
| 672 | 701 | "joi": ["joi@18.0.2", "", { "dependencies": { "@hapi/address": "^5.1.1", "@hapi/formula": "^3.0.2", "@hapi/hoek": "^11.0.7", "@hapi/pinpoint": "^2.0.1", "@hapi/tlds": "^1.1.1", "@hapi/topo": "^6.0.2", "@standard-schema/spec": "^1.0.0" } }, "sha512-RuCOQMIt78LWnktPoeBL0GErkNaJPTBGcYuyaBvUOQSpcpcLfWrHPPihYdOGbV5pam9VTWbeoF7TsGiHugcjGA=="], |
| 673 | 702 | |
| ... | ... | @@ -831,6 +860,10 @@ |
| 831 | 860 | |
| 832 | 861 | "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], |
| 833 | 862 | |
| 863 | "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], | |
| 864 | ||
| 865 | "node-pty": ["node-pty@1.1.0", "", { "dependencies": { "node-addon-api": "^7.1.0" } }, "sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg=="], | |
| 866 | ||
| 834 | 867 | "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], |
| 835 | 868 | |
| 836 | 869 | "normalize-path": ["normalize-path@2.1.1", "", { "dependencies": { "remove-trailing-separator": "^1.0.1" } }, "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w=="], |
| ... | ... | @@ -847,7 +880,7 @@ |
| 847 | 880 | |
| 848 | 881 | "oxfmt": ["oxfmt@0.28.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/darwin-arm64": "0.28.0", "@oxfmt/darwin-x64": "0.28.0", "@oxfmt/linux-arm64-gnu": "0.28.0", "@oxfmt/linux-arm64-musl": "0.28.0", "@oxfmt/linux-x64-gnu": "0.28.0", "@oxfmt/linux-x64-musl": "0.28.0", "@oxfmt/win32-arm64": "0.28.0", "@oxfmt/win32-x64": "0.28.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-3+hhBqPE6Kp22KfJmnstrZbl+KdOVSEu1V0ABaFIg1rYLtrMgrupx9znnHgHLqKxAVHebjTdiCJDk30CXOt6cw=="], |
| 849 | 882 | |
| 850 | "oxlint": ["oxlint@1.43.0", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "1.43.0", "@oxlint/darwin-x64": "1.43.0", "@oxlint/linux-arm64-gnu": "1.43.0", "@oxlint/linux-arm64-musl": "1.43.0", "@oxlint/linux-x64-gnu": "1.43.0", "@oxlint/linux-x64-musl": "1.43.0", "@oxlint/win32-arm64": "1.43.0", "@oxlint/win32-x64": "1.43.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.11.2" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-xiqTCsKZch+R61DPCjyqUVP2MhkQlRRYxLRBeBDi+dtQJ90MOgdcjIktvDCgXz0bgtx94EQzHEndsizZjMX2OA=="], | |
| 883 | "oxlint": ["oxlint@1.46.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.46.0", "@oxlint/binding-android-arm64": "1.46.0", "@oxlint/binding-darwin-arm64": "1.46.0", "@oxlint/binding-darwin-x64": "1.46.0", "@oxlint/binding-freebsd-x64": "1.46.0", "@oxlint/binding-linux-arm-gnueabihf": "1.46.0", "@oxlint/binding-linux-arm-musleabihf": "1.46.0", "@oxlint/binding-linux-arm64-gnu": "1.46.0", "@oxlint/binding-linux-arm64-musl": "1.46.0", "@oxlint/binding-linux-ppc64-gnu": "1.46.0", "@oxlint/binding-linux-riscv64-gnu": "1.46.0", "@oxlint/binding-linux-riscv64-musl": "1.46.0", "@oxlint/binding-linux-s390x-gnu": "1.46.0", "@oxlint/binding-linux-x64-gnu": "1.46.0", "@oxlint/binding-linux-x64-musl": "1.46.0", "@oxlint/binding-openharmony-arm64": "1.46.0", "@oxlint/binding-win32-arm64-msvc": "1.46.0", "@oxlint/binding-win32-ia32-msvc": "1.46.0", "@oxlint/binding-win32-x64-msvc": "1.46.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.11.2" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-I9h42QDtAVsRwoueJ4PL/7qN5jFzIUXvbO4Z5ddtII92ZCiD7uiS/JW2V4viBSfGLsbZkQp3YEs6Ls4I8q+8tA=="], | |
| 851 | 884 | |
| 852 | 885 | "p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="], |
| 853 | 886 | |
| ... | ... | @@ -977,21 +1010,19 @@ |
| 977 | 1010 | |
| 978 | 1011 | "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], |
| 979 | 1012 | |
| 980 | "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], | |
| 981 | ||
| 982 | "turbo": ["turbo@2.8.3", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.3", "turbo-darwin-arm64": "2.8.3", "turbo-linux-64": "2.8.3", "turbo-linux-arm64": "2.8.3", "turbo-windows-64": "2.8.3", "turbo-windows-arm64": "2.8.3" }, "bin": { "turbo": "bin/turbo" } }, "sha512-8Osxz5Tu/Dw2kb31EAY+nhq/YZ3wzmQSmYa1nIArqxgCAldxv9TPlrAiaBUDVnKA4aiPn0OFBD1ACcpc5VFOAQ=="], | |
| 1013 | "turbo": ["turbo@2.8.7", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.7", "turbo-darwin-arm64": "2.8.7", "turbo-linux-64": "2.8.7", "turbo-linux-arm64": "2.8.7", "turbo-windows-64": "2.8.7", "turbo-windows-arm64": "2.8.7" }, "bin": { "turbo": "bin/turbo" } }, "sha512-RBLh5caMAu1kFdTK1jgH2gH/z+jFsvX5rGbhgJ9nlIAWXSvxlzwId05uDlBA1+pBd3wO/UaKYzaQZQBXDd7kcA=="], | |
| 983 | 1014 | |
| 984 | "turbo-darwin-64": ["turbo-darwin-64@2.8.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-4kXRLfcygLOeNcP6JquqRLmGB/ATjjfehiojL2dJkL7GFm3SPSXbq7oNj8UbD8XriYQ5hPaSuz59iF1ijPHkTw=="], | |
| 1015 | "turbo-darwin-64": ["turbo-darwin-64@2.8.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-Xr4TO/oDDwoozbDtBvunb66g//WK8uHRygl72vUthuwzmiw48pil4IuoG/QbMHd9RE8aBnVmzC0WZEWk/WWt3A=="], | |
| 985 | 1016 | |
| 986 | "turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-xF7uCeC0UY0Hrv/tqax0BMbFlVP1J/aRyeGQPZT4NjvIPj8gSPDgFhfkfz06DhUwDg5NgMo04uiSkAWE8WB/QQ=="], | |
| 1017 | "turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-p8Xbmb9kZEY/NoshQUcFmQdO80s2PCGoLYj5DbpxjZr3diknipXxzOK7pcmT7l2gNHaMCpFVWLkiFY9nO3EU5w=="], | |
| 987 | 1018 | |
| 988 | "turbo-linux-64": ["turbo-linux-64@2.8.3", "", { "os": "linux", "cpu": "x64" }, "sha512-vxMDXwaOjweW/4etY7BxrXCSkvtwh0PbwVafyfT1Ww659SedUxd5rM3V2ZCmbwG8NiCfY7d6VtxyHx3Wh1GoZA=="], | |
| 1019 | "turbo-linux-64": ["turbo-linux-64@2.8.7", "", { "os": "linux", "cpu": "x64" }, "sha512-nwfEPAH3m5y/nJeYly3j1YJNYU2EG5+2ysZUxvBNM+VBV2LjQaLxB9CsEIpIOKuWKCjnFHKIADTSDPZ3D12J5Q=="], | |
| 989 | 1020 | |
| 990 | "turbo-linux-arm64": ["turbo-linux-arm64@2.8.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-mQX7uYBZFkuPLLlKaNe9IjR1JIef4YvY8f21xFocvttXvdPebnq3PK1Zjzl9A1zun2BEuWNUwQIL8lgvN9Pm3Q=="], | |
| 1021 | "turbo-linux-arm64": ["turbo-linux-arm64@2.8.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-mgA/M6xiJzyxtXV70TtWGDPh+I6acOKmeQGtOzbFQZYEf794pu5jax26bCk5skAp1gqZu3vacPr6jhYHoHU9IQ=="], | |
| 991 | 1022 | |
| 992 | "turbo-windows-64": ["turbo-windows-64@2.8.3", "", { "os": "win32", "cpu": "x64" }, "sha512-YLGEfppGxZj3VWcNOVa08h6ISsVKiG85aCAWosOKNUjb6yErWEuydv6/qImRJUI+tDLvDvW7BxopAkujRnWCrw=="], | |
| 1023 | "turbo-windows-64": ["turbo-windows-64@2.8.7", "", { "os": "win32", "cpu": "x64" }, "sha512-sHTYMaXuCcyHnGUQgfUUt7S8407TWoP14zc/4N2tsM0wZNK6V9h4H2t5jQPtqKEb6Fg8313kygdDgEwuM4vsHg=="], | |
| 993 | 1024 | |
| 994 | "turbo-windows-arm64": ["turbo-windows-arm64@2.8.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-afTUGKBRmOJU1smQSBnFGcbq0iabAPwh1uXu2BVk7BREg30/1gMnJh9DFEQTah+UD3n3ru8V55J83RQNFfqoyw=="], | |
| 1025 | "turbo-windows-arm64": ["turbo-windows-arm64@2.8.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-WyGiOI2Zp3AhuzVagzQN+T+iq0fWx0oGxDfAWT3ZiLEd4U0cDUkwUZDKVGb3rKqPjDL6lWnuxKKu73ge5xtovQ=="], | |
| 995 | 1026 | |
| 996 | 1027 | "type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="], |
| 997 | 1028 | |
| ... | ... | @@ -1077,8 +1108,6 @@ |
| 1077 | 1108 | |
| 1078 | 1109 | "@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], |
| 1079 | 1110 | |
| 1080 | "@tailwindcss/node/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], | |
| 1081 | ||
| 1082 | 1111 | "@tailwindcss/node/lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="], |
| 1083 | 1112 | |
| 1084 | 1113 | "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], |
| ... | ... | @@ -1093,13 +1122,13 @@ |
| 1093 | 1122 | |
| 1094 | 1123 | "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], |
| 1095 | 1124 | |
| 1096 | "@tailwindcss/vite/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], | |
| 1125 | "@tailwindcss/vite/vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], | |
| 1097 | 1126 | |
| 1098 | 1127 | "@types/babel__core/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], |
| 1099 | 1128 | |
| 1100 | 1129 | "@types/babel__template/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], |
| 1101 | 1130 | |
| 1102 | "@vitejs/plugin-react/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], | |
| 1131 | "@vitejs/plugin-react/vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], | |
| 1103 | 1132 | |
| 1104 | 1133 | "clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="], |
| 1105 | 1134 | |
| ... | ... | @@ -1113,7 +1142,7 @@ |
| 1113 | 1142 | |
| 1114 | 1143 | "rolldown-plugin-dts/@babel/types": ["@babel/types@8.0.0-rc.1", "", { "dependencies": { "@babel/helper-string-parser": "^8.0.0-rc.1", "@babel/helper-validator-identifier": "^8.0.0-rc.1" } }, "sha512-ubmJ6TShyaD69VE9DQrlXcdkvJbmwWPB8qYj0H2kaJi29O7vJT9ajSdBd2W8CG34pwL9pYA74fi7RHC1qbLoVQ=="], |
| 1115 | 1144 | |
| 1116 | "vitest/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], | |
| 1145 | "vitest/vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], | |
| 1117 | 1146 | |
| 1118 | 1147 | "@babel/generator/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@8.0.0-rc.1", "", {}, "sha512-vi/pfmbrOtQmqgfboaBhaCU50G7mcySVu69VU8z+lYoPPB6WzI9VgV7WQfL908M4oeSH5fDkmoupIqoE0SdApw=="], |
| 1119 | 1148 | |
| ... | ... | @@ -1141,168 +1170,6 @@ |
| 1141 | 1170 | |
| 1142 | 1171 | "@tailwindcss/node/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="], |
| 1143 | 1172 | |
| 1144 | "@tailwindcss/vite/vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], | |
| 1145 | ||
| 1146 | "@vitejs/plugin-react/vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], | |
| 1147 | ||
| 1148 | 1173 | "rolldown-plugin-dts/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@8.0.0-rc.1", "", {}, "sha512-vi/pfmbrOtQmqgfboaBhaCU50G7mcySVu69VU8z+lYoPPB6WzI9VgV7WQfL908M4oeSH5fDkmoupIqoE0SdApw=="], |
| 1149 | ||
| 1150 | "vitest/vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], | |
| 1151 | ||
| 1152 | "@tailwindcss/vite/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], | |
| 1153 | ||
| 1154 | "@tailwindcss/vite/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], | |
| 1155 | ||
| 1156 | "@tailwindcss/vite/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], | |
| 1157 | ||
| 1158 | "@tailwindcss/vite/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], | |
| 1159 | ||
| 1160 | "@tailwindcss/vite/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], | |
| 1161 | ||
| 1162 | "@tailwindcss/vite/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], | |
| 1163 | ||
| 1164 | "@tailwindcss/vite/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], | |
| 1165 | ||
| 1166 | "@tailwindcss/vite/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], | |
| 1167 | ||
| 1168 | "@tailwindcss/vite/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], | |
| 1169 | ||
| 1170 | "@tailwindcss/vite/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], | |
| 1171 | ||
| 1172 | "@tailwindcss/vite/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], | |
| 1173 | ||
| 1174 | "@tailwindcss/vite/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], | |
| 1175 | ||
| 1176 | "@tailwindcss/vite/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], | |
| 1177 | ||
| 1178 | "@tailwindcss/vite/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], | |
| 1179 | ||
| 1180 | "@tailwindcss/vite/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], | |
| 1181 | ||
| 1182 | "@tailwindcss/vite/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], | |
| 1183 | ||
| 1184 | "@tailwindcss/vite/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], | |
| 1185 | ||
| 1186 | "@tailwindcss/vite/vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], | |
| 1187 | ||
| 1188 | "@tailwindcss/vite/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], | |
| 1189 | ||
| 1190 | "@tailwindcss/vite/vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], | |
| 1191 | ||
| 1192 | "@tailwindcss/vite/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], | |
| 1193 | ||
| 1194 | "@tailwindcss/vite/vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], | |
| 1195 | ||
| 1196 | "@tailwindcss/vite/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], | |
| 1197 | ||
| 1198 | "@tailwindcss/vite/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], | |
| 1199 | ||
| 1200 | "@tailwindcss/vite/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], | |
| 1201 | ||
| 1202 | "@tailwindcss/vite/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], | |
| 1203 | ||
| 1204 | "@vitejs/plugin-react/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], | |
| 1205 | ||
| 1206 | "@vitejs/plugin-react/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], | |
| 1207 | ||
| 1208 | "@vitejs/plugin-react/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], | |
| 1209 | ||
| 1210 | "@vitejs/plugin-react/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], | |
| 1211 | ||
| 1212 | "@vitejs/plugin-react/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], | |
| 1213 | ||
| 1214 | "@vitejs/plugin-react/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], | |
| 1215 | ||
| 1216 | "@vitejs/plugin-react/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], | |
| 1217 | ||
| 1218 | "@vitejs/plugin-react/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], | |
| 1219 | ||
| 1220 | "@vitejs/plugin-react/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], | |
| 1221 | ||
| 1222 | "@vitejs/plugin-react/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], | |
| 1223 | ||
| 1224 | "@vitejs/plugin-react/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], | |
| 1225 | ||
| 1226 | "@vitejs/plugin-react/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], | |
| 1227 | ||
| 1228 | "@vitejs/plugin-react/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], | |
| 1229 | ||
| 1230 | "@vitejs/plugin-react/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], | |
| 1231 | ||
| 1232 | "@vitejs/plugin-react/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], | |
| 1233 | ||
| 1234 | "@vitejs/plugin-react/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], | |
| 1235 | ||
| 1236 | "@vitejs/plugin-react/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], | |
| 1237 | ||
| 1238 | "@vitejs/plugin-react/vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], | |
| 1239 | ||
| 1240 | "@vitejs/plugin-react/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], | |
| 1241 | ||
| 1242 | "@vitejs/plugin-react/vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], | |
| 1243 | ||
| 1244 | "@vitejs/plugin-react/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], | |
| 1245 | ||
| 1246 | "@vitejs/plugin-react/vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], | |
| 1247 | ||
| 1248 | "@vitejs/plugin-react/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], | |
| 1249 | ||
| 1250 | "@vitejs/plugin-react/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], | |
| 1251 | ||
| 1252 | "@vitejs/plugin-react/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], | |
| 1253 | ||
| 1254 | "@vitejs/plugin-react/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], | |
| 1255 | ||
| 1256 | "vitest/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], | |
| 1257 | ||
| 1258 | "vitest/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], | |
| 1259 | ||
| 1260 | "vitest/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], | |
| 1261 | ||
| 1262 | "vitest/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], | |
| 1263 | ||
| 1264 | "vitest/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], | |
| 1265 | ||
| 1266 | "vitest/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], | |
| 1267 | ||
| 1268 | "vitest/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], | |
| 1269 | ||
| 1270 | "vitest/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], | |
| 1271 | ||
| 1272 | "vitest/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], | |
| 1273 | ||
| 1274 | "vitest/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], | |
| 1275 | ||
| 1276 | "vitest/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], | |
| 1277 | ||
| 1278 | "vitest/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], | |
| 1279 | ||
| 1280 | "vitest/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], | |
| 1281 | ||
| 1282 | "vitest/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], | |
| 1283 | ||
| 1284 | "vitest/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], | |
| 1285 | ||
| 1286 | "vitest/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], | |
| 1287 | ||
| 1288 | "vitest/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], | |
| 1289 | ||
| 1290 | "vitest/vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], | |
| 1291 | ||
| 1292 | "vitest/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], | |
| 1293 | ||
| 1294 | "vitest/vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], | |
| 1295 | ||
| 1296 | "vitest/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], | |
| 1297 | ||
| 1298 | "vitest/vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], | |
| 1299 | ||
| 1300 | "vitest/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], | |
| 1301 | ||
| 1302 | "vitest/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], | |
| 1303 | ||
| 1304 | "vitest/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], | |
| 1305 | ||
| 1306 | "vitest/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], | |
| 1307 | 1174 | } |
| 1308 | 1175 | } |
packages/contracts/src/ipc.ts+16-2| ... | ... | @@ -25,7 +25,15 @@ import type { |
| 25 | 25 | ProjectListResult, |
| 26 | 26 | ProjectRemoveInput, |
| 27 | 27 | } from "./project"; |
| 28 | import type { TerminalCommandInput, TerminalCommandResult } from "./terminal"; | |
| 28 | import type { | |
| 29 | TerminalCloseInput, | |
| 30 | TerminalEvent, | |
| 31 | TerminalOpenInput, | |
| 32 | TerminalResizeInput, | |
| 33 | TerminalSessionSnapshot, | |
| 34 | TerminalThreadInput, | |
| 35 | TerminalWriteInput, | |
| 36 | } from "./terminal"; | |
| 29 | 37 | import type { NewTodoInput, Todo } from "./todo"; |
| 30 | 38 | |
| 31 | 39 | export const EDITORS = [ |
| ... | ... | @@ -46,7 +54,13 @@ export interface NativeApi { |
| 46 | 54 | pickFolder: () => Promise<string | null>; |
| 47 | 55 | }; |
| 48 | 56 | terminal: { |
| 49 | run: (input: TerminalCommandInput) => Promise<TerminalCommandResult>; | |
| 57 | open: (input: TerminalOpenInput) => Promise<TerminalSessionSnapshot>; | |
| 58 | write: (input: TerminalWriteInput) => Promise<void>; | |
| 59 | resize: (input: TerminalResizeInput) => Promise<void>; | |
| 60 | clear: (input: TerminalThreadInput) => Promise<void>; | |
| 61 | restart: (input: TerminalOpenInput) => Promise<TerminalSessionSnapshot>; | |
| 62 | close: (input: TerminalCloseInput) => Promise<void>; | |
| 63 | onEvent: (callback: (event: TerminalEvent) => void) => () => void; | |
| 50 | 64 | }; |
| 51 | 65 | agent: { |
| 52 | 66 | spawn: (config: AgentConfig) => Promise<string>; |
packages/contracts/src/terminal.test.ts+105-11| ... | ... | @@ -1,24 +1,118 @@ |
| 1 | 1 | import { describe, expect, it } from "vitest"; |
| 2 | 2 | |
| 3 | import { terminalCommandInputSchema } from "./terminal"; | |
| 3 | import { | |
| 4 | terminalCloseInputSchema, | |
| 5 | terminalEventSchema, | |
| 6 | terminalOpenInputSchema, | |
| 7 | terminalResizeInputSchema, | |
| 8 | terminalSessionSnapshotSchema, | |
| 9 | terminalThreadInputSchema, | |
| 10 | terminalWriteInputSchema, | |
| 11 | } from "./terminal"; | |
| 4 | 12 | |
| 5 | describe("terminalCommandInputSchema", () => { | |
| 6 | it("accepts a valid command", () => { | |
| 7 | const result = terminalCommandInputSchema.safeParse({ | |
| 8 | command: "echo hello", | |
| 13 | describe("terminalOpenInputSchema", () => { | |
| 14 | it("accepts valid open input", () => { | |
| 15 | const result = terminalOpenInputSchema.safeParse({ | |
| 16 | threadId: "thread-1", | |
| 17 | cwd: "/tmp/project", | |
| 18 | cols: 120, | |
| 19 | rows: 40, | |
| 9 | 20 | }); |
| 10 | 21 | expect(result.success).toBe(true); |
| 11 | 22 | }); |
| 12 | 23 | |
| 13 | it("trims whitespace", () => { | |
| 14 | const result = terminalCommandInputSchema.parse({ | |
| 15 | command: " echo hello ", | |
| 24 | it("rejects invalid bounds", () => { | |
| 25 | const result = terminalOpenInputSchema.safeParse({ | |
| 26 | threadId: "thread-1", | |
| 27 | cwd: "/tmp/project", | |
| 28 | cols: 10, | |
| 29 | rows: 2, | |
| 16 | 30 | }); |
| 17 | expect(result.command).toBe("echo hello"); | |
| 31 | expect(result.success).toBe(false); | |
| 18 | 32 | }); |
| 33 | }); | |
| 19 | 34 | |
| 20 | it("rejects an empty command", () => { | |
| 21 | const result = terminalCommandInputSchema.safeParse({ command: " " }); | |
| 35 | describe("terminalWriteInputSchema", () => { | |
| 36 | it("accepts non-empty data", () => { | |
| 37 | const result = terminalWriteInputSchema.safeParse({ | |
| 38 | threadId: "thread-1", | |
| 39 | data: "echo hello\n", | |
| 40 | }); | |
| 41 | expect(result.success).toBe(true); | |
| 42 | }); | |
| 43 | ||
| 44 | it("rejects empty data", () => { | |
| 45 | const result = terminalWriteInputSchema.safeParse({ | |
| 46 | threadId: "thread-1", | |
| 47 | data: "", | |
| 48 | }); | |
| 22 | 49 | expect(result.success).toBe(false); |
| 23 | 50 | }); |
| 24 | 51 | }); |
| 52 | ||
| 53 | describe("terminalThreadInputSchema", () => { | |
| 54 | it("trims thread ids", () => { | |
| 55 | const parsed = terminalThreadInputSchema.parse({ threadId: " thread-1 " }); | |
| 56 | expect(parsed.threadId).toBe("thread-1"); | |
| 57 | }); | |
| 58 | }); | |
| 59 | ||
| 60 | describe("terminalResizeInputSchema", () => { | |
| 61 | it("accepts valid size", () => { | |
| 62 | const result = terminalResizeInputSchema.safeParse({ | |
| 63 | threadId: "thread-1", | |
| 64 | cols: 80, | |
| 65 | rows: 24, | |
| 66 | }); | |
| 67 | expect(result.success).toBe(true); | |
| 68 | }); | |
| 69 | }); | |
| 70 | ||
| 71 | describe("terminalCloseInputSchema", () => { | |
| 72 | it("accepts optional deleteHistory", () => { | |
| 73 | const result = terminalCloseInputSchema.safeParse({ | |
| 74 | threadId: "thread-1", | |
| 75 | deleteHistory: true, | |
| 76 | }); | |
| 77 | expect(result.success).toBe(true); | |
| 78 | }); | |
| 79 | }); | |
| 80 | ||
| 81 | describe("terminalSessionSnapshotSchema", () => { | |
| 82 | it("accepts running snapshots", () => { | |
| 83 | const result = terminalSessionSnapshotSchema.safeParse({ | |
| 84 | threadId: "thread-1", | |
| 85 | cwd: "/tmp/project", | |
| 86 | status: "running", | |
| 87 | pid: 1234, | |
| 88 | history: "hello\n", | |
| 89 | exitCode: null, | |
| 90 | exitSignal: null, | |
| 91 | updatedAt: new Date().toISOString(), | |
| 92 | }); | |
| 93 | expect(result.success).toBe(true); | |
| 94 | }); | |
| 95 | }); | |
| 96 | ||
| 97 | describe("terminalEventSchema", () => { | |
| 98 | it("accepts output events", () => { | |
| 99 | const result = terminalEventSchema.safeParse({ | |
| 100 | type: "output", | |
| 101 | threadId: "thread-1", | |
| 102 | createdAt: new Date().toISOString(), | |
| 103 | data: "line\n", | |
| 104 | }); | |
| 105 | expect(result.success).toBe(true); | |
| 106 | }); | |
| 107 | ||
| 108 | it("accepts exited events", () => { | |
| 109 | const result = terminalEventSchema.safeParse({ | |
| 110 | type: "exited", | |
| 111 | threadId: "thread-1", | |
| 112 | createdAt: new Date().toISOString(), | |
| 113 | exitCode: 0, | |
| 114 | exitSignal: null, | |
| 115 | }); | |
| 116 | expect(result.success).toBe(true); | |
| 117 | }); | |
| 118 | }); |
packages/contracts/src/terminal.ts+92-12| ... | ... | @@ -1,18 +1,98 @@ |
| 1 | 1 | import { z } from "zod"; |
| 2 | 2 | |
| 3 | export const terminalCommandInputSchema = z.object({ | |
| 4 | command: z.string().trim().min(1).max(4000), | |
| 5 | cwd: z.string().min(1).optional(), | |
| 6 | timeoutMs: z.number().int().positive().max(120_000).optional(), | |
| 3 | const terminalColsSchema = z.number().int().min(20).max(400); | |
| 4 | const terminalRowsSchema = z.number().int().min(5).max(200); | |
| 5 | ||
| 6 | export const terminalThreadInputSchema = z.object({ | |
| 7 | threadId: z.string().trim().min(1), | |
| 8 | }); | |
| 9 | ||
| 10 | export const terminalOpenInputSchema = terminalThreadInputSchema.extend({ | |
| 11 | cwd: z.string().trim().min(1), | |
| 12 | cols: terminalColsSchema, | |
| 13 | rows: terminalRowsSchema, | |
| 14 | }); | |
| 15 | ||
| 16 | export const terminalWriteInputSchema = terminalThreadInputSchema.extend({ | |
| 17 | data: z.string().min(1).max(65_536), | |
| 18 | }); | |
| 19 | ||
| 20 | export const terminalResizeInputSchema = terminalThreadInputSchema.extend({ | |
| 21 | cols: terminalColsSchema, | |
| 22 | rows: terminalRowsSchema, | |
| 7 | 23 | }); |
| 8 | 24 | |
| 9 | export const terminalCommandResultSchema = z.object({ | |
| 10 | stdout: z.string(), | |
| 11 | stderr: z.string(), | |
| 12 | code: z.number().nullable(), | |
| 13 | signal: z.string().nullable(), | |
| 14 | timedOut: z.boolean(), | |
| 25 | export const terminalCloseInputSchema = terminalThreadInputSchema.extend({ | |
| 26 | deleteHistory: z.boolean().optional(), | |
| 15 | 27 | }); |
| 16 | 28 | |
| 17 | export type TerminalCommandInput = z.infer<typeof terminalCommandInputSchema>; | |
| 18 | export type TerminalCommandResult = z.infer<typeof terminalCommandResultSchema>; | |
| 29 | export const terminalSessionStatusSchema = z.enum([ | |
| 30 | "starting", | |
| 31 | "running", | |
| 32 | "exited", | |
| 33 | "error", | |
| 34 | ]); | |
| 35 | ||
| 36 | export const terminalSessionSnapshotSchema = z.object({ | |
| 37 | threadId: z.string().min(1), | |
| 38 | cwd: z.string().min(1), | |
| 39 | status: terminalSessionStatusSchema, | |
| 40 | pid: z.number().int().positive().nullable(), | |
| 41 | history: z.string(), | |
| 42 | exitCode: z.number().int().nullable(), | |
| 43 | exitSignal: z.number().int().nullable(), | |
| 44 | updatedAt: z.string().datetime(), | |
| 45 | }); | |
| 46 | ||
| 47 | const terminalEventBaseSchema = z.object({ | |
| 48 | threadId: z.string().min(1), | |
| 49 | createdAt: z.string().datetime(), | |
| 50 | }); | |
| 51 | ||
| 52 | export const terminalStartedEventSchema = terminalEventBaseSchema.extend({ | |
| 53 | type: z.literal("started"), | |
| 54 | snapshot: terminalSessionSnapshotSchema, | |
| 55 | }); | |
| 56 | ||
| 57 | export const terminalOutputEventSchema = terminalEventBaseSchema.extend({ | |
| 58 | type: z.literal("output"), | |
| 59 | data: z.string(), | |
| 60 | }); | |
| 61 | ||
| 62 | export const terminalExitedEventSchema = terminalEventBaseSchema.extend({ | |
| 63 | type: z.literal("exited"), | |
| 64 | exitCode: z.number().int().nullable(), | |
| 65 | exitSignal: z.number().int().nullable(), | |
| 66 | }); | |
| 67 | ||
| 68 | export const terminalErrorEventSchema = terminalEventBaseSchema.extend({ | |
| 69 | type: z.literal("error"), | |
| 70 | message: z.string().min(1), | |
| 71 | }); | |
| 72 | ||
| 73 | export const terminalClearedEventSchema = terminalEventBaseSchema.extend({ | |
| 74 | type: z.literal("cleared"), | |
| 75 | }); | |
| 76 | ||
| 77 | export const terminalRestartedEventSchema = terminalEventBaseSchema.extend({ | |
| 78 | type: z.literal("restarted"), | |
| 79 | snapshot: terminalSessionSnapshotSchema, | |
| 80 | }); | |
| 81 | ||
| 82 | export const terminalEventSchema = z.discriminatedUnion("type", [ | |
| 83 | terminalStartedEventSchema, | |
| 84 | terminalOutputEventSchema, | |
| 85 | terminalExitedEventSchema, | |
| 86 | terminalErrorEventSchema, | |
| 87 | terminalClearedEventSchema, | |
| 88 | terminalRestartedEventSchema, | |
| 89 | ]); | |
| 90 | ||
| 91 | export type TerminalThreadInput = z.input<typeof terminalThreadInputSchema>; | |
| 92 | export type TerminalOpenInput = z.input<typeof terminalOpenInputSchema>; | |
| 93 | export type TerminalWriteInput = z.input<typeof terminalWriteInputSchema>; | |
| 94 | export type TerminalResizeInput = z.input<typeof terminalResizeInputSchema>; | |
| 95 | export type TerminalCloseInput = z.input<typeof terminalCloseInputSchema>; | |
| 96 | export type TerminalSessionStatus = z.infer<typeof terminalSessionStatusSchema>; | |
| 97 | export type TerminalSessionSnapshot = z.infer<typeof terminalSessionSnapshotSchema>; | |
| 98 | export type TerminalEvent = z.infer<typeof terminalEventSchema>; |
packages/contracts/src/ws.ts+9| ... | ... | @@ -27,6 +27,14 @@ export const WS_METHODS = { |
| 27 | 27 | gitCheckout: "git.checkout", |
| 28 | 28 | gitInit: "git.init", |
| 29 | 29 | |
| 30 | // Terminal methods | |
| 31 | terminalOpen: "terminal.open", | |
| 32 | terminalWrite: "terminal.write", | |
| 33 | terminalResize: "terminal.resize", | |
| 34 | terminalClear: "terminal.clear", | |
| 35 | terminalRestart: "terminal.restart", | |
| 36 | terminalClose: "terminal.close", | |
| 37 | ||
| 30 | 38 | // Server meta |
| 31 | 39 | serverGetConfig: "server.getConfig", |
| 32 | 40 | } as const; |
| ... | ... | @@ -35,6 +43,7 @@ export const WS_METHODS = { |
| 35 | 43 | |
| 36 | 44 | export const WS_CHANNELS = { |
| 37 | 45 | providerEvent: "providers.event", |
| 46 | terminalEvent: "terminal.event", | |
| 38 | 47 | serverWelcome: "server.welcome", |
| 39 | 48 | } as const; |
| 40 | 49 |