mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-09 03:00:33 +00:00
feat: Add terminal shell selection to settings
This commit is contained in:
parent
48db7cf07a
commit
9cbfcfad12
15 changed files with 199 additions and 43 deletions
2
bun.lock
2
bun.lock
|
|
@ -14,7 +14,9 @@
|
|||
"devDependencies": {
|
||||
"@actions/artifact": "5.0.1",
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/mime-types": "3.0.1",
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"glob": "13.0.5",
|
||||
"husky": "9.1.7",
|
||||
|
|
|
|||
|
|
@ -1,29 +1,8 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@tsconfig/bun/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "Preserve",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
"types": ["node", "bun"]
|
||||
},
|
||||
"include": ["./index.ts", "./sst-env.d.ts"]
|
||||
}
|
||||
|
|
|
|||
8
nix/scripts/tsconfig.json
Normal file
8
nix/scripts/tsconfig.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@tsconfig/bun/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"types": ["node", "bun"]
|
||||
},
|
||||
"include": ["./**/*.ts"]
|
||||
}
|
||||
|
|
@ -74,7 +74,9 @@
|
|||
"devDependencies": {
|
||||
"@actions/artifact": "5.0.1",
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/mime-types": "3.0.1",
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"glob": "13.0.5",
|
||||
"husky": "9.1.7",
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme/context"
|
|||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import {
|
||||
monoDefault,
|
||||
monoFontFamily,
|
||||
|
|
@ -133,6 +135,21 @@ export const SettingsGeneral: Component = () => {
|
|||
|
||||
const themeOptions = createMemo<ThemeOption[]>(() => theme.ids().map((id) => ({ id, name: theme.name(id) })))
|
||||
|
||||
const globalSync = useGlobalSync()
|
||||
const globalSdk = useGlobalSDK()
|
||||
|
||||
const [shells] = createResource(() => globalSdk.client.pty.shells().then((res) => res.data || []))
|
||||
|
||||
const shellOptions = createMemo(() => {
|
||||
const list = shells() || []
|
||||
const current = globalSync.data.config.shell
|
||||
const options = list.map((s) => ({ value: s, label: s }))
|
||||
if (current && !list.includes(current)) {
|
||||
options.unshift({ value: current, label: current })
|
||||
}
|
||||
return options
|
||||
})
|
||||
|
||||
const colorSchemeOptions = createMemo((): { value: ColorScheme; label: string }[] => [
|
||||
{ value: "system", label: language.t("theme.scheme.system") },
|
||||
{ value: "light", label: language.t("theme.scheme.light") },
|
||||
|
|
@ -206,6 +223,32 @@ export const SettingsGeneral: Component = () => {
|
|||
/>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
title={language.t("settings.general.row.shell.title")}
|
||||
description={language.t("settings.general.row.shell.description")}
|
||||
>
|
||||
<Select
|
||||
data-action="settings-shell"
|
||||
options={shellOptions()}
|
||||
current={
|
||||
shellOptions().find((o) => o.value === globalSync.data.config.shell) ?? {
|
||||
value: "auto",
|
||||
label: "Auto (Default)",
|
||||
}
|
||||
}
|
||||
value={(o) => o.value}
|
||||
label={(o) => o.label}
|
||||
onSelect={(option) => {
|
||||
const value = option?.value === "auto" ? undefined : option?.value
|
||||
globalSync.updateConfig({ shell: value })
|
||||
}}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
triggerVariant="settings"
|
||||
triggerStyle={{ "min-width": "180px" }}
|
||||
/>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
title={language.t("settings.general.row.reasoningSummaries.title")}
|
||||
description={language.t("settings.general.row.reasoningSummaries.description")}
|
||||
|
|
@ -301,23 +344,24 @@ export const SettingsGeneral: Component = () => {
|
|||
}
|
||||
>
|
||||
<Select
|
||||
data-action="settings-theme"
|
||||
options={themeOptions()}
|
||||
current={themeOptions().find((o) => o.id === theme.themeId())}
|
||||
value={(o) => o.id}
|
||||
label={(o) => o.name}
|
||||
data-action="settings-shell"
|
||||
options={shellOptions()}
|
||||
current={
|
||||
shellOptions().find((o) => o.value === globalSync.data.config.shell) ?? {
|
||||
value: "auto",
|
||||
label: "Auto (Default)",
|
||||
}
|
||||
}
|
||||
value={(o) => o.value}
|
||||
label={(o) => o.label}
|
||||
onSelect={(option) => {
|
||||
if (!option) return
|
||||
theme.setTheme(option.id)
|
||||
}}
|
||||
onHighlight={(option) => {
|
||||
if (!option) return
|
||||
theme.previewTheme(option.id)
|
||||
return () => theme.cancelPreview()
|
||||
const value = option?.value === "auto" ? undefined : option?.value
|
||||
globalSync.updateConfig({ shell: value })
|
||||
}}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
triggerVariant="settings"
|
||||
triggerStyle={{ "min-width": "180px" }}
|
||||
/>
|
||||
</SettingsRow>
|
||||
|
||||
|
|
|
|||
|
|
@ -723,6 +723,9 @@ export const dict = {
|
|||
|
||||
"settings.general.row.language.title": "Language",
|
||||
"settings.general.row.language.description": "Change the display language for OpenCode",
|
||||
"settings.general.row.shell.title": "Terminal Shell",
|
||||
"settings.general.row.shell.description":
|
||||
"Choose the default shell used for your terminal and the agent's background processes.",
|
||||
"settings.general.row.appearance.title": "Appearance",
|
||||
"settings.general.row.appearance.description": "Customise how OpenCode looks on your device",
|
||||
"settings.general.row.colorScheme.title": "Color scheme",
|
||||
|
|
|
|||
|
|
@ -803,6 +803,7 @@ export namespace Config {
|
|||
export const Info = z
|
||||
.object({
|
||||
$schema: z.string().optional().describe("JSON schema reference for configuration validation"),
|
||||
shell: z.string().optional().describe("Default shell to use for terminal and bash tool"),
|
||||
logLevel: Log.Level.optional().describe("Log level"),
|
||||
server: Server.optional().describe("Server configuration for opencode serve and web commands"),
|
||||
command: z
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { Shell } from "@/shell/shell"
|
|||
import { Plugin } from "@/plugin"
|
||||
import { PtyID } from "./schema"
|
||||
import { Effect, Layer, ServiceMap } from "effect"
|
||||
import { Config } from "../config/config"
|
||||
|
||||
export namespace Pty {
|
||||
const log = Log.create({ service: "pty" })
|
||||
|
|
@ -172,9 +173,10 @@ export namespace Pty {
|
|||
|
||||
const create = Effect.fn("Pty.create")(function* (input: CreateInput) {
|
||||
const s = yield* InstanceState.get(state)
|
||||
const config = yield* Effect.promise(() => Config.get())
|
||||
return yield* Effect.promise(async () => {
|
||||
const id = PtyID.ascending()
|
||||
const command = input.command || Shell.preferred()
|
||||
const command = input.command || Shell.preferred(config.shell)
|
||||
const args = input.args || []
|
||||
if (Shell.login(command)) {
|
||||
args.push("-l")
|
||||
|
|
|
|||
|
|
@ -7,9 +7,31 @@ import { PtyID } from "@/pty/schema"
|
|||
import { NotFoundError } from "../../storage/db"
|
||||
import { errors } from "../error"
|
||||
import { lazy } from "../../util/lazy"
|
||||
import { Shell } from "@/shell/shell"
|
||||
|
||||
export const PtyRoutes = lazy(() =>
|
||||
new Hono()
|
||||
.get(
|
||||
"/shells",
|
||||
describeRoute({
|
||||
summary: "List available shells",
|
||||
description: "Get a list of available shells on the system.",
|
||||
operationId: "pty.shells",
|
||||
responses: {
|
||||
200: {
|
||||
description: "List of shells",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.array(z.string())),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
return c.json(await Shell.available())
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/",
|
||||
describeRoute({
|
||||
|
|
|
|||
|
|
@ -104,7 +104,33 @@ export namespace Shell {
|
|||
return POSIX.has(name(file))
|
||||
}
|
||||
|
||||
export const preferred = lazy(() => select(process.env.SHELL))
|
||||
const defaultPreferred = lazy(() => select(process.env.SHELL))
|
||||
const defaultAcceptable = lazy(() => select(process.env.SHELL, { acceptable: true }))
|
||||
|
||||
export const acceptable = lazy(() => select(process.env.SHELL, { acceptable: true }))
|
||||
export function preferred(configShell?: string) {
|
||||
if (configShell) return select(configShell)
|
||||
return defaultPreferred()
|
||||
}
|
||||
preferred.reset = () => defaultPreferred.reset()
|
||||
|
||||
export function acceptable(configShell?: string) {
|
||||
if (configShell) return select(configShell, { acceptable: true })
|
||||
return defaultAcceptable()
|
||||
}
|
||||
acceptable.reset = () => defaultAcceptable.reset()
|
||||
|
||||
export async function available(): Promise<string[]> {
|
||||
if (process.platform === "win32") {
|
||||
return [gitbash(), Bun.which("pwsh"), Bun.which("powershell"), process.env.COMSPEC || "cmd.exe"].filter(
|
||||
Boolean,
|
||||
) as string[]
|
||||
} else {
|
||||
try {
|
||||
const text = await import("fs/promises").then((fs) => fs.readFile("/etc/shells", "utf-8"))
|
||||
return text.split("\n").filter((line) => line.trim() && !line.startsWith("#"))
|
||||
} catch {
|
||||
return ["/bin/bash", "/bin/zsh", "/bin/sh"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { Shell } from "@/shell/shell"
|
|||
import { BashArity } from "@/permission/arity"
|
||||
import { Truncate } from "./truncate"
|
||||
import { Plugin } from "@/plugin"
|
||||
import { Config } from "../config/config"
|
||||
|
||||
const MAX_METADATA_LENGTH = 30_000
|
||||
const DEFAULT_TIMEOUT = Flag.OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS || 2 * 60 * 1000
|
||||
|
|
@ -441,7 +442,8 @@ const parser = lazy(async () => {
|
|||
|
||||
// TODO: we may wanna rename this tool so it works better on other shells
|
||||
export const BashTool = Tool.define("bash", async () => {
|
||||
const shell = Shell.acceptable()
|
||||
const config = await Config.get()
|
||||
const shell = Shell.acceptable(config.shell)
|
||||
const name = Shell.name(shell)
|
||||
const chain =
|
||||
name === "powershell"
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ import type {
|
|||
PtyListResponses,
|
||||
PtyRemoveErrors,
|
||||
PtyRemoveResponses,
|
||||
PtyShellsResponses,
|
||||
PtyUpdateErrors,
|
||||
PtyUpdateResponses,
|
||||
QuestionAnswer,
|
||||
|
|
@ -663,6 +664,36 @@ export class Project extends HeyApiClient {
|
|||
}
|
||||
|
||||
export class Pty extends HeyApiClient {
|
||||
/**
|
||||
* List available shells
|
||||
*
|
||||
* Get a list of available shells on the system.
|
||||
*/
|
||||
public shells<ThrowOnError extends boolean = false>(
|
||||
parameters?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
},
|
||||
options?: Options<never, ThrowOnError>,
|
||||
) {
|
||||
const params = buildClientParams(
|
||||
[parameters],
|
||||
[
|
||||
{
|
||||
args: [
|
||||
{ in: "query", key: "directory" },
|
||||
{ in: "query", key: "workspace" },
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
return (options?.client ?? this.client).get<PtyShellsResponses, unknown, ThrowOnError>({
|
||||
url: "/pty/shells",
|
||||
...options,
|
||||
...params,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* List PTY sessions
|
||||
*
|
||||
|
|
|
|||
|
|
@ -1417,6 +1417,10 @@ export type Config = {
|
|||
* JSON schema reference for configuration validation
|
||||
*/
|
||||
$schema?: string
|
||||
/**
|
||||
* Default shell to use for terminal and bash tool
|
||||
*/
|
||||
shell?: string
|
||||
logLevel?: LogLevel
|
||||
server?: ServerConfig
|
||||
/**
|
||||
|
|
@ -2400,6 +2404,25 @@ export type ProjectUpdateResponses = {
|
|||
|
||||
export type ProjectUpdateResponse = ProjectUpdateResponses[keyof ProjectUpdateResponses]
|
||||
|
||||
export type PtyShellsData = {
|
||||
body?: never
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/pty/shells"
|
||||
}
|
||||
|
||||
export type PtyShellsResponses = {
|
||||
/**
|
||||
* List of shells
|
||||
*/
|
||||
200: Array<string>
|
||||
}
|
||||
|
||||
export type PtyShellsResponse = PtyShellsResponses[keyof PtyShellsResponses]
|
||||
|
||||
export type PtyListData = {
|
||||
body?: never
|
||||
path?: never
|
||||
|
|
|
|||
8
script/tsconfig.json
Normal file
8
script/tsconfig.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@tsconfig/bun/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"types": ["node", "bun"]
|
||||
},
|
||||
"include": ["./**/*.ts"]
|
||||
}
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@tsconfig/bun/tsconfig.json",
|
||||
"compilerOptions": {}
|
||||
"compilerOptions": {
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["sst.config.ts", "sst-env.d.ts", "infra/**/*.ts"]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue