authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-17 23:01:21-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-18 00:59:06-08:00
log88f31d2f65b1db7a557320d99f70b8c707cfdf4f
tree393e7485bcb37cdf692bafc373a29122dd523b5f
parentc8c0dc6c3c3aa99a36f9d29a9718306a62b4727a

Pass runtime env through terminal open and disable zsh prompt spacer

- add optional `env` to terminal open contract and validate env keys/limits - propagate runtime env from web terminal/script launches to server PTY spawn - start `zsh` with `-o nopromptsp` to avoid trailing `%` markers and add coverage

11 files changed, 200 insertions(+), 84 deletions(-)

apps/server/src/ptyAdapter.ts+2-1
......@@ -20,6 +20,7 @@ export interface PtyProcess {
2020
2121export interface PtySpawnInput {
2222 shell: string;
23 args?: string[];
2324 cwd: string;
2425 cols: number;
2526 rows: number;
......@@ -118,7 +119,7 @@ class NodePtyProcess implements PtyProcess {
118119export class NodePtyAdapter implements PtyAdapter {
119120 spawn(input: PtySpawnInput): PtyProcess {
120121 ensureNodePtySpawnHelperExecutable();
121 const ptyProcess = nodePty.spawn(input.shell, [], {
122 const ptyProcess = nodePty.spawn(input.shell, input.args ?? [], {
122123 cwd: input.cwd,
123124 cols: input.cols,
124125 rows: input.rows,
apps/server/src/terminalManager.test.ts+38
......@@ -466,4 +466,42 @@ describe("TerminalManager", () => {
466466 restoreEnv();
467467 }
468468 });
469
470 it("injects runtime env overrides into spawned terminals", async () => {
471 const { manager, ptyAdapter } = makeManager();
472 await manager.open(
473 openInput({
474 env: {
475 T3CODE_PROJECT_ROOT: "/repo",
476 T3CODE_WORKTREE_PATH: "/repo/worktree-a",
477 CUSTOM_FLAG: "1",
478 },
479 }),
480 );
481 const spawnInput = ptyAdapter.spawnInputs[0];
482 expect(spawnInput).toBeDefined();
483 if (!spawnInput) return;
484
485 expect(spawnInput.env.T3CODE_PROJECT_ROOT).toBe("/repo");
486 expect(spawnInput.env.T3CODE_WORKTREE_PATH).toBe("/repo/worktree-a");
487 expect(spawnInput.env.CUSTOM_FLAG).toBe("1");
488
489 manager.dispose();
490 });
491
492 it("starts zsh with prompt spacer disabled to avoid `%` end markers", async () => {
493 if (process.platform === "win32") return;
494 const { manager, ptyAdapter } = makeManager(5, {
495 shellResolver: () => "/bin/zsh",
496 });
497 await manager.open(openInput());
498 const spawnInput = ptyAdapter.spawnInputs[0];
499 expect(spawnInput).toBeDefined();
500 if (!spawnInput) return;
501
502 expect(spawnInput.shell).toBe("/bin/zsh");
503 expect(spawnInput.args).toEqual(["-o", "nopromptsp"]);
504
505 manager.dispose();
506 });
469507});
apps/server/src/terminalManager.ts+83-25
......@@ -59,6 +59,12 @@ interface TerminalSessionState {
5959 unsubscribeData: (() => void) | null;
6060 unsubscribeExit: (() => void) | null;
6161 hasRunningSubprocess: boolean;
62 runtimeEnv: Record<string, string> | null;
63}
64
65interface ShellCandidate {
66 shell: string;
67 args?: string[];
6268}
6369
6470function defaultShellResolver(): string {
......@@ -82,34 +88,54 @@ function normalizeShellCommand(value: string | undefined): string | null {
8288 return firstToken.replace(/^['"]|['"]$/g, "");
8389}
8490
85function uniqueShells(shells: Array<string | null>): string[] {
91function shellCandidateFromCommand(command: string | null): ShellCandidate | null {
92 if (!command || command.length === 0) return null;
93 const shellName = path.basename(command).toLowerCase();
94 if (process.platform !== "win32" && shellName === "zsh") {
95 return { shell: command, args: ["-o", "nopromptsp"] };
96 }
97 return { shell: command };
98}
99
100function formatShellCandidate(candidate: ShellCandidate): string {
101 if (!candidate.args || candidate.args.length === 0) return candidate.shell;
102 return `${candidate.shell} ${candidate.args.join(" ")}`;
103}
104
105function uniqueShellCandidates(candidates: Array<ShellCandidate | null>): ShellCandidate[] {
86106 const seen = new Set<string>();
87 const ordered: string[] = [];
88 for (const shell of shells) {
89 if (!shell || shell.length === 0) continue;
90 if (seen.has(shell)) continue;
91 seen.add(shell);
92 ordered.push(shell);
107 const ordered: ShellCandidate[] = [];
108 for (const candidate of candidates) {
109 if (!candidate) continue;
110 const key = formatShellCandidate(candidate);
111 if (seen.has(key)) continue;
112 seen.add(key);
113 ordered.push(candidate);
93114 }
94115 return ordered;
95116}
96117
97function resolveShellCandidates(shellResolver: () => string): string[] {
98 const requested = normalizeShellCommand(shellResolver());
118function resolveShellCandidates(shellResolver: () => string): ShellCandidate[] {
119 const requested = shellCandidateFromCommand(normalizeShellCommand(shellResolver()));
99120
100121 if (process.platform === "win32") {
101 return uniqueShells([requested, process.env.ComSpec ?? null, "powershell.exe", "cmd.exe"]);
122 return uniqueShellCandidates([
123 requested,
124 shellCandidateFromCommand(process.env.ComSpec ?? null),
125 shellCandidateFromCommand("powershell.exe"),
126 shellCandidateFromCommand("cmd.exe"),
127 ]);
102128 }
103129
104 return uniqueShells([
130 return uniqueShellCandidates([
105131 requested,
106 normalizeShellCommand(process.env.SHELL),
107 "/bin/zsh",
108 "/bin/bash",
109 "/bin/sh",
110 "zsh",
111 "bash",
112 "sh",
132 shellCandidateFromCommand(normalizeShellCommand(process.env.SHELL)),
133 shellCandidateFromCommand("/bin/zsh"),
134 shellCandidateFromCommand("/bin/bash"),
135 shellCandidateFromCommand("/bin/sh"),
136 shellCandidateFromCommand("zsh"),
137 shellCandidateFromCommand("bash"),
138 shellCandidateFromCommand("sh"),
113139 ]);
114140}
115141
......@@ -241,16 +267,33 @@ function shouldExcludeTerminalEnvKey(key: string): boolean {
241267 return TERMINAL_ENV_BLOCKLIST.has(normalizedKey);
242268}
243269
244function createTerminalSpawnEnv(baseEnv: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
270function createTerminalSpawnEnv(
271 baseEnv: NodeJS.ProcessEnv,
272 runtimeEnv?: Record<string, string> | null,
273): NodeJS.ProcessEnv {
245274 const spawnEnv: NodeJS.ProcessEnv = {};
246275 for (const [key, value] of Object.entries(baseEnv)) {
247276 if (value === undefined) continue;
248277 if (shouldExcludeTerminalEnvKey(key)) continue;
249278 spawnEnv[key] = value;
250279 }
280 if (runtimeEnv) {
281 for (const [key, value] of Object.entries(runtimeEnv)) {
282 spawnEnv[key] = value;
283 }
284 }
251285 return spawnEnv;
252286}
253287
288function normalizedRuntimeEnv(
289 env: Record<string, string> | undefined,
290): Record<string, string> | null {
291 if (!env) return null;
292 const entries = Object.entries(env);
293 if (entries.length === 0) return null;
294 return Object.fromEntries(entries.sort(([left], [right]) => left.localeCompare(right)));
295}
296
254297export class TerminalManager extends EventEmitter<TerminalManagerEvents> {
255298 private readonly sessions = new Map<string, TerminalSessionState>();
256299 private readonly logsDir: string;
......@@ -307,20 +350,30 @@ export class TerminalManager extends EventEmitter<TerminalManagerEvents> {
307350 unsubscribeData: null,
308351 unsubscribeExit: null,
309352 hasRunningSubprocess: false,
353 runtimeEnv: normalizedRuntimeEnv(input.env),
310354 };
311355 this.sessions.set(sessionKey, session);
312356 this.startSession(session, input, "started");
313357 return this.snapshot(session);
314358 }
315359
316 if (existing.cwd !== input.cwd) {
360 const nextRuntimeEnv = normalizedRuntimeEnv(input.env);
361 const currentRuntimeEnv = existing.runtimeEnv;
362 const runtimeEnvChanged =
363 JSON.stringify(currentRuntimeEnv) !== JSON.stringify(nextRuntimeEnv);
364
365 if (existing.cwd !== input.cwd || runtimeEnvChanged) {
317366 this.stopProcess(existing);
318367 existing.cwd = input.cwd;
368 existing.runtimeEnv = nextRuntimeEnv;
319369 existing.history = "";
320370 await this.persistHistory(existing.threadId, existing.terminalId, existing.history);
321371 } else if (existing.status === "exited" || existing.status === "error") {
372 existing.runtimeEnv = nextRuntimeEnv;
322373 existing.history = "";
323374 await this.persistHistory(existing.threadId, existing.terminalId, existing.history);
375 } else if (currentRuntimeEnv !== nextRuntimeEnv) {
376 existing.runtimeEnv = nextRuntimeEnv;
324377 }
325378
326379 if (!existing.process) {
......@@ -404,11 +457,13 @@ export class TerminalManager extends EventEmitter<TerminalManagerEvents> {
404457 unsubscribeData: null,
405458 unsubscribeExit: null,
406459 hasRunningSubprocess: false,
460 runtimeEnv: normalizedRuntimeEnv(input.env),
407461 };
408462 this.sessions.set(sessionKey, session);
409463 } else {
410464 this.stopProcess(session);
411465 session.cwd = input.cwd;
466 session.runtimeEnv = normalizedRuntimeEnv(input.env);
412467 }
413468
414469 session.history = "";
......@@ -480,19 +535,20 @@ export class TerminalManager extends EventEmitter<TerminalManagerEvents> {
480535 let startedShell: string | null = null;
481536 try {
482537 const shellCandidates = resolveShellCandidates(this.shellResolver);
483 const terminalEnv = createTerminalSpawnEnv(process.env);
538 const terminalEnv = createTerminalSpawnEnv(process.env, session.runtimeEnv);
484539 let lastSpawnError: unknown = null;
485540
486 for (const shell of shellCandidates) {
541 for (const candidate of shellCandidates) {
487542 try {
488543 ptyProcess = this.ptyAdapter.spawn({
489 shell,
544 shell: candidate.shell,
545 ...(candidate.args ? { args: candidate.args } : {}),
490546 cwd: session.cwd,
491547 cols: session.cols,
492548 rows: session.rows,
493549 env: terminalEnv,
494550 });
495 startedShell = shell;
551 startedShell = formatShellCandidate(candidate);
496552 break;
497553 } catch (error) {
498554 lastSpawnError = error;
......@@ -506,7 +562,9 @@ export class TerminalManager extends EventEmitter<TerminalManagerEvents> {
506562 const detail =
507563 lastSpawnError instanceof Error ? lastSpawnError.message : "Terminal start failed";
508564 const tried =
509 shellCandidates.length > 0 ? ` Tried shells: ${shellCandidates.join(", ")}.` : "";
565 shellCandidates.length > 0
566 ? ` Tried shells: ${shellCandidates.map((candidate) => formatShellCandidate(candidate)).join(", ")}.`
567 : "";
510568 throw new Error(`${detail}.${tried}`.trim());
511569 }
512570
apps/server/src/wsServer.test.ts+2
......@@ -323,6 +323,8 @@ describe("WebSocket Server", () => {
323323 });
324324
325325 it("responds to server.getConfig", async () => {
326 const fakeHome = makeTempDir("t3code-home-");
327 vi.spyOn(os, "homedir").mockReturnValue(fakeHome);
326328 server = createTestServer({ cwd: "/my/workspace" });
327329 await server.start();
328330 const addr = server.httpServer.address();
apps/web/src/components/ChatView.tsx+27-20
......@@ -102,7 +102,6 @@ import { Command, CommandInput, CommandItem, CommandList } from "./ui/command";
102102import ProjectScriptsControl, { type NewProjectScriptInput } from "./ProjectScriptsControl";
103103import {
104104 commandForProjectScript,
105 injectEnvIntoShellCommand,
106105 nextProjectScriptId,
107106 projectScriptRuntimeEnv,
108107 projectScriptIdFromCommand,
......@@ -545,6 +544,15 @@ export default function ChatView() {
545544 [composerHighlightedItemId, composerMenuItems],
546545 );
547546 const keybindings = keybindingsQuery.data ?? EMPTY_KEYBINDINGS;
547 const threadTerminalRuntimeEnv = useMemo(() => {
548 if (!activeProject) return {};
549 return projectScriptRuntimeEnv({
550 project: {
551 cwd: activeProject.cwd,
552 },
553 worktreePath: activeThread?.worktreePath ?? null,
554 });
555 }, [activeProject, activeThread?.worktreePath]);
548556 // Default true while loading to avoid toolbar flicker.
549557 const isGitRepo = branchesQuery.data?.isRepo ?? true;
550558 const splitTerminalShortcutLabel = useMemo(
......@@ -661,13 +669,16 @@ export default function ChatView() {
661669 env?: Record<string, string>;
662670 worktreePath?: string | null;
663671 preferNewTerminal?: boolean;
672 rememberAsLastInvoked?: boolean;
664673 },
665674 ) => {
666675 if (!api || !activeThreadId || !activeProject || !activeThread) return;
667 setLastInvokedScriptByProjectId((current) => {
668 if (current[activeProject.id] === script.id) return current;
669 return { ...current, [activeProject.id]: script.id };
670 });
676 if (options?.rememberAsLastInvoked !== false) {
677 setLastInvokedScriptByProjectId((current) => {
678 if (current[activeProject.id] === script.id) return current;
679 return { ...current, [activeProject.id]: script.id };
680 });
681 }
671682 const targetCwd = options?.cwd ?? gitCwd ?? activeProject.cwd;
672683 const baseTerminalId =
673684 activeThread.activeTerminalId ||
......@@ -701,20 +712,13 @@ export default function ChatView() {
701712 }
702713 setTerminalFocusRequestId((value) => value + 1);
703714
704 const resolvedCommand = injectEnvIntoShellCommand(
705 script.command,
706 projectScriptRuntimeEnv({
707 project: {
708 id: activeProject.id,
709 name: activeProject.name,
710 cwd: activeProject.cwd,
711 },
712 script,
713 threadId: activeThreadId,
714 worktreePath: options?.worktreePath ?? activeThread.worktreePath ?? null,
715 ...(options?.env ? { extraEnv: options.env } : {}),
716 }),
717 );
715 const runtimeEnv = projectScriptRuntimeEnv({
716 project: {
717 cwd: activeProject.cwd,
718 },
719 worktreePath: options?.worktreePath ?? activeThread.worktreePath ?? null,
720 ...(options?.env ? { extraEnv: options.env } : {}),
721 });
718722
719723 try {
720724 await api.terminal.open({
......@@ -723,11 +727,12 @@ export default function ChatView() {
723727 cwd: targetCwd,
724728 cols: SCRIPT_TERMINAL_COLS,
725729 rows: SCRIPT_TERMINAL_ROWS,
730 env: runtimeEnv,
726731 });
727732 await api.terminal.write({
728733 threadId: activeThreadId,
729734 terminalId: targetTerminalId,
730 data: `${resolvedCommand}\n`,
735 data: `${script.command}\r`,
731736 });
732737 } catch (error) {
733738 dispatch({
......@@ -1337,6 +1342,7 @@ export default function ChatView() {
13371342 void runProjectScript(setupScript, {
13381343 cwd: result.worktree.path,
13391344 worktreePath: result.worktree.path,
1345 rememberAsLastInvoked: false,
13401346 });
13411347 }
13421348 } catch (err) {
......@@ -1889,6 +1895,7 @@ export default function ChatView() {
18891895 api={api}
18901896 threadId={activeThread.id}
18911897 cwd={gitCwd ?? activeProject.cwd}
1898 runtimeEnv={threadTerminalRuntimeEnv}
18921899 height={activeThread.terminalHeight}
18931900 terminalIds={activeThread.terminalIds}
18941901 activeTerminalId={activeThread.activeTerminalId}
apps/web/src/components/ProjectScriptsControl.tsx+2-2
......@@ -6,12 +6,12 @@ import type {
66import {
77 BugIcon,
88 ChevronDownIcon,
9 CogIcon,
109 FlaskConicalIcon,
1110 HammerIcon,
1211 ListChecksIcon,
1312 PlayIcon,
1413 PlusIcon,
14 SettingsIcon,
1515 WrenchIcon,
1616} from "lucide-react";
1717import React, { type FormEvent, type KeyboardEvent, useMemo, useState } from "react";
......@@ -319,7 +319,7 @@ export default function ProjectScriptsControl({
319319 openEditDialog(script);
320320 }}
321321 >
322 <CogIcon className="size-3.5" />
322 <SettingsIcon className="size-3.5" />
323323 </Button>
324324 </span>
325325 </MenuItem>
apps/web/src/components/ThreadTerminalDrawer.tsx+8-1
......@@ -111,6 +111,7 @@ interface TerminalViewportProps {
111111 threadId: string;
112112 terminalId: string;
113113 cwd: string;
114 runtimeEnv?: Record<string, string>;
114115 focusRequestId: number;
115116 autoFocus: boolean;
116117 resizeEpoch: number;
......@@ -122,6 +123,7 @@ function TerminalViewport({
122123 threadId,
123124 terminalId,
124125 cwd,
126 runtimeEnv,
125127 focusRequestId,
126128 autoFocus,
127129 resizeEpoch,
......@@ -274,6 +276,7 @@ function TerminalViewport({
274276 cwd,
275277 cols: activeTerminal.cols,
276278 rows: activeTerminal.rows,
279 ...(runtimeEnv ? { env: runtimeEnv } : {}),
277280 });
278281 if (disposed) return;
279282 activeTerminal.write("\u001bc");
......@@ -369,7 +372,7 @@ function TerminalViewport({
369372 fitAddonRef.current = null;
370373 terminal.dispose();
371374 };
372 }, [api, cwd, terminalId, threadId]);
375 }, [api, autoFocus, cwd, runtimeEnv, terminalId, threadId]);
373376
374377 useEffect(() => {
375378 if (!autoFocus) return;
......@@ -414,6 +417,7 @@ interface ThreadTerminalDrawerProps {
414417 api: NativeApi;
415418 threadId: string;
416419 cwd: string;
420 runtimeEnv?: Record<string, string>;
417421 height: number;
418422 terminalIds: string[];
419423 activeTerminalId: string;
......@@ -463,6 +467,7 @@ export default function ThreadTerminalDrawer({
463467 api,
464468 threadId,
465469 cwd,
470 runtimeEnv,
466471 height,
467472 terminalIds,
468473 activeTerminalId,
......@@ -778,6 +783,7 @@ export default function ThreadTerminalDrawer({
778783 threadId={threadId}
779784 terminalId={terminalId}
780785 cwd={cwd}
786 {...(runtimeEnv ? { runtimeEnv } : {})}
781787 focusRequestId={focusRequestId}
782788 autoFocus={terminalId === resolvedActiveTerminalId}
783789 resizeEpoch={resizeEpoch}
......@@ -795,6 +801,7 @@ export default function ThreadTerminalDrawer({
795801 threadId={threadId}
796802 terminalId={resolvedActiveTerminalId}
797803 cwd={cwd}
804 {...(runtimeEnv ? { runtimeEnv } : {})}
798805 focusRequestId={focusRequestId}
799806 autoFocus
800807 resizeEpoch={resizeEpoch}
apps/web/src/projectScripts.test.ts+2-24
......@@ -67,40 +67,19 @@ describe("projectScripts helpers", () => {
6767
6868 it("builds default runtime env for scripts", () => {
6969 const env = projectScriptRuntimeEnv({
70 project: { id: "project-1", name: "acme-web", cwd: "/repo" },
71 script: {
72 id: "test",
73 name: "Test",
74 icon: "test",
75 runOnWorktreeCreate: false,
76 },
77 threadId: "thread-1",
70 project: { cwd: "/repo" },
7871 worktreePath: "/repo/worktree-a",
7972 });
8073
8174 expect(env).toMatchObject({
8275 T3CODE_PROJECT_ROOT: "/repo",
83 T3CODE_PROJECT_ID: "project-1",
84 T3CODE_PROJECT_NAME: "acme-web",
85 T3CODE_THREAD_ID: "thread-1",
86 T3CODE_SCRIPT_ID: "test",
87 T3CODE_SCRIPT_NAME: "Test",
88 T3CODE_SCRIPT_ICON: "test",
89 T3CODE_SCRIPT_IS_SETUP: "0",
9076 T3CODE_WORKTREE_PATH: "/repo/worktree-a",
9177 });
9278 });
9379
9480 it("allows overriding runtime env values", () => {
9581 const env = projectScriptRuntimeEnv({
96 project: { id: "project-1", name: "acme-web", cwd: "/repo" },
97 script: {
98 id: "setup",
99 name: "Setup",
100 icon: "configure",
101 runOnWorktreeCreate: true,
102 },
103 threadId: "thread-1",
82 project: { cwd: "/repo" },
10483 extraEnv: {
10584 T3CODE_PROJECT_ROOT: "/custom-root",
10685 CUSTOM_FLAG: "1",
......@@ -108,7 +87,6 @@ describe("projectScripts helpers", () => {
10887 });
10988
11089 expect(env.T3CODE_PROJECT_ROOT).toBe("/custom-root");
111 expect(env.T3CODE_SCRIPT_IS_SETUP).toBe("1");
11290 expect(env.CUSTOM_FLAG).toBe("1");
11391 expect(env.T3CODE_WORKTREE_PATH).toBeUndefined();
11492 });
apps/web/src/projectScripts.ts-11
......@@ -85,12 +85,8 @@ export function injectEnvIntoShellCommand(
8585
8686interface ProjectScriptRuntimeEnvInput {
8787 project: {
88 id: string;
89 name: string;
9088 cwd: string;
9189 };
92 script: Pick<ProjectScript, "id" | "name" | "icon" | "runOnWorktreeCreate">;
93 threadId: string;
9490 worktreePath?: string | null;
9591 extraEnv?: Record<string, string>;
9692}
......@@ -98,13 +94,6 @@ interface ProjectScriptRuntimeEnvInput {
9894export function projectScriptRuntimeEnv(input: ProjectScriptRuntimeEnvInput): Record<string, string> {
9995 const env: Record<string, string> = {
10096 T3CODE_PROJECT_ROOT: input.project.cwd,
101 T3CODE_PROJECT_ID: input.project.id,
102 T3CODE_PROJECT_NAME: input.project.name,
103 T3CODE_THREAD_ID: input.threadId,
104 T3CODE_SCRIPT_ID: input.script.id,
105 T3CODE_SCRIPT_NAME: input.script.name,
106 T3CODE_SCRIPT_ICON: input.script.icon,
107 T3CODE_SCRIPT_IS_SETUP: input.script.runOnWorktreeCreate ? "1" : "0",
10897 };
10998 if (input.worktreePath) {
11099 env.T3CODE_WORKTREE_PATH = input.worktreePath;
packages/contracts/src/terminal.test.ts+30
......@@ -42,6 +42,36 @@ describe("terminalOpenInputSchema", () => {
4242 });
4343 expect(parsed.terminalId).toBe(DEFAULT_TERMINAL_ID);
4444 });
45
46 it("accepts optional env overrides", () => {
47 const parsed = terminalOpenInputSchema.parse({
48 threadId: "thread-1",
49 cwd: "/tmp/project",
50 cols: 100,
51 rows: 24,
52 env: {
53 T3CODE_PROJECT_ROOT: "/tmp/project",
54 CUSTOM_FLAG: "1",
55 },
56 });
57 expect(parsed.env).toMatchObject({
58 T3CODE_PROJECT_ROOT: "/tmp/project",
59 CUSTOM_FLAG: "1",
60 });
61 });
62
63 it("rejects invalid env keys", () => {
64 const result = terminalOpenInputSchema.safeParse({
65 threadId: "thread-1",
66 cwd: "/tmp/project",
67 cols: 100,
68 rows: 24,
69 env: {
70 "bad-key": "1",
71 },
72 });
73 expect(result.success).toBe(false);
74 });
4575});
4676
4777describe("terminalWriteInputSchema", () => {
packages/contracts/src/terminal.ts+6
......@@ -5,6 +5,11 @@ export const DEFAULT_TERMINAL_ID = "default";
55const terminalColsSchema = z.number().int().min(20).max(400);
66const terminalRowsSchema = z.number().int().min(5).max(200);
77const terminalIdSchema = z.string().trim().min(1).max(128);
8const terminalEnvKeySchema = z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/).max(128);
9const terminalEnvValueSchema = z.string().max(8_192);
10const terminalEnvSchema = z
11 .record(terminalEnvKeySchema, terminalEnvValueSchema)
12 .refine((env) => Object.keys(env).length <= 128, "Too many terminal env vars");
813
914export const terminalThreadInputSchema = z.object({
1015 threadId: z.string().trim().min(1),
......@@ -18,6 +23,7 @@ export const terminalOpenInputSchema = terminalSessionInputSchema.extend({
1823 cwd: z.string().trim().min(1),
1924 cols: terminalColsSchema,
2025 rows: terminalRowsSchema,
26 env: terminalEnvSchema.optional(),
2127});
2228
2329export const terminalWriteInputSchema = terminalSessionInputSchema.extend({