mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-17 01:48:29 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a0d887b4d9 |
@@ -10,15 +10,17 @@ import { Updater } from "../../services/updater"
|
||||
import { UpdatePreflight } from "../../services/update-preflight"
|
||||
import { Npm } from "@opencode-ai/util/npm"
|
||||
import { OPENCODE_CHANNEL, OPENCODE_VERSION } from "../../version"
|
||||
import { Env } from "../../env"
|
||||
|
||||
export default Runtime.handler(Commands, (input) =>
|
||||
Effect.gen(function* () {
|
||||
const requestedDirectory = Option.getOrUndefined(input.directory)
|
||||
const requestedServer = Option.getOrUndefined(input.server)
|
||||
if (requestedDirectory !== undefined) process.chdir(requestedDirectory)
|
||||
const preflight = UpdatePreflight.make()
|
||||
yield* Effect.addFinalizer(() => Effect.promise(() => preflight.close()))
|
||||
const server = yield* ServerConnection.resolve({
|
||||
server: Option.getOrUndefined(input.server),
|
||||
server: requestedServer,
|
||||
standalone: input.standalone,
|
||||
mismatch: "replace",
|
||||
onStart: (reason, previousVersion) => {
|
||||
@@ -75,6 +77,7 @@ export default Runtime.handler(Commands, (input) =>
|
||||
resolve: (spec) =>
|
||||
runPromise(npm.add(spec, { subpaths: ["tui"] }).pipe(Effect.map((result) => result.entrypoint))),
|
||||
},
|
||||
environment: requestedServer === undefined ? Env.session() : undefined,
|
||||
terminalHandoff: () => preflight.finish(),
|
||||
log: (level, message, tags) => {
|
||||
const effect =
|
||||
|
||||
@@ -12,4 +12,13 @@ export const password = Config.redacted("OPENCODE_PASSWORD").pipe(
|
||||
Config.withDefault(undefined),
|
||||
)
|
||||
|
||||
export function session() {
|
||||
return Object.fromEntries(
|
||||
Object.entries(process.env).filter(
|
||||
(entry): entry is [string, string] =>
|
||||
entry[1] !== undefined && entry[0] !== "OPENCODE_PASSWORD" && entry[0] !== "OPENCODE_SERVER_PASSWORD",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
export * as Env from "./env"
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { Env } from "../src/env"
|
||||
|
||||
test("session environment omits server credentials", () => {
|
||||
const previousPassword = process.env.OPENCODE_PASSWORD
|
||||
const previousLegacyPassword = process.env.OPENCODE_SERVER_PASSWORD
|
||||
const previousValue = process.env.OPENCODE_SESSION_ENV_TEST
|
||||
process.env.OPENCODE_PASSWORD = "password"
|
||||
process.env.OPENCODE_SERVER_PASSWORD = "legacy"
|
||||
process.env.OPENCODE_SESSION_ENV_TEST = "included"
|
||||
|
||||
const environment = Env.session()
|
||||
|
||||
if (previousPassword === undefined) delete process.env.OPENCODE_PASSWORD
|
||||
else process.env.OPENCODE_PASSWORD = previousPassword
|
||||
if (previousLegacyPassword === undefined) delete process.env.OPENCODE_SERVER_PASSWORD
|
||||
else process.env.OPENCODE_SERVER_PASSWORD = previousLegacyPassword
|
||||
if (previousValue === undefined) delete process.env.OPENCODE_SESSION_ENV_TEST
|
||||
else process.env.OPENCODE_SESSION_ENV_TEST = previousValue
|
||||
|
||||
expect(environment.OPENCODE_PASSWORD).toBeUndefined()
|
||||
expect(environment.OPENCODE_SERVER_PASSWORD).toBeUndefined()
|
||||
expect(environment.OPENCODE_SESSION_ENV_TEST).toBe("included")
|
||||
})
|
||||
@@ -914,6 +914,10 @@ export type Endpoint5_34Input = { readonly sessionID: Session.ID; readonly messa
|
||||
export type Endpoint5_34Output = SessionMessage.Info
|
||||
export type SessionMessageOperation<E = never> = (input: Endpoint5_34Input) => Effect.Effect<Endpoint5_34Output, E>
|
||||
|
||||
export type Endpoint5_35Input = { readonly sessionID: Session.ID; readonly variables: { readonly [x: string]: string } }
|
||||
export type Endpoint5_35Output = void
|
||||
export type SessionEnvironmentOperation<E = never> = (input: Endpoint5_35Input) => Effect.Effect<Endpoint5_35Output, E>
|
||||
|
||||
export interface SessionApi<E = never> {
|
||||
readonly list: SessionListOperation<E>
|
||||
readonly create: SessionCreateOperation<E>
|
||||
@@ -958,6 +962,7 @@ export interface SessionApi<E = never> {
|
||||
readonly interrupt: SessionInterruptOperation<E>
|
||||
readonly background: SessionBackgroundOperation<E>
|
||||
readonly message: SessionMessageOperation<E>
|
||||
readonly environment: SessionEnvironmentOperation<E>
|
||||
}
|
||||
|
||||
export type Endpoint6_0Input = {
|
||||
|
||||
@@ -86,6 +86,8 @@ import type {
|
||||
Endpoint5_33Output,
|
||||
Endpoint5_34Input,
|
||||
Endpoint5_34Output,
|
||||
Endpoint5_35Input,
|
||||
Endpoint5_35Output,
|
||||
Endpoint6_0Input,
|
||||
Endpoint6_0Output,
|
||||
Endpoint7_0Input,
|
||||
@@ -610,6 +612,14 @@ const Endpoint5_34 = (raw: RawClient["server.session"]) => (input: Endpoint5_34I
|
||||
),
|
||||
)
|
||||
|
||||
const Endpoint5_35 = (raw: RawClient["server.session"]) => (input: Endpoint5_35Input) =>
|
||||
preserveEffect<Endpoint5_35Output>()(
|
||||
raw["session.environment"]({
|
||||
params: { sessionID: input["sessionID"] },
|
||||
payload: { variables: input["variables"] },
|
||||
}).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
const adaptGroup5 = (raw: RawClient["server.session"]) => ({
|
||||
list: Endpoint5_0(raw),
|
||||
create: Endpoint5_1(raw),
|
||||
@@ -639,6 +649,7 @@ const adaptGroup5 = (raw: RawClient["server.session"]) => ({
|
||||
interrupt: Endpoint5_32(raw),
|
||||
background: Endpoint5_33(raw),
|
||||
message: Endpoint5_34(raw),
|
||||
environment: Endpoint5_35(raw),
|
||||
})
|
||||
|
||||
const Endpoint6_0 = (raw: RawClient["server.message"]) => (input: Endpoint6_0Input) =>
|
||||
|
||||
@@ -80,6 +80,8 @@ import type {
|
||||
SessionBackgroundOutput,
|
||||
SessionMessageInput,
|
||||
SessionMessageOutput,
|
||||
SessionEnvironmentInput,
|
||||
SessionEnvironmentOutput,
|
||||
MessageListInput,
|
||||
MessageListOutput,
|
||||
ModelListInput,
|
||||
@@ -896,6 +898,18 @@ export function make(options: ClientOptions) {
|
||||
},
|
||||
requestOptions,
|
||||
).then((value) => value.data),
|
||||
environment: (input: SessionEnvironmentInput, requestOptions?: RequestOptions) =>
|
||||
request<SessionEnvironmentOutput>(
|
||||
{
|
||||
method: "PUT",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/environment`,
|
||||
body: { variables: input["variables"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [404, 401, 400],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
},
|
||||
message: {
|
||||
list: (input: MessageListInput, requestOptions?: RequestOptions) =>
|
||||
|
||||
@@ -3936,6 +3936,13 @@ export type SessionMessageInput = {
|
||||
|
||||
export type SessionMessageOutput = { data: SessionMessageInfo }["data"]
|
||||
|
||||
export type SessionEnvironmentInput = {
|
||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||
readonly variables: { readonly variables: { readonly [x: string]: string } }["variables"]
|
||||
}
|
||||
|
||||
export type SessionEnvironmentOutput = void
|
||||
|
||||
export type MessageListInput = {
|
||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||
readonly limit?: {
|
||||
|
||||
@@ -51,6 +51,7 @@ import { Global } from "@opencode-ai/util/global"
|
||||
import { Shell as ShellSchema } from "@opencode-ai/schema/shell"
|
||||
import { KeyedMutex } from "./effect/keyed-mutex.js"
|
||||
import { fileURLToPath } from "url"
|
||||
import { SessionEnvironment } from "./session/environment.js"
|
||||
|
||||
// get project -> project.locations
|
||||
//
|
||||
@@ -165,6 +166,10 @@ export interface Interface {
|
||||
input: ForkInput,
|
||||
) => Effect.Effect<SessionSchema.Info, NotFoundError | MessageNotFoundError | ForkEmptyError>
|
||||
readonly get: (sessionID: SessionSchema.ID) => Effect.Effect<SessionSchema.Info, NotFoundError>
|
||||
readonly environment: (input: {
|
||||
readonly sessionID: SessionSchema.ID
|
||||
readonly variables?: SessionEnvironment.Variables
|
||||
}) => Effect.Effect<SessionEnvironment.Variables | undefined, NotFoundError>
|
||||
readonly remove: (sessionID: SessionSchema.ID) => Effect.Effect<void, NotFoundError>
|
||||
readonly messages: (input: {
|
||||
sessionID: SessionSchema.ID
|
||||
@@ -304,6 +309,7 @@ const layer = Layer.effect(
|
||||
const locations = yield* LocationServiceMap.Service
|
||||
const fs = yield* FSUtil.Service
|
||||
const jobs = yield* Job.Service
|
||||
const environments = yield* SessionEnvironment.Service
|
||||
const scope = yield* Scope.Scope
|
||||
const activeShells = new Set<SessionSchema.ID>()
|
||||
const shellLocks = KeyedMutex.makeUnsafe<SessionSchema.ID>()
|
||||
@@ -447,6 +453,11 @@ const layer = Layer.effect(
|
||||
if (!session) return yield* new NotFoundError({ sessionID })
|
||||
return session
|
||||
}),
|
||||
environment: Effect.fn("Session.environment")(function* (input) {
|
||||
yield* result.get(input.sessionID)
|
||||
if (input.variables !== undefined) yield* environments.set(input.sessionID, input.variables)
|
||||
return yield* environments.get(input.sessionID)
|
||||
}),
|
||||
remove: Effect.fn("Session.remove")(function* (sessionID) {
|
||||
const session = yield* result.get(sessionID)
|
||||
yield* execution.interrupt(sessionID)
|
||||
@@ -454,6 +465,7 @@ const layer = Layer.effect(
|
||||
yield* closeTransport(session)
|
||||
const children = yield* result.list({ parentID: sessionID })
|
||||
yield* Effect.forEach(children.data, (child) => result.remove(child.id), { concurrency: 1, discard: true })
|
||||
yield* environments.clear(sessionID)
|
||||
yield* bus.publish(SessionEvent.Deleted, { sessionID })
|
||||
yield* bus.remove(sessionID)
|
||||
}),
|
||||
@@ -652,7 +664,12 @@ const layer = Layer.effect(
|
||||
const started = yield* Effect.gen(function* () {
|
||||
const shell = yield* Shell.Service
|
||||
return yield* shell
|
||||
.create({ command: input.command, cwd: session.location.directory, timeout: 0 })
|
||||
.create({
|
||||
command: input.command,
|
||||
cwd: session.location.directory,
|
||||
timeout: 0,
|
||||
metadata: { sessionID: input.sessionID },
|
||||
})
|
||||
.pipe(Effect.orDie)
|
||||
}).pipe(Effect.provide(locations.get(session.location)))
|
||||
yield* bus.publish(
|
||||
@@ -1102,6 +1119,7 @@ export const node = makeGlobalNode({
|
||||
layer: layer.pipe(Layer.orDie),
|
||||
deps: [
|
||||
Job.node,
|
||||
SessionEnvironment.node,
|
||||
Database.node,
|
||||
Bus.node,
|
||||
Project.node,
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
export * as SessionEnvironment from "./environment.js"
|
||||
|
||||
import { Context, Effect, Layer, Ref } from "effect"
|
||||
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { SessionSchema } from "./schema.js"
|
||||
|
||||
export type Variables = Readonly<Record<string, string>>
|
||||
|
||||
export interface Interface {
|
||||
readonly get: (sessionID: SessionSchema.ID) => Effect.Effect<Variables | undefined>
|
||||
readonly set: (sessionID: SessionSchema.ID, variables: Variables) => Effect.Effect<void>
|
||||
readonly clear: (sessionID: SessionSchema.ID) => Effect.Effect<void>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/SessionEnvironment") {}
|
||||
|
||||
const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const environments = yield* Ref.make(new Map<SessionSchema.ID, Variables>())
|
||||
|
||||
return Service.of({
|
||||
get: Effect.fn("SessionEnvironment.get")(function* (sessionID) {
|
||||
return (yield* Ref.get(environments)).get(sessionID)
|
||||
}),
|
||||
set: Effect.fn("SessionEnvironment.set")(function* (sessionID, variables) {
|
||||
yield* Ref.update(environments, (current) => new Map(current).set(sessionID, { ...variables }))
|
||||
}),
|
||||
clear: Effect.fn("SessionEnvironment.clear")(function* (sessionID) {
|
||||
yield* Ref.update(environments, (current) => {
|
||||
if (!current.has(sessionID)) return current
|
||||
const next = new Map(current)
|
||||
next.delete(sessionID)
|
||||
return next
|
||||
})
|
||||
}),
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
export const node = makeGlobalNode({ service: Service, layer, deps: [] })
|
||||
@@ -15,6 +15,8 @@ import { Global } from "@opencode-ai/util/global"
|
||||
import { ShellSelect } from "./shell/select.js"
|
||||
import type { ShellCreateBefore } from "@opencode-ai/plugin/effect/shell"
|
||||
import { PluginHooks } from "./plugin/hooks.js"
|
||||
import { SessionEnvironment } from "./session/environment.js"
|
||||
import { SessionSchema } from "./session/schema.js"
|
||||
|
||||
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Shell.NotFoundError", {
|
||||
id: Shell.ID,
|
||||
@@ -76,6 +78,7 @@ export const layer = (options?: ShellSelect.Options) =>
|
||||
const global = yield* Global.Service
|
||||
const environment = yield* Environment.Service
|
||||
const hooks = yield* PluginHooks.Service
|
||||
const environments = yield* SessionEnvironment.Service
|
||||
const context = yield* Effect.context()
|
||||
const runFork = Effect.runForkWith(context)
|
||||
const sessions = new Map<string, Active>()
|
||||
@@ -184,13 +187,18 @@ export const layer = (options?: ShellSelect.Options) =>
|
||||
input: Shell.CreateInput,
|
||||
before?: (input: ShellCreateBefore) => Effect.Effect<void, E, R>,
|
||||
) {
|
||||
const sessionID = input.metadata?.sessionID
|
||||
const sessionEnvironment =
|
||||
location.workspaceID === undefined && Schema.is(SessionSchema.ID)(sessionID)
|
||||
? yield* environments.get(sessionID)
|
||||
: undefined
|
||||
const invocation: ShellCreateBefore = {
|
||||
command: input.command,
|
||||
cwd: input.cwd ?? location.directory,
|
||||
timeout: input.timeout,
|
||||
shell: yield* resolve(),
|
||||
env: {
|
||||
...process.env,
|
||||
...(sessionEnvironment ?? process.env),
|
||||
TERM: "xterm-256color",
|
||||
OPENCODE_TERMINAL: "1",
|
||||
},
|
||||
@@ -349,7 +357,15 @@ export function configured(options?: ShellSelect.Options) {
|
||||
return makeLocationNode({
|
||||
service: Service,
|
||||
layer: layer(options),
|
||||
deps: [Bus.node, Location.node, Config.node, Global.node, Environment.node, PluginHooks.node],
|
||||
deps: [
|
||||
Bus.node,
|
||||
Location.node,
|
||||
Config.node,
|
||||
Global.node,
|
||||
Environment.node,
|
||||
PluginHooks.node,
|
||||
SessionEnvironment.node,
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -679,13 +679,18 @@ describe("Session.create", () => {
|
||||
const created = yield* session.create({
|
||||
location: Location.Ref.make({ directory: AbsolutePath.make(directory) }),
|
||||
})
|
||||
yield* session.environment({ sessionID: created.id, variables: { OPENCODE_SESSION_ENV_TEST: "attached" } })
|
||||
|
||||
yield* session.shell({ sessionID: created.id, command: "echo hello" })
|
||||
const command =
|
||||
process.platform === "win32"
|
||||
? "[Console]::Out.Write($env:OPENCODE_SESSION_ENV_TEST)"
|
||||
: 'printf %s "$OPENCODE_SESSION_ENV_TEST"'
|
||||
yield* session.shell({ sessionID: created.id, command })
|
||||
|
||||
const messages = yield* session.messages({ sessionID: created.id, order: "asc" })
|
||||
const shell = messages.find((message): message is SessionMessage.Shell => message.type === "shell")
|
||||
expect(shell).toMatchObject({ type: "shell", command: "echo hello", status: "exited", exit: 0 })
|
||||
expect(shell?.output?.output).toContain("hello")
|
||||
expect(shell).toMatchObject({ type: "shell", command, status: "exited", exit: 0 })
|
||||
expect(shell?.output?.output).toContain("attached")
|
||||
expect(shell?.output?.truncated).toBe(false)
|
||||
expect(shell?.time.completed).toBeDefined()
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { Session } from "@opencode-ai/core/session"
|
||||
import { SessionEnvironment } from "@opencode-ai/core/session/environment"
|
||||
import { testEffect } from "./lib/effect"
|
||||
|
||||
const it = testEffect(AppNodeBuilder.build(SessionEnvironment.node))
|
||||
|
||||
describe("SessionEnvironment", () => {
|
||||
it.effect("stores replacement snapshots by session", () =>
|
||||
Effect.gen(function* () {
|
||||
const environments = yield* SessionEnvironment.Service
|
||||
const first = Session.ID.make("ses_environment_first")
|
||||
const second = Session.ID.make("ses_environment_second")
|
||||
|
||||
yield* environments.set(first, { TOOLCHAIN: "first", PATH: "/first/bin" })
|
||||
yield* environments.set(second, { TOOLCHAIN: "second" })
|
||||
yield* environments.set(first, { TOOLCHAIN: "updated" })
|
||||
|
||||
expect(yield* environments.get(first)).toEqual({ TOOLCHAIN: "updated" })
|
||||
expect(yield* environments.get(second)).toEqual({ TOOLCHAIN: "second" })
|
||||
|
||||
yield* environments.clear(first)
|
||||
|
||||
expect(yield* environments.get(first)).toBeUndefined()
|
||||
expect(yield* environments.get(second)).toEqual({ TOOLCHAIN: "second" })
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -12,6 +12,7 @@ import { SessionExecution } from "@opencode-ai/core/session/execution"
|
||||
import { SessionModelTransport } from "@opencode-ai/core/session/model-transport"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { SessionStore } from "@opencode-ai/core/session/store"
|
||||
import { SessionEnvironment } from "@opencode-ai/core/session/environment"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-services"
|
||||
import { testEffect } from "./lib/effect"
|
||||
import { globalProjectLayer } from "./lib/project"
|
||||
@@ -32,6 +33,7 @@ const it = testEffect(
|
||||
Bus.node,
|
||||
SessionProjector.node,
|
||||
SessionStore.node,
|
||||
SessionEnvironment.node,
|
||||
Session.node,
|
||||
LocationServiceMap.node,
|
||||
]),
|
||||
@@ -50,6 +52,8 @@ describe("Session.remove", () => {
|
||||
const session = yield* Session.Service
|
||||
const parent = yield* session.create({ location })
|
||||
const child = yield* session.create({ parentID: parent.id })
|
||||
yield* session.environment({ sessionID: parent.id, variables: { SESSION_ENV: "parent" } })
|
||||
yield* session.environment({ sessionID: child.id, variables: { SESSION_ENV: "child" } })
|
||||
yield* (yield* LocationServiceMap.Service).contextEffect(location)
|
||||
closed.length = 0
|
||||
|
||||
@@ -57,6 +61,9 @@ describe("Session.remove", () => {
|
||||
|
||||
expect((yield* session.list()).data).toEqual([])
|
||||
expect(closed).toEqual([parent.id, child.id])
|
||||
const environments = yield* SessionEnvironment.Service
|
||||
expect(yield* environments.get(parent.id)).toBeUndefined()
|
||||
expect(yield* environments.get(child.id)).toBeUndefined()
|
||||
expect(yield* Effect.result(session.get(parent.id))).toMatchObject({ _tag: "Failure" })
|
||||
expect(yield* Effect.result(session.get(child.id))).toMatchObject({ _tag: "Failure" })
|
||||
}),
|
||||
|
||||
@@ -260,6 +260,36 @@ describe("ShellTool", () => {
|
||||
{ timeout: 15_000 },
|
||||
)
|
||||
|
||||
productionIt.live(
|
||||
"uses the session environment instead of the server environment",
|
||||
() =>
|
||||
Effect.acquireUseRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => {
|
||||
reset()
|
||||
return withSession(tmp.path, (registry) =>
|
||||
Effect.gen(function* () {
|
||||
const sessions = yield* Session.Service
|
||||
yield* sessions.environment({
|
||||
sessionID,
|
||||
variables: { OPENCODE_SESSION_ENV_TEST: "from-session" },
|
||||
})
|
||||
const command = isWindows
|
||||
? "[Console]::Out.Write($env:OPENCODE_SESSION_ENV_TEST)"
|
||||
: 'printf %s "$OPENCODE_SESSION_ENV_TEST"'
|
||||
|
||||
const settled = yield* executeTool(registry, call({ command }))
|
||||
|
||||
expect(settled.status).toBe("completed")
|
||||
expect(settled.content?.[0]).toEqual({ type: "text", text: "from-session" })
|
||||
}),
|
||||
)
|
||||
},
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]().then(() => undefined)),
|
||||
),
|
||||
{ timeout: 15_000 },
|
||||
)
|
||||
|
||||
it.live("resolves a relative workdir from the active Location", () =>
|
||||
Effect.acquireUseRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
|
||||
@@ -693,6 +693,20 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.put("session.environment", "/api/session/:sessionID/environment", {
|
||||
params: { sessionID: Session.ID },
|
||||
payload: Schema.Struct({ variables: Schema.Record(Schema.String, Schema.String) }),
|
||||
success: HttpApiSchema.NoContent,
|
||||
error: SessionNotFoundError,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.environment",
|
||||
summary: "Set session environment",
|
||||
description: "Replace the process environment used by local shell commands for this session.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "session",
|
||||
|
||||
@@ -196,6 +196,22 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"session.environment",
|
||||
Effect.fn(function* (ctx) {
|
||||
yield* session.environment({ sessionID: ctx.params.sessionID, variables: ctx.payload.variables }).pipe(
|
||||
Effect.catchTag(
|
||||
"Session.NotFoundError",
|
||||
(error) =>
|
||||
new SessionNotFoundError({
|
||||
sessionID: error.sessionID,
|
||||
message: `Session not found: ${error.sessionID}`,
|
||||
}),
|
||||
),
|
||||
)
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"session.fork",
|
||||
Effect.fn(function* (ctx) {
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
useTuiApp,
|
||||
useTuiPaths,
|
||||
useTuiStartup,
|
||||
useTuiTerminalEnvironment,
|
||||
type TuiApp,
|
||||
} from "./context/runtime"
|
||||
import { DialogProvider, useDialog } from "./ui/dialog"
|
||||
@@ -185,6 +186,7 @@ export type TuiInput = {
|
||||
args: Args
|
||||
config: Config.Interface
|
||||
packages: PackageResolver
|
||||
environment?: Readonly<Record<string, string>>
|
||||
terminalHandoff?: () => Promise<
|
||||
| {
|
||||
readonly renderer: CliRenderer
|
||||
@@ -332,6 +334,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
|
||||
: process.env.DISPLAY
|
||||
? "x11"
|
||||
: undefined,
|
||||
variables: input.environment,
|
||||
}}
|
||||
>
|
||||
<TuiStartupProvider
|
||||
@@ -480,6 +483,17 @@ function App(props: { pair?: DialogPairCredentials }) {
|
||||
const promptRef = usePromptRef()
|
||||
const plugins = usePlugin()
|
||||
const clipboard = useClipboard()
|
||||
const terminalEnvironment = useTuiTerminalEnvironment()
|
||||
createEffect(() => {
|
||||
if (client.connection.status() !== "connected") return
|
||||
if (route.data.type !== "session") return
|
||||
const session = data.session.get(route.data.sessionID)
|
||||
if (!session) return
|
||||
if (session.location.workspaceID !== undefined || terminalEnvironment.variables === undefined) return
|
||||
void client.api.session
|
||||
.environment({ sessionID: session.id, variables: terminalEnvironment.variables })
|
||||
.catch(toast.error)
|
||||
})
|
||||
const [layout, updateLayout] = useStorage().store<{ verticalTabsWidth?: number }>("layout", {
|
||||
initial: { verticalTabsWidth: SESSION_SIDEBAR_WIDTH },
|
||||
})
|
||||
|
||||
@@ -1206,6 +1206,19 @@ export function Prompt(props: PromptProps) {
|
||||
|
||||
sessionID = created.id
|
||||
session = created
|
||||
if (created.location.workspaceID === undefined && terminalEnvironment.variables !== undefined) {
|
||||
const error = await client.api.session
|
||||
.environment({ sessionID, variables: terminalEnvironment.variables })
|
||||
.then(
|
||||
() => undefined,
|
||||
(error) => error,
|
||||
)
|
||||
if (error) {
|
||||
if (finishMoveProgress) move.finishSubmit()
|
||||
toast.show({ title: "Failed to set session environment", message: errorMessage(error), variant: "error" })
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Capture mode before it gets reset
|
||||
|
||||
@@ -17,6 +17,7 @@ export type TuiTerminalEnvironment = Readonly<{
|
||||
platform: string
|
||||
multiplexer?: "tmux" | "screen"
|
||||
displayServer?: "wayland" | "x11"
|
||||
variables?: Readonly<Record<string, string>>
|
||||
}>
|
||||
|
||||
export type TuiStartup = Readonly<{
|
||||
|
||||
Reference in New Issue
Block a user