From ae74a522bf6ee3e99610544ff38c18aa61b7de01 Mon Sep 17 00:00:00 2001 From: wSedlacek Date: Tue, 3 Nov 2026 22:06:25 -0800 Subject: [PATCH 1/4] feat(server): open projects in a system terminal - Add Terminal to Open in with platform-specific launch support - Start the terminal in the project or preview file's folder --- .../src/process/externalLauncher.test.ts | 64 ++++++++++++++ apps/server/src/process/externalLauncher.ts | 87 ++++++++++++++++++- apps/web/src/components/chat/OpenInPicker.tsx | 12 ++- apps/web/src/editorLabels.ts | 2 +- apps/web/src/editorPreferences.test.ts | 8 ++ apps/web/src/editorPreferences.ts | 4 +- apps/web/src/fileContextMenu.ts | 4 +- docs/user/terminal.md | 6 ++ packages/contracts/src/editor.ts | 1 + 9 files changed, 183 insertions(+), 5 deletions(-) diff --git a/apps/server/src/process/externalLauncher.test.ts b/apps/server/src/process/externalLauncher.test.ts index 9f975ee209e3d..e644127a5cab2 100644 --- a/apps/server/src/process/externalLauncher.test.ts +++ b/apps/server/src/process/externalLauncher.test.ts @@ -257,6 +257,70 @@ it.effect("launches an installed editor with platform-safe arguments", () => }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), ); +// Needs POSIX stubs; the Windows PowerShell stub is one file named with backslashes. +it.effect.skipIf(windowsHost)("opens the terminal in the target's folder on each platform", () => + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const binDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-editors-" }); + const workspace = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3 it's a repo-" }); + const script = path.join(workspace, "run.sh"); + yield* fileSystem.writeFileString(script, "#!/bin/sh\n"); + const stub = path.join(binDir, "konsole"); + yield* fileSystem.writeFileString(stub, "#!/bin/sh\n"); + yield* fileSystem.chmod(stub, 0o755); + + const launch = (cwd: string, platform: NodeJS.Platform, env: Record) => + Effect.gen(function* () { + const spawned: ChildProcess.StandardCommand[] = []; + const editors = yield* Effect.gen(function* () { + const launcher = yield* ExternalLauncher.ExternalLauncher; + const editors = yield* launcher.resolveAvailableEditors(); + if (editors.includes("terminal")) { + yield* launcher.launchEditor({ editor: "terminal", cwd }); + } + return editors; + }).pipe( + Effect.provide(layerTest({ platform, env, onSpawn: (command) => spawned.push(command) })), + ); + return { available: editors.includes("terminal"), spawned: spawned.at(-1) }; + }); + + for (const cwd of [workspace, script]) { + const mac = yield* launch(cwd, "darwin", { PATH: "/usr/bin" }); + assert.equal(mac.available, true); + assert.equal(mac.spawned?.command, "open"); + assert.deepEqual(mac.spawned?.args, ["-a", "Terminal", workspace]); + + const linux = yield* launch(cwd, "linux", { PATH: binDir, DISPLAY: ":0" }); + assert.equal(linux.spawned?.command, "konsole"); + assert.deepEqual(linux.spawned?.args, []); + assert.equal(linux.spawned?.options.cwd, workspace); + } + + const headless = yield* launch(workspace, "linux", { PATH: binDir }); + assert.equal(headless.available, false); + const noTerminal = yield* launch(workspace, "linux", { PATH: "/nonexistent", DISPLAY: ":0" }); + assert.equal(noTerminal.available, false); + + const systemRoot = path.join(binDir, "Windows"); + yield* fileSystem.writeFileString( + `${systemRoot}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`, + "", + ); + const windows = yield* launch("C:/it's a repo/src/index.ts", "win32", { + PATH: binDir, + PATHEXT: ".EXE", + SYSTEMROOT: systemRoot, + }); + assert.equal(windows.spawned?.options.detached, false); + assert.equal( + Buffer.from(String(windows.spawned?.args.at(-1)), "base64").toString("utf16le"), + "$ProgressPreference = 'SilentlyContinue'; Start-Process 'powershell.exe' -WorkingDirectory 'C:\\it''s a repo\\src'", + ); + }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), +); + it.effect.skipIf(windowsHost)( "reveals a WSL file in Windows File Explorer through its UNC path", () => diff --git a/apps/server/src/process/externalLauncher.ts b/apps/server/src/process/externalLauncher.ts index 7f4417d1fb67a..63830ffee4516 100644 --- a/apps/server/src/process/externalLauncher.ts +++ b/apps/server/src/process/externalLauncher.ts @@ -48,6 +48,7 @@ interface EditorLaunch { readonly target: string; readonly command: string; readonly args: ReadonlyArray; + readonly cwd?: string; readonly detached?: boolean; } @@ -94,6 +95,20 @@ const compactEnv = (input: Record>): NodeJS.Proces ), ); +// Each of these opens in its spawn cwd when run bare, so none needs a directory flag. +const LINUX_TERMINAL_COMMANDS = [ + "xdg-terminal-exec", + "x-terminal-emulator", + "gnome-terminal", + "kgx", + "konsole", + "xfce4-terminal", + "kitty", + "alacritty", + "foot", + "xterm", +] as const; + const BrowserLaunchEnvConfig = Config.all({ SYSTEMROOT: Config.String("SYSTEMROOT").pipe(Config.option), windir: Config.String("windir").pipe(Config.option), @@ -357,6 +372,26 @@ const fileManagerRevealKindForPlatform = Effect.fn( return hasGraphicalLinuxSession(env) ? "files" : undefined; }); +const resolveTerminalCommand = Effect.fn("externalLauncher.resolveTerminalCommand")(function* ( + platform: NodeJS.Platform, + env: NodeJS.ProcessEnv, +): Effect.fn.Return< + string | undefined, + never, + FileSystem.FileSystem | Path.Path | ChildProcessSpawner.ChildProcessSpawner +> { + if (platform === "darwin") return "open"; + if (platform === "win32") { + const powershell = resolvePowerShellPath(env); + return (yield* isCommandAvailable(powershell, { env })) ? powershell : undefined; + } + if (!hasGraphicalLinuxSession(env)) return undefined; + for (const command of LINUX_TERMINAL_COMMANDS) { + if (yield* isCommandAvailable(command, { env })) return command; + } + return undefined; +}); + function resolveWslFileManagerPath(target: string, distroName: string): string { const relativePath = target.replace(/^\/+/, "").replaceAll("/", "\\"); return `\\\\wsl.localhost\\${distroName}${relativePath.length > 0 ? `\\${relativePath}` : ""}`; @@ -406,7 +441,11 @@ const buildAvailableEditors = Effect.fn("externalLauncher.buildAvailableEditors" for (const editor of EDITORS) { if (editor.commands === null) { - if ((yield* resolveUsableFileManagerCommand(platform, env)) !== undefined) { + const command = + editor.id === "terminal" + ? yield* resolveTerminalCommand(platform, env) + : yield* resolveUsableFileManagerCommand(platform, env); + if (command !== undefined) { available.push(editor.id); } continue; @@ -491,6 +530,14 @@ const resolveEditorLaunch = Effect.fn("resolveEditorLaunch")(function* ( }; } + if (editorDef.id === "terminal") { + const command = yield* resolveTerminalCommand(platform, env); + if (command === undefined) { + return yield* new ExternalLauncherUnsupportedEditorError({ editor: input.editor }); + } + return yield* resolveTerminalLaunch(input.cwd, platform, command); + } + if (editorDef.id !== "file-manager") { return yield* new ExternalLauncherUnsupportedEditorError({ editor: input.editor }); } @@ -518,6 +565,43 @@ const resolveEditorLaunch = Effect.fn("resolveEditorLaunch")(function* ( }; }); +// Terminal.app runs a file it is asked to open, so a file target must become its folder. +const resolveTerminalLaunch = Effect.fn("resolveTerminalLaunch")(function* ( + target: string, + platform: NodeJS.Platform, + command: string, +): Effect.fn.Return { + const fileSystem = yield* FileSystem.FileSystem; + const isDirectory = yield* fileSystem.stat(target).pipe( + Effect.map((info) => info.type === "Directory"), + Effect.orElseSucceed(() => false), + ); + + if (platform === "win32") { + const path = yield* Path.Path.pipe(Effect.provide(NodePath.layerWin32)); + const windowsTarget = normalizeWindowsFileManagerPath(target); + const directory = isDirectory ? windowsTarget : path.dirname(windowsTarget); + return { + editor: "terminal", + target, + command, + args: [ + ...POWERSHELL_ARGUMENTS_PREFIX, + encodeUtf16LeBase64( + `$ProgressPreference = 'SilentlyContinue'; Start-Process 'powershell.exe' -WorkingDirectory ${escapePowerShellStringLiteral(directory)}`, + ), + ], + detached: WINDOWS_POWERSHELL_DETACHED, + }; + } + + const path = yield* Path.Path; + const directory = isDirectory ? target : path.dirname(target); + return platform === "darwin" + ? { editor: "terminal", target, command, args: ["-a", "Terminal", directory] } + : { editor: "terminal", target, command, args: [], cwd: directory }; +}); + // A replacement manager registers a default Directory shell verb; stock Windows // uses "none". Explorer needs its /select argument passed verbatim through // PowerShell because Node's argument quoting changes how Explorer parses it. @@ -774,6 +858,7 @@ const launchEditorProcess = Effect.fn("externalLauncher.launchEditorProcess")(fu command: spawnCommand.command, args: spawnCommand.args, options: { + ...(launch.cwd === undefined ? {} : { cwd: launch.cwd }), detached: launch.detached ?? true, shell: spawnCommand.shell, env: yield* readGraphicalSessionEnv(), diff --git a/apps/web/src/components/chat/OpenInPicker.tsx b/apps/web/src/components/chat/OpenInPicker.tsx index a19491adcd15e..ff7ce99174b86 100644 --- a/apps/web/src/components/chat/OpenInPicker.tsx +++ b/apps/web/src/components/chat/OpenInPicker.tsx @@ -19,7 +19,12 @@ import { useRemoteOpenState, } from "../../remoteOpen"; import { useEnvironment } from "../../state/environments"; -import { ChevronDownIcon, FolderClosedIcon, SquareArrowOutUpRightIcon } from "lucide-react"; +import { + ChevronDownIcon, + FolderClosedIcon, + SquareArrowOutUpRightIcon, + TerminalIcon, +} from "lucide-react"; import { Group, GroupSeparator } from "../ui/group"; import { @@ -192,6 +197,11 @@ const resolveOpenInOptions = (platform: string, availableEditors: ReadonlyArray< value: "file-manager", kind: isMacPlatform(platform) || isWindowsPlatform(platform) ? "brand" : "generic", }, + { + Icon: TerminalIcon, + value: "terminal", + kind: "generic", + }, ]; const availableEditorSet = new Set(availableEditors); return baseOptions diff --git a/apps/web/src/editorLabels.ts b/apps/web/src/editorLabels.ts index 7d44bc328b74c..951c69505cc53 100644 --- a/apps/web/src/editorLabels.ts +++ b/apps/web/src/editorLabels.ts @@ -13,7 +13,7 @@ export function editorLabelForPlatform(editorId: EditorId, platform: string): st } export function openInEditorMenuLabel(editorId: EditorId | null): string { - return editorId === null || editorId === "file-manager" + return editorId === null || editorId === "file-manager" || editorId === "terminal" ? "Open in editor" : `Open in ${editorLabels.get(editorId) ?? "Editor"}`; } diff --git a/apps/web/src/editorPreferences.test.ts b/apps/web/src/editorPreferences.test.ts index 212eda3304221..ae30dbcd783f3 100644 --- a/apps/web/src/editorPreferences.test.ts +++ b/apps/web/src/editorPreferences.test.ts @@ -67,3 +67,11 @@ it("checks revocation and regrant when a retained editor callback is invoked", a }); expect(state.setPreference).toHaveBeenCalledTimes(1); }); + +it("opens files in an editor without losing a stored terminal preference", async () => { + state.allowed.add(primary); + state.getPreference.mockReturnValue("terminal"); + const open = useOpenInPreferredEditor(primary, ["vscode", "terminal"]); + expect(await open("/work/readme.md:5")).toMatchObject({ _tag: "Success", value: "vscode" }); + expect(state.setPreference).not.toHaveBeenCalled(); +}); diff --git a/apps/web/src/editorPreferences.ts b/apps/web/src/editorPreferences.ts index e6aacd32b02de..51b77b36fab2d 100644 --- a/apps/web/src/editorPreferences.ts +++ b/apps/web/src/editorPreferences.ts @@ -55,12 +55,14 @@ export function usePreferredEditor(availableEditors: ReadonlyArray) { return [effectiveEditor, setLastEditor] as const; } +// These targets are files, which a terminal cannot show, so a stored terminal choice is skipped and kept. function resolveAndPersistPreferredEditor(availableEditors: readonly EditorId[]): EditorId | null { const availableEditorIds = new Set(availableEditors); + availableEditorIds.delete("terminal"); const stored = getLocalStorageItem(LAST_EDITOR_KEY, EditorId); if (stored && availableEditorIds.has(stored)) return stored; const editor = EDITORS.find((editor) => availableEditorIds.has(editor.id))?.id ?? null; - if (editor) setLocalStorageItem(LAST_EDITOR_KEY, editor, EditorId); + if (editor && stored !== "terminal") setLocalStorageItem(LAST_EDITOR_KEY, editor, EditorId); return editor ?? null; } diff --git a/apps/web/src/fileContextMenu.ts b/apps/web/src/fileContextMenu.ts index 1ea95c96de518..3bb89a1928b1f 100644 --- a/apps/web/src/fileContextMenu.ts +++ b/apps/web/src/fileContextMenu.ts @@ -72,7 +72,9 @@ function buildFileContextMenuItems(input: { icon: "folder-tree", }); } - const editorIds = input.capabilities.editorIds.filter((id) => id !== "file-manager"); + const editorIds = input.capabilities.editorIds.filter( + (id) => id !== "file-manager" && id !== "terminal", + ); if (editorIds.length > 0) { items.push({ id: "open-with", diff --git a/docs/user/terminal.md b/docs/user/terminal.md index dd921eb9a9650..b5aacf6812bc9 100644 --- a/docs/user/terminal.md +++ b/docs/user/terminal.md @@ -20,6 +20,12 @@ Jump to latest in the context menu returns to the current output. On iPhone and iPad, long-press the terminal to paste or to choose Select Text. Select Text shows the visible output, where you can copy text or tap a link. +To work in a system terminal instead, choose Terminal from the Open in menu. It +opens a new window in the project folder on the machine running T3 Code; from a +file preview it opens in that file's folder. On macOS this is Terminal.app, on +Windows it is PowerShell, and on Linux T3 Code uses the first terminal it finds, +starting with your desktop's default. + ## fish T3 Code starts fish with `no-query-term`, avoiding startup waits for terminal diff --git a/packages/contracts/src/editor.ts b/packages/contracts/src/editor.ts index 75e5aebbc31a4..0f52d2c667743 100644 --- a/packages/contracts/src/editor.ts +++ b/packages/contracts/src/editor.ts @@ -146,6 +146,7 @@ export const EDITORS = [ jetbrainsProductCode: "WS", }, { id: "file-manager", label: "File Manager", commands: null, launchStyle: "direct-path" }, + { id: "terminal", label: "Terminal", commands: null, launchStyle: "direct-path" }, ] as const satisfies ReadonlyArray; export const EditorId = Schema.Literals(EDITORS.map((e) => e.id)); From 5f1a79668880ad09a5c0155209cb5778fd046a39 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Wed, 4 Nov 2026 08:46:31 +0000 Subject: [PATCH 2/4] fix(server): preserve terminal launch directories --- .../src/process/externalLauncher.test.ts | 57 ++++++++++++++++--- apps/server/src/process/externalLauncher.ts | 34 +++++++++-- 2 files changed, 80 insertions(+), 11 deletions(-) diff --git a/apps/server/src/process/externalLauncher.test.ts b/apps/server/src/process/externalLauncher.test.ts index e644127a5cab2..a5b2f837d9830 100644 --- a/apps/server/src/process/externalLauncher.test.ts +++ b/apps/server/src/process/externalLauncher.test.ts @@ -266,11 +266,16 @@ it.effect.skipIf(windowsHost)("opens the terminal in the target's folder on each const workspace = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3 it's a repo-" }); const script = path.join(workspace, "run.sh"); yield* fileSystem.writeFileString(script, "#!/bin/sh\n"); - const stub = path.join(binDir, "konsole"); + const stub = path.join(binDir, "x-terminal-emulator"); yield* fileSystem.writeFileString(stub, "#!/bin/sh\n"); yield* fileSystem.chmod(stub, 0o755); - const launch = (cwd: string, platform: NodeJS.Platform, env: Record) => + const launch = ( + cwd: string, + platform: NodeJS.Platform, + env: Record, + display?: string, + ) => Effect.gen(function* () { const spawned: ChildProcess.StandardCommand[] = []; const editors = yield* Effect.gen(function* () { @@ -281,7 +286,18 @@ it.effect.skipIf(windowsHost)("opens the terminal in the target's folder on each } return editors; }).pipe( - Effect.provide(layerTest({ platform, env, onSpawn: (command) => spawned.push(command) })), + Effect.provide( + layerTest({ + platform, + env, + onSpawn: (command) => spawned.push(command), + spawnResult: (command) => + command.command === "busctl" + ? { stdout: JSON.stringify({ data: display ? [`DISPLAY=${display}`] : [] }) } + : undefined, + }), + ), + Effect.provideService(HostProcess.Environment, env), ); return { available: editors.includes("terminal"), spawned: spawned.at(-1) }; }); @@ -293,22 +309,49 @@ it.effect.skipIf(windowsHost)("opens the terminal in the target's folder on each assert.deepEqual(mac.spawned?.args, ["-a", "Terminal", workspace]); const linux = yield* launch(cwd, "linux", { PATH: binDir, DISPLAY: ":0" }); - assert.equal(linux.spawned?.command, "konsole"); - assert.deepEqual(linux.spawned?.args, []); + assert.equal(linux.spawned?.command, "x-terminal-emulator"); + assert.deepEqual(linux.spawned?.args, [ + "-e", + "/bin/sh", + "-c", + 'cd -- "$1" && exec "${SHELL:-/bin/sh}"', + "sh", + workspace, + ]); assert.equal(linux.spawned?.options.cwd, workspace); } const headless = yield* launch(workspace, "linux", { PATH: binDir }); assert.equal(headless.available, false); + const recovered = yield* launch(workspace, "linux", { PATH: binDir }, ":0"); + assert.equal(recovered.available, true); + assert.equal(recovered.spawned?.options.env?.DISPLAY, ":0"); const noTerminal = yield* launch(workspace, "linux", { PATH: "/nonexistent", DISPLAY: ":0" }); assert.equal(noTerminal.available, false); + for (const [command, args] of [ + ["xdg-terminal-exec", [`--dir=${workspace}`]], + ["konsole", ["--workdir", workspace]], + ["kitty", ["--directory", workspace]], + ...["gnome-terminal", "kgx", "xfce4-terminal", "alacritty", "foot"].map( + (command) => [command, [`--working-directory=${workspace}`]] as const, + ), + ] as const) { + const terminalBin = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-terminal-" }); + const executable = path.join(terminalBin, command); + yield* fileSystem.writeFileString(executable, "#!/bin/sh\n"); + yield* fileSystem.chmod(executable, 0o755); + const linux = yield* launch(workspace, "linux", { PATH: terminalBin, DISPLAY: ":0" }); + assert.equal(linux.spawned?.command, command); + assert.deepEqual(linux.spawned?.args, args); + } + const systemRoot = path.join(binDir, "Windows"); yield* fileSystem.writeFileString( `${systemRoot}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`, "", ); - const windows = yield* launch("C:/it's a repo/src/index.ts", "win32", { + const windows = yield* launch("C:/it's a repo’;calc;#/src/index.ts", "win32", { PATH: binDir, PATHEXT: ".EXE", SYSTEMROOT: systemRoot, @@ -316,7 +359,7 @@ it.effect.skipIf(windowsHost)("opens the terminal in the target's folder on each assert.equal(windows.spawned?.options.detached, false); assert.equal( Buffer.from(String(windows.spawned?.args.at(-1)), "base64").toString("utf16le"), - "$ProgressPreference = 'SilentlyContinue'; Start-Process 'powershell.exe' -WorkingDirectory 'C:\\it''s a repo\\src'", + `$ProgressPreference = 'SilentlyContinue'; Start-Process 'powershell.exe' -WorkingDirectory ([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('${Buffer.from("C:\\it's a repo’;calc;#\\src", "utf16le").toString("base64")}')))`, ); }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), ); diff --git a/apps/server/src/process/externalLauncher.ts b/apps/server/src/process/externalLauncher.ts index 63830ffee4516..87eed526925d6 100644 --- a/apps/server/src/process/externalLauncher.ts +++ b/apps/server/src/process/externalLauncher.ts @@ -95,7 +95,6 @@ const compactEnv = (input: Record>): NodeJS.Proces ), ); -// Each of these opens in its spawn cwd when run bare, so none needs a directory flag. const LINUX_TERMINAL_COMMANDS = [ "xdg-terminal-exec", "x-terminal-emulator", @@ -385,7 +384,8 @@ const resolveTerminalCommand = Effect.fn("externalLauncher.resolveTerminalComman const powershell = resolvePowerShellPath(env); return (yield* isCommandAvailable(powershell, { env })) ? powershell : undefined; } - if (!hasGraphicalLinuxSession(env)) return undefined; + if (!hasGraphicalLinuxSession(env) && !hasGraphicalLinuxSession(yield* readGraphicalSessionEnv())) + return undefined; for (const command of LINUX_TERMINAL_COMMANDS) { if (yield* isCommandAvailable(command, { env })) return command; } @@ -588,7 +588,7 @@ const resolveTerminalLaunch = Effect.fn("resolveTerminalLaunch")(function* ( args: [ ...POWERSHELL_ARGUMENTS_PREFIX, encodeUtf16LeBase64( - `$ProgressPreference = 'SilentlyContinue'; Start-Process 'powershell.exe' -WorkingDirectory ${escapePowerShellStringLiteral(directory)}`, + `$ProgressPreference = 'SilentlyContinue'; Start-Process 'powershell.exe' -WorkingDirectory ([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('${encodeUtf16LeBase64(directory)}')))`, ), ], detached: WINDOWS_POWERSHELL_DETACHED, @@ -599,9 +599,35 @@ const resolveTerminalLaunch = Effect.fn("resolveTerminalLaunch")(function* ( const directory = isDirectory ? target : path.dirname(target); return platform === "darwin" ? { editor: "terminal", target, command, args: ["-a", "Terminal", directory] } - : { editor: "terminal", target, command, args: [], cwd: directory }; + : { + editor: "terminal", + target, + command, + args: linuxTerminalArgs(command, directory), + cwd: directory, + }; }); +function linuxTerminalArgs(command: string, directory: string): ReadonlyArray { + switch (command) { + case "xdg-terminal-exec": + return [`--dir=${directory}`]; + case "konsole": + return ["--workdir", directory]; + case "kitty": + return ["--directory", directory]; + case "gnome-terminal": + case "kgx": + case "xfce4-terminal": + case "alacritty": + case "foot": + return [`--working-directory=${directory}`]; + default: + // The desktop's x-terminal-emulator can ignore its inherited cwd. + return ["-e", "/bin/sh", "-c", 'cd -- "$1" && exec "${SHELL:-/bin/sh}"', "sh", directory]; + } +} + // A replacement manager registers a default Directory shell verb; stock Windows // uses "none". Explorer needs its /select argument passed verbatim through // PowerShell because Node's argument quoting changes how Explorer parses it. From 0445828aae68bc43f83b766e64a1604650e88a7f Mon Sep 17 00:00:00 2001 From: wSedlacek Date: Wed, 4 Nov 2026 16:47:10 -0800 Subject: [PATCH 3/4] feat(web): give Terminal an app-style icon in Open in The thin gray prompt looked out of place next to the full-color app icons. Co-Authored-By: Claude Opus 5.5 --- apps/web/src/components/Icons.tsx | 22 +++++++++++++++++++ apps/web/src/components/chat/OpenInPicker.tsx | 12 ++++------ 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/apps/web/src/components/Icons.tsx b/apps/web/src/components/Icons.tsx index d6977fc043097..0ab7e2b3cb7e0 100644 --- a/apps/web/src/components/Icons.tsx +++ b/apps/web/src/components/Icons.tsx @@ -25,6 +25,28 @@ export const FinderIcon: Icon = (props) => ( ); +export const TerminalAppIcon: Icon = (props) => ( + + + + +); + export const FileExplorerIcon: Icon = (props) => ( Date: Tue, 10 Nov 2026 06:05:12 +0000 Subject: [PATCH 4/4] docs(user): say to click Open after choosing Terminal --- docs/user/terminal.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/user/terminal.md b/docs/user/terminal.md index b5aacf6812bc9..d185a23cb88e1 100644 --- a/docs/user/terminal.md +++ b/docs/user/terminal.md @@ -20,8 +20,8 @@ Jump to latest in the context menu returns to the current output. On iPhone and iPad, long-press the terminal to paste or to choose Select Text. Select Text shows the visible output, where you can copy text or tap a link. -To work in a system terminal instead, choose Terminal from the Open in menu. It -opens a new window in the project folder on the machine running T3 Code; from a +To work in a system terminal instead, choose Terminal from the Open in menu, +then click Open. It opens a new window in the project folder on the machine running T3 Code; from a file preview it opens in that file's folder. On macOS this is Terminal.app, on Windows it is PowerShell, and on Linux T3 Code uses the first terminal it finds, starting with your desktop's default.