mirror of
https://github.com/anomalyco/opencode.git
synced 2026-03-17 12:14:29 +00:00
Compare commits
10 Commits
effectify-
...
fix/shell-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1ef03d53d | ||
|
|
c40b478756 | ||
|
|
a64f604d54 | ||
|
|
d7093abf61 | ||
|
|
60af447908 | ||
|
|
1cdc558ac0 | ||
|
|
3849822769 | ||
|
|
e9a17e4480 | ||
|
|
68809365df | ||
|
|
8da511dfa8 |
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-WJgo6UclmtQOEubnKMZybdIEhZ1uRTucF61yojjd+l0=",
|
||||
"aarch64-linux": "sha256-QfZ/g7EZFpe6ndR3dG8WvVfMj5Kyd/R/4kkTJfGJxL4=",
|
||||
"aarch64-darwin": "sha256-ezr/R70XJr9eN5l3mgb7HzLF6QsofNEKUOtuxbfli80=",
|
||||
"x86_64-darwin": "sha256-MbsBGS415uEU/n1RQ/5H5pqh+udLY3+oimJ+eS5uJVI="
|
||||
"x86_64-linux": "sha256-VF3rXpIz9XbTTfM8YB98DJJOs4Sotaq5cSwIBUfbNDA=",
|
||||
"aarch64-linux": "sha256-cIE10+0xhb5u0TQedaDbEu6e40ypHnSBmh8unnhCDZE=",
|
||||
"aarch64-darwin": "sha256-d/l7g/4angRw/oxoSGpcYL0i9pNphgRChJwhva5Kypo=",
|
||||
"x86_64-darwin": "sha256-WQyuUKMfHpO1rpWsjhCXuG99iX2jEdSe3AVltxvt+1Y="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import { DialogAlert } from "../../ui/dialog-alert"
|
||||
import { useToast } from "../../ui/toast"
|
||||
import { useKV } from "../../context/kv"
|
||||
import { useTextareaKeybindings } from "../textarea-keybindings"
|
||||
import { shellPassthrough } from "./key"
|
||||
import { DialogSkill } from "../dialog-skill"
|
||||
|
||||
export type PromptProps = {
|
||||
@@ -97,6 +98,21 @@ export function Prompt(props: PromptProps) {
|
||||
const pasteStyleId = syntax().getStyleId("extmark.paste")!
|
||||
let promptPartTypeId = 0
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => store.mode,
|
||||
(mode, prev) => {
|
||||
if (prev === "shell") command.keybinds(true)
|
||||
if (mode === "shell") command.keybinds(false)
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
onCleanup(() => {
|
||||
if (store.mode === "shell") command.keybinds(true)
|
||||
})
|
||||
|
||||
sdk.event.on(TuiEvent.PromptAppend.type, (evt) => {
|
||||
if (!input || input.isDestroyed) return
|
||||
input.insertText(evt.properties.text)
|
||||
@@ -894,6 +910,10 @@ export function Prompt(props: PromptProps) {
|
||||
return
|
||||
}
|
||||
if (store.mode === "shell") {
|
||||
if (shellPassthrough(keybind, e, store.mode)) {
|
||||
e.stopPropagation()
|
||||
return
|
||||
}
|
||||
if ((e.name === "backspace" && input.visualCursor.offset === 0) || e.name === "escape") {
|
||||
setStore("mode", "normal")
|
||||
e.preventDefault()
|
||||
|
||||
16
packages/opencode/src/cli/cmd/tui/component/prompt/key.ts
Normal file
16
packages/opencode/src/cli/cmd/tui/component/prompt/key.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { KeybindKey } from "@/cli/cmd/tui/context/keybind"
|
||||
|
||||
type Mode = "normal" | "shell"
|
||||
|
||||
type Key = {
|
||||
readonly name?: string
|
||||
readonly shift?: boolean
|
||||
}
|
||||
|
||||
export function shellPassthrough<E extends Key>(
|
||||
keybind: { readonly match: (key: KeybindKey, evt: E) => boolean | undefined },
|
||||
evt: E,
|
||||
mode: Mode,
|
||||
) {
|
||||
return mode === "shell" && (keybind.match("agent_cycle", evt) || keybind.match("agent_cycle_reverse", evt))
|
||||
}
|
||||
@@ -70,7 +70,7 @@ function init() {
|
||||
useKeyboard((evt) => {
|
||||
if (store.stack.length === 0) return
|
||||
if (evt.defaultPrevented) return
|
||||
if ((evt.name === "escape" || (evt.ctrl && evt.name === "c")) && renderer.getSelection()) return
|
||||
if ((evt.name === "escape" || (evt.ctrl && evt.name === "c")) && renderer.getSelection()?.getSelectedText()) return
|
||||
if (evt.name === "escape" || (evt.ctrl && evt.name === "c")) {
|
||||
const current = store.stack.at(-1)!
|
||||
current.onClose?.()
|
||||
|
||||
@@ -17,17 +17,21 @@ export namespace Editor {
|
||||
await Filesystem.write(filepath, opts.value)
|
||||
opts.renderer.suspend()
|
||||
opts.renderer.currentRenderBuffer.clear()
|
||||
const parts = editor.split(" ")
|
||||
const proc = Process.spawn([...parts, filepath], {
|
||||
stdin: "inherit",
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
})
|
||||
await proc.exited
|
||||
const content = await Filesystem.readText(filepath)
|
||||
opts.renderer.currentRenderBuffer.clear()
|
||||
opts.renderer.resume()
|
||||
opts.renderer.requestRender()
|
||||
return content || undefined
|
||||
try {
|
||||
const parts = editor.split(" ")
|
||||
const proc = Process.spawn([...parts, filepath], {
|
||||
stdin: "inherit",
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
shell: process.platform === "win32",
|
||||
})
|
||||
await proc.exited
|
||||
const content = await Filesystem.readText(filepath)
|
||||
return content || undefined
|
||||
} finally {
|
||||
opts.renderer.currentRenderBuffer.clear()
|
||||
opts.renderer.resume()
|
||||
opts.renderer.requestRender()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -409,9 +409,7 @@ export class FileService extends ServiceMap.Service<FileService, FileService.Ser
|
||||
dirs.add(entry.name + "/")
|
||||
|
||||
const base = path.join(instance.directory, entry.name)
|
||||
const children = await fs.promises
|
||||
.readdir(base, { withFileTypes: true })
|
||||
.catch(() => [] as fs.Dirent[])
|
||||
const children = await fs.promises.readdir(base, { withFileTypes: true }).catch(() => [] as fs.Dirent[])
|
||||
for (const child of children) {
|
||||
if (!child.isDirectory()) continue
|
||||
if (shouldIgnoreNested(child.name)) continue
|
||||
|
||||
@@ -185,12 +185,10 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise<Hooks> {
|
||||
const deploymentType = inputs.deploymentType || "github.com"
|
||||
|
||||
let domain = "github.com"
|
||||
let actualProvider = "github-copilot"
|
||||
|
||||
if (deploymentType === "enterprise") {
|
||||
const enterpriseUrl = inputs.enterpriseUrl
|
||||
domain = normalizeDomain(enterpriseUrl!)
|
||||
actualProvider = "github-copilot-enterprise"
|
||||
}
|
||||
|
||||
const urls = getUrls(domain)
|
||||
@@ -262,8 +260,7 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise<Hooks> {
|
||||
expires: 0,
|
||||
}
|
||||
|
||||
if (actualProvider === "github-copilot-enterprise") {
|
||||
result.provider = "github-copilot-enterprise"
|
||||
if (deploymentType === "enterprise") {
|
||||
result.enterpriseUrl = domain
|
||||
}
|
||||
|
||||
|
||||
@@ -197,16 +197,6 @@ export namespace Provider {
|
||||
options: {},
|
||||
}
|
||||
},
|
||||
"github-copilot-enterprise": async () => {
|
||||
return {
|
||||
autoload: false,
|
||||
async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
|
||||
if (useLanguageModel(sdk)) return sdk.languageModel(modelID)
|
||||
return shouldUseCopilotResponsesApi(modelID) ? sdk.responses(modelID) : sdk.chat(modelID)
|
||||
},
|
||||
options: {},
|
||||
}
|
||||
},
|
||||
azure: async (provider) => {
|
||||
const resource = iife(() => {
|
||||
const name = provider.options?.resourceName
|
||||
@@ -863,20 +853,6 @@ export namespace Provider {
|
||||
|
||||
const configProviders = Object.entries(config.provider ?? {})
|
||||
|
||||
// Add GitHub Copilot Enterprise provider that inherits from GitHub Copilot
|
||||
if (database["github-copilot"]) {
|
||||
const githubCopilot = database["github-copilot"]
|
||||
database["github-copilot-enterprise"] = {
|
||||
...githubCopilot,
|
||||
id: ProviderID.githubCopilotEnterprise,
|
||||
name: "GitHub Copilot Enterprise",
|
||||
models: mapValues(githubCopilot.models, (model) => ({
|
||||
...model,
|
||||
providerID: ProviderID.githubCopilotEnterprise,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
function mergeProvider(providerID: ProviderID, provider: Partial<Info>) {
|
||||
const existing = providers[providerID]
|
||||
if (existing) {
|
||||
@@ -1003,46 +979,16 @@ export namespace Provider {
|
||||
const providerID = ProviderID.make(plugin.auth.provider)
|
||||
if (disabled.has(providerID)) continue
|
||||
|
||||
// For github-copilot plugin, check if auth exists for either github-copilot or github-copilot-enterprise
|
||||
let hasAuth = false
|
||||
const auth = await Auth.get(providerID)
|
||||
if (auth) hasAuth = true
|
||||
|
||||
// Special handling for github-copilot: also check for enterprise auth
|
||||
if (providerID === ProviderID.githubCopilot && !hasAuth) {
|
||||
const enterpriseAuth = await Auth.get("github-copilot-enterprise")
|
||||
if (enterpriseAuth) hasAuth = true
|
||||
}
|
||||
|
||||
if (!hasAuth) continue
|
||||
if (!auth) continue
|
||||
if (!plugin.auth.loader) continue
|
||||
|
||||
// Load for the main provider if auth exists
|
||||
if (auth) {
|
||||
const options = await plugin.auth.loader(() => Auth.get(providerID) as any, database[plugin.auth.provider])
|
||||
const opts = options ?? {}
|
||||
const patch: Partial<Info> = providers[providerID] ? { options: opts } : { source: "custom", options: opts }
|
||||
mergeProvider(providerID, patch)
|
||||
}
|
||||
|
||||
// If this is github-copilot plugin, also register for github-copilot-enterprise if auth exists
|
||||
if (providerID === ProviderID.githubCopilot) {
|
||||
const enterpriseProviderID = ProviderID.githubCopilotEnterprise
|
||||
if (!disabled.has(enterpriseProviderID)) {
|
||||
const enterpriseAuth = await Auth.get(enterpriseProviderID)
|
||||
if (enterpriseAuth) {
|
||||
const enterpriseOptions = await plugin.auth.loader(
|
||||
() => Auth.get(enterpriseProviderID) as any,
|
||||
database[enterpriseProviderID],
|
||||
)
|
||||
const opts = enterpriseOptions ?? {}
|
||||
const patch: Partial<Info> = providers[enterpriseProviderID]
|
||||
? { options: opts }
|
||||
: { source: "custom", options: opts }
|
||||
mergeProvider(enterpriseProviderID, patch)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [id, fn] of Object.entries(CUSTOM_LOADERS)) {
|
||||
|
||||
@@ -18,7 +18,6 @@ export const ProviderID = providerIdSchema.pipe(
|
||||
google: schema.makeUnsafe("google"),
|
||||
googleVertex: schema.makeUnsafe("google-vertex"),
|
||||
githubCopilot: schema.makeUnsafe("github-copilot"),
|
||||
githubCopilotEnterprise: schema.makeUnsafe("github-copilot-enterprise"),
|
||||
amazonBedrock: schema.makeUnsafe("amazon-bedrock"),
|
||||
azure: schema.makeUnsafe("azure"),
|
||||
openrouter: schema.makeUnsafe("openrouter"),
|
||||
|
||||
@@ -3,6 +3,7 @@ import { buffer } from "node:stream/consumers"
|
||||
|
||||
export namespace Process {
|
||||
export type Stdio = "inherit" | "pipe" | "ignore"
|
||||
export type Shell = boolean | string
|
||||
|
||||
export interface Options {
|
||||
cwd?: string
|
||||
@@ -10,6 +11,7 @@ export namespace Process {
|
||||
stdin?: Stdio
|
||||
stdout?: Stdio
|
||||
stderr?: Stdio
|
||||
shell?: Shell
|
||||
abort?: AbortSignal
|
||||
kill?: NodeJS.Signals | number
|
||||
timeout?: number
|
||||
@@ -60,6 +62,7 @@ export namespace Process {
|
||||
cwd: opts.cwd,
|
||||
env: opts.env === null ? {} : opts.env ? { ...process.env, ...opts.env } : undefined,
|
||||
stdio: [opts.stdin ?? "ignore", opts.stdout ?? "ignore", opts.stderr ?? "ignore"],
|
||||
shell: opts.shell,
|
||||
windowsHide: process.platform === "win32",
|
||||
})
|
||||
|
||||
|
||||
26
packages/opencode/test/cli/tui/prompt-key.test.ts
Normal file
26
packages/opencode/test/cli/tui/prompt-key.test.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { shellPassthrough } from "../../../src/cli/cmd/tui/component/prompt/key"
|
||||
|
||||
const key = (name: string, extra: { readonly shift?: boolean } = {}) => ({
|
||||
name,
|
||||
shift: false,
|
||||
...extra,
|
||||
})
|
||||
|
||||
describe("shellPassthrough", () => {
|
||||
test("allows tab agent-cycle bindings to pass through in shell mode", () => {
|
||||
const match = (target: string, evt: { readonly name?: string }) => target === "agent_cycle" && evt.name === "tab"
|
||||
expect(shellPassthrough({ match }, key("tab"), "shell")).toBe(true)
|
||||
})
|
||||
|
||||
test("allows reverse agent-cycle bindings to pass through in shell mode", () => {
|
||||
const match = (target: string, evt: { readonly name?: string; readonly shift?: boolean }) =>
|
||||
target === "agent_cycle_reverse" && evt.name === "tab" && evt.shift
|
||||
expect(shellPassthrough({ match }, key("tab", { shift: true }), "shell")).toBe(true)
|
||||
})
|
||||
|
||||
test("does not bypass agent-cycle outside shell mode", () => {
|
||||
const match = (target: string, evt: { readonly name?: string }) => target === "agent_cycle" && evt.name === "tab"
|
||||
expect(shellPassthrough({ match }, key("tab"), "normal")).toBe(false)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user