diff --git a/packages/server/src/handlers/persistent-pty.ts b/packages/server/src/handlers/persistent-pty.ts index f5297bb47207..692832edcb9e 100644 --- a/packages/server/src/handlers/persistent-pty.ts +++ b/packages/server/src/handlers/persistent-pty.ts @@ -1,5 +1,6 @@ import { PersistentPty } from "@opencode/core/persistent-pty" import { PtyTicket } from "@opencode/core/pty/ticket" +import { ShellSelect } from "@opencode/core/shell/select" import { ForbiddenError, InvalidRequestError, @@ -62,10 +63,13 @@ export const PersistentPtyHandler = HttpApiBuilder.group(Api, "server.experiment .handle( "persistentPty.create", Effect.fn(function* (ctx) { + const shell = yield* ShellSelect.Service return { data: yield* pty .create(ctx.params.sessionID, { - command: ctx.payload.command, + // PersistentPty is process-global and cannot yield the Location-scoped shell + // service, so the configured shell is resolved here instead. + command: ctx.payload.command ?? (yield* shell.resolve({ priority: "config" })), args: ctx.payload.args, cwd: ctx.payload.cwd, title: ctx.payload.title, diff --git a/packages/server/test/persistent-pty.test.ts b/packages/server/test/persistent-pty.test.ts index 2be137bb61bf..da66c7b1afa2 100644 --- a/packages/server/test/persistent-pty.test.ts +++ b/packages/server/test/persistent-pty.test.ts @@ -497,6 +497,43 @@ smoke( 20_000, ) +smoke( + "uses the configured shell for terminals created without a command", + () => + Effect.gen(function* () { + const fixture = yield* testDirectory() + const configured = path.join(fixture.root, "configured-shell") + yield* Effect.promise(async () => { + await fs.writeFile(configured, "#!/bin/sh\nprintf configured-shell; exec cat\n") + await fs.chmod(configured, 0o755) + }) + const server = yield* ServerProcess.start({ + hostname: "127.0.0.1", + port: 0, + password: "secret", + app: { version: "test-version" }, + database: { path: fixture.database }, + fs: { filewatcher: false }, + config: { content: JSON.stringify({ shell: configured }) }, + }) + const base = HttpServer.formatAddress(server.address) + const sessionID = Session.ID.make("ses_persistent_pty_configured_shell") + expect((yield* request(base, "GET", `/api/experimental/session/${sessionID}/terminal`)).data).toEqual([]) + const terminal = Schema.decodeUnknownSync(PersistentPty.Info)( + (yield* request(base, "POST", `/api/experimental/session/${sessionID}/terminal`, { + args: [], + cwd: fixture.root, + title: "configured shell", + env: {}, + })).data, + ) + expect(terminal.command).toBe(configured) + expect(yield* waitForText(base, terminal.id, "configured-shell")).toContain("configured-shell") + yield* request(base, "DELETE", `/api/experimental/persistent-pty/${terminal.id}`) + }), + 20_000, +) + smoke( "isolates servers sharing a database and preserves terminals only through explicit restart handoff", () =>