mirror of
https://github.com/sst/opencode.git
synced 2025-12-23 10:11:41 +00:00
tui: remove deprecated and unused keybinds to simplify configuration
Users no longer see confusing deprecated keybind options in config that were never implemented (switch_mode, file operations, etc). Keybinds now use consistent Zod schema defaults instead of hardcoded fallbacks, making configuration more predictable. Session interruption (esc) now works consistently through keybind system.
This commit is contained in:
parent
8b2ce5486d
commit
63fbff523f
7 changed files with 346 additions and 163 deletions
|
|
@ -316,7 +316,9 @@ export function Prompt(props: PromptProps) {
|
|||
|
||||
// Expand pasted text inline before submitting
|
||||
const allExtmarks = input.extmarks.getAllForTypeId(promptPartTypeId)
|
||||
const sortedExtmarks = allExtmarks.sort((a: { start: number }, b: { start: number }) => b.start - a.start)
|
||||
const sortedExtmarks = allExtmarks.sort(
|
||||
(a: { start: number }, b: { start: number }) => b.start - a.start,
|
||||
)
|
||||
|
||||
for (const extmark of sortedExtmarks) {
|
||||
const partIndex = store.extmarkToPartIndex.get(extmark.id)
|
||||
|
|
@ -472,15 +474,28 @@ export function Prompt(props: PromptProps) {
|
|||
<box
|
||||
flexDirection="row"
|
||||
{...SplitBorder}
|
||||
borderColor={keybind.leader ? Theme.accent : store.mode === "shell" ? Theme.secondary : undefined}
|
||||
borderColor={
|
||||
keybind.leader ? Theme.accent : store.mode === "shell" ? Theme.secondary : undefined
|
||||
}
|
||||
justifyContent="space-evenly"
|
||||
>
|
||||
<box backgroundColor={Theme.backgroundElement} width={3} height="100%" alignItems="center" paddingTop={1}>
|
||||
<box
|
||||
backgroundColor={Theme.backgroundElement}
|
||||
width={3}
|
||||
height="100%"
|
||||
alignItems="center"
|
||||
paddingTop={1}
|
||||
>
|
||||
<text attributes={TextAttributes.BOLD} fg={Theme.primary}>
|
||||
{store.mode === "normal" ? ">" : "!"}
|
||||
</text>
|
||||
</box>
|
||||
<box paddingTop={1} paddingBottom={1} backgroundColor={Theme.backgroundElement} flexGrow={1}>
|
||||
<box
|
||||
paddingTop={1}
|
||||
paddingBottom={1}
|
||||
backgroundColor={Theme.backgroundElement}
|
||||
flexGrow={1}
|
||||
>
|
||||
<textarea
|
||||
placeholder={
|
||||
props.showPlaceholder
|
||||
|
|
@ -523,7 +538,10 @@ export function Prompt(props: PromptProps) {
|
|||
return
|
||||
}
|
||||
if (store.mode === "shell") {
|
||||
if ((e.name === "backspace" && input.visualCursor.offset === 0) || e.name === "escape") {
|
||||
if (
|
||||
(e.name === "backspace" && input.visualCursor.offset === 0) ||
|
||||
e.name === "escape"
|
||||
) {
|
||||
setStore("mode", "normal")
|
||||
e.preventDefault()
|
||||
return
|
||||
|
|
@ -554,7 +572,7 @@ export function Prompt(props: PromptProps) {
|
|||
input.cursorOffset = input.plainText.length
|
||||
}
|
||||
if (!autocomplete.visible) {
|
||||
if (e.name === "escape" && props.sessionID) {
|
||||
if (keybind.match("session_interrupt", e) && props.sessionID) {
|
||||
sdk.client.session.abort({
|
||||
path: {
|
||||
id: props.sessionID,
|
||||
|
|
@ -644,7 +662,12 @@ export function Prompt(props: PromptProps) {
|
|||
syntaxStyle={syntaxTheme}
|
||||
/>
|
||||
</box>
|
||||
<box backgroundColor={Theme.backgroundElement} width={1} justifyContent="center" alignItems="center"></box>
|
||||
<box
|
||||
backgroundColor={Theme.backgroundElement}
|
||||
width={1}
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
></box>
|
||||
</box>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
<text flexShrink={0} wrapMode="none">
|
||||
|
|
|
|||
|
|
@ -14,8 +14,7 @@ export const { use: useKeybind, provider: KeybindProvider } = createSimpleContex
|
|||
const sync = useSync()
|
||||
const keybinds = createMemo(() => {
|
||||
return pipe(
|
||||
DEFAULT_KEYBINDS,
|
||||
(val) => Object.assign(val, sync.data.config.keybinds),
|
||||
sync.data.config.keybinds ?? {},
|
||||
mapValues((value) => Keybind.parse(value)),
|
||||
)
|
||||
})
|
||||
|
|
@ -102,46 +101,3 @@ export const { use: useKeybind, provider: KeybindProvider } = createSimpleContex
|
|||
return result
|
||||
},
|
||||
})
|
||||
|
||||
const DEFAULT_KEYBINDS: KeybindsConfig = {
|
||||
leader: "ctrl+x",
|
||||
app_help: "<leader>h",
|
||||
app_exit: "ctrl+c,<leader>q",
|
||||
status_view: "<leader>s",
|
||||
editor_open: "<leader>e",
|
||||
theme_list: "<leader>t",
|
||||
project_init: "<leader>i",
|
||||
tool_details: "<leader>d",
|
||||
thinking_blocks: "<leader>b",
|
||||
sidebar_toggle: "<leader>b",
|
||||
session_export: "<leader>x",
|
||||
session_new: "<leader>n",
|
||||
session_list: "<leader>l",
|
||||
session_share: "none",
|
||||
session_unshare: "none",
|
||||
session_interrupt: "esc",
|
||||
session_compact: "<leader>c",
|
||||
session_child_cycle: "ctrl+right",
|
||||
session_child_cycle_reverse: "ctrl+left",
|
||||
session_timeline: "<leader>t",
|
||||
messages_page_up: "pageup",
|
||||
messages_page_down: "pagedown",
|
||||
messages_half_page_up: "ctrl+alt+u",
|
||||
messages_half_page_down: "ctrl+alt+d",
|
||||
messages_first: "home",
|
||||
messages_last: "end",
|
||||
messages_copy: "<leader>y",
|
||||
messages_undo: "<leader>u",
|
||||
messages_redo: "<leader>r",
|
||||
model_list: "<leader>m",
|
||||
command_list: "ctrl+p",
|
||||
model_cycle_recent: "f2",
|
||||
model_cycle_recent_reverse: "shift+f2",
|
||||
agent_list: "<leader>a",
|
||||
agent_cycle: "tab",
|
||||
agent_cycle_reverse: "shift+tab",
|
||||
input_clear: "ctrl+c",
|
||||
input_paste: "ctrl+v",
|
||||
input_submit: "enter",
|
||||
input_newline: "ctrl+j,shift+enter",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,14 @@ import { SplitBorder } from "@tui/component/border"
|
|||
import { syntaxTheme, Theme } from "@tui/context/theme"
|
||||
import { BoxRenderable, ScrollBoxRenderable, addDefaultParsers } from "@opentui/core"
|
||||
import { Prompt, type PromptRef } from "@tui/component/prompt"
|
||||
import type { AssistantMessage, Part, ToolPart, UserMessage, TextPart, ReasoningPart } from "@opencode-ai/sdk"
|
||||
import type {
|
||||
AssistantMessage,
|
||||
Part,
|
||||
ToolPart,
|
||||
UserMessage,
|
||||
TextPart,
|
||||
ReasoningPart,
|
||||
} from "@opencode-ai/sdk"
|
||||
import { useLocal } from "@tui/context/local"
|
||||
import { Locale } from "@/util/locale"
|
||||
import type { Tool } from "@/tool/tool"
|
||||
|
|
@ -177,19 +184,18 @@ export function Session() {
|
|||
disabled: !!session()?.share?.url,
|
||||
category: "Session",
|
||||
onSelect: async (dialog) => {
|
||||
await sdk.client.session.share({
|
||||
path: {
|
||||
id: route.sessionID,
|
||||
},
|
||||
})
|
||||
await sdk.client.session
|
||||
.share({
|
||||
path: {
|
||||
id: route.sessionID,
|
||||
},
|
||||
})
|
||||
.then((res) =>
|
||||
Clipboard.copy(res.data!.share!.url).catch(() =>
|
||||
toast.show({ message: "Failed to copy URL to clipboard", type: "error" })
|
||||
)
|
||||
)
|
||||
.then(() =>
|
||||
toast.show({ message: "Share URL copied to clipboard!", type: "success" })
|
||||
toast.show({ message: "Failed to copy URL to clipboard", type: "error" }),
|
||||
),
|
||||
)
|
||||
.then(() => toast.show({ message: "Share URL copied to clipboard!", type: "success" }))
|
||||
.catch(() => toast.show({ message: "Failed to share session", type: "error" }))
|
||||
dialog.clear()
|
||||
},
|
||||
|
|
@ -399,7 +405,14 @@ export function Session() {
|
|||
},
|
||||
}}
|
||||
>
|
||||
<box flexDirection="row" paddingBottom={1} paddingTop={1} paddingLeft={2} paddingRight={2} gap={2}>
|
||||
<box
|
||||
flexDirection="row"
|
||||
paddingBottom={1}
|
||||
paddingTop={1}
|
||||
paddingLeft={2}
|
||||
paddingRight={2}
|
||||
gap={2}
|
||||
>
|
||||
<box flexGrow={1} gap={1}>
|
||||
<Show when={session()}>
|
||||
<Show when={!sidebarVisible()}>
|
||||
|
|
@ -447,12 +460,18 @@ export function Session() {
|
|||
paddingTop={1}
|
||||
paddingBottom={1}
|
||||
paddingLeft={2}
|
||||
backgroundColor={hover() ? Theme.backgroundElement : Theme.backgroundPanel}
|
||||
backgroundColor={
|
||||
hover() ? Theme.backgroundElement : Theme.backgroundPanel
|
||||
}
|
||||
>
|
||||
<text fg={Theme.textMuted}>{revert()!.reverted.length} message reverted</text>
|
||||
<text fg={Theme.textMuted}>
|
||||
<span style={{ fg: Theme.text }}>{keybind.print("messages_redo")}</span> or /redo to
|
||||
restore
|
||||
{revert()!.reverted.length} message reverted
|
||||
</text>
|
||||
<text fg={Theme.textMuted}>
|
||||
<span style={{ fg: Theme.text }}>
|
||||
{keybind.print("messages_redo")}
|
||||
</span>{" "}
|
||||
or /redo to restore
|
||||
</text>
|
||||
<Show when={revert()!.diffFiles?.length}>
|
||||
<box marginTop={1}>
|
||||
|
|
@ -461,10 +480,16 @@ export function Session() {
|
|||
<text>
|
||||
{file.filename}
|
||||
<Show when={file.additions > 0}>
|
||||
<span style={{ fg: Theme.diffAdded }}> +{file.additions}</span>
|
||||
<span style={{ fg: Theme.diffAdded }}>
|
||||
{" "}
|
||||
+{file.additions}
|
||||
</span>
|
||||
</Show>
|
||||
<Show when={file.deletions > 0}>
|
||||
<span style={{ fg: Theme.diffRemoved }}> -{file.deletions}</span>
|
||||
<span style={{ fg: Theme.diffRemoved }}>
|
||||
{" "}
|
||||
-{file.deletions}
|
||||
</span>
|
||||
</Show>
|
||||
</text>
|
||||
)}
|
||||
|
|
@ -483,7 +508,9 @@ export function Session() {
|
|||
<UserMessage
|
||||
index={index()}
|
||||
onMouseUp={() =>
|
||||
dialog.replace(() => <DialogMessage messageID={message.id} sessionID={route.sessionID} />)
|
||||
dialog.replace(() => (
|
||||
<DialogMessage messageID={message.id} sessionID={route.sessionID} />
|
||||
))
|
||||
}
|
||||
message={message as UserMessage}
|
||||
parts={sync.data.part[message.id] ?? []}
|
||||
|
|
@ -539,7 +566,9 @@ function UserMessage(props: {
|
|||
index: number
|
||||
pending?: string
|
||||
}) {
|
||||
const text = createMemo(() => props.parts.flatMap((x) => (x.type === "text" && !x.synthetic ? [x] : []))[0])
|
||||
const text = createMemo(
|
||||
() => props.parts.flatMap((x) => (x.type === "text" && !x.synthetic ? [x] : []))[0],
|
||||
)
|
||||
const files = createMemo(() => props.parts.flatMap((x) => (x.type === "file" ? [x] : [])))
|
||||
const sync = useSync()
|
||||
const [hover, setHover] = createSignal(false)
|
||||
|
|
@ -579,8 +608,14 @@ function UserMessage(props: {
|
|||
})
|
||||
return (
|
||||
<text>
|
||||
<span style={{ bg: bg(), fg: Theme.background }}> {MIME_BADGE[file.mime] ?? file.mime} </span>
|
||||
<span style={{ bg: Theme.backgroundElement, fg: Theme.textMuted }}> {file.filename} </span>
|
||||
<span style={{ bg: bg(), fg: Theme.background }}>
|
||||
{" "}
|
||||
{MIME_BADGE[file.mime] ?? file.mime}{" "}
|
||||
</span>
|
||||
<span style={{ bg: Theme.backgroundElement, fg: Theme.textMuted }}>
|
||||
{" "}
|
||||
{file.filename}{" "}
|
||||
</span>
|
||||
</text>
|
||||
)
|
||||
}}
|
||||
|
|
@ -591,9 +626,16 @@ function UserMessage(props: {
|
|||
{sync.data.config.username ?? "You"}{" "}
|
||||
<Show
|
||||
when={queued()}
|
||||
fallback={<span style={{ fg: Theme.textMuted }}>({Locale.time(props.message.time.created)})</span>}
|
||||
fallback={
|
||||
<span style={{ fg: Theme.textMuted }}>
|
||||
({Locale.time(props.message.time.created)})
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<span style={{ bg: Theme.accent, fg: Theme.backgroundPanel, bold: true }}> QUEUED </span>
|
||||
<span style={{ bg: Theme.accent, fg: Theme.backgroundPanel, bold: true }}>
|
||||
{" "}
|
||||
QUEUED{" "}
|
||||
</span>
|
||||
</Show>
|
||||
</text>
|
||||
</box>
|
||||
|
|
@ -632,7 +674,8 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
|
|||
<Show
|
||||
when={
|
||||
!props.message.time.completed ||
|
||||
(props.last && props.parts.some((item) => item.type === "step-finish" && item.reason === "tool-calls"))
|
||||
(props.last &&
|
||||
props.parts.some((item) => item.type === "step-finish" && item.reason === "tool-calls"))
|
||||
}
|
||||
>
|
||||
<box
|
||||
|
|
@ -644,7 +687,9 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
|
|||
customBorderChars={SplitBorder.customBorderChars}
|
||||
borderColor={Theme.backgroundElement}
|
||||
>
|
||||
<text fg={local.agent.color(props.message.mode)}>{Locale.titlecase(props.message.mode)}</text>
|
||||
<text fg={local.agent.color(props.message.mode)}>
|
||||
{Locale.titlecase(props.message.mode)}
|
||||
</text>
|
||||
<Shimmer text={`${props.message.modelID}`} color={Theme.text} />
|
||||
</box>
|
||||
</Show>
|
||||
|
|
@ -656,7 +701,9 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
|
|||
>
|
||||
<box paddingLeft={3}>
|
||||
<text marginTop={1}>
|
||||
<span style={{ fg: local.agent.color(props.message.mode) }}>{Locale.titlecase(props.message.mode)}</span>{" "}
|
||||
<span style={{ fg: local.agent.color(props.message.mode) }}>
|
||||
{Locale.titlecase(props.message.mode)}
|
||||
</span>{" "}
|
||||
<span style={{ fg: Theme.textMuted }}>{props.message.modelID}</span>
|
||||
</text>
|
||||
</box>
|
||||
|
|
@ -682,7 +729,12 @@ function ReasoningPart(props: { part: ReasoningPart; message: AssistantMessage }
|
|||
customBorderChars={SplitBorder.customBorderChars}
|
||||
borderColor={Theme.backgroundPanel}
|
||||
>
|
||||
<box paddingTop={1} paddingBottom={1} paddingLeft={2} backgroundColor={Theme.backgroundPanel}>
|
||||
<box
|
||||
paddingTop={1}
|
||||
paddingBottom={1}
|
||||
paddingLeft={2}
|
||||
backgroundColor={Theme.backgroundPanel}
|
||||
>
|
||||
<text>{props.part.text.trim()}</text>
|
||||
</box>
|
||||
</box>
|
||||
|
|
@ -814,7 +866,10 @@ function GenericTool(props: ToolProps<any>) {
|
|||
}
|
||||
|
||||
const ToolRegistry = (() => {
|
||||
const state: Record<string, { name: string; container: "inline" | "block"; render?: Component<ToolProps<any>> }> = {}
|
||||
const state: Record<
|
||||
string,
|
||||
{ name: string; container: "inline" | "block"; render?: Component<ToolProps<any>> }
|
||||
> = {}
|
||||
function register<T extends Tool.Info>(input: {
|
||||
name: string
|
||||
container: "inline" | "block"
|
||||
|
|
@ -908,10 +963,16 @@ ToolRegistry.register<typeof WriteTool>({
|
|||
</ToolTitle>
|
||||
<box flexDirection="row">
|
||||
<box flexShrink={0}>
|
||||
<For each={numbers()}>{(value) => <text style={{ fg: Theme.textMuted }}>{value}</text>}</For>
|
||||
<For each={numbers()}>
|
||||
{(value) => <text style={{ fg: Theme.textMuted }}>{value}</text>}
|
||||
</For>
|
||||
</box>
|
||||
<box paddingLeft={1} flexGrow={1}>
|
||||
<code filetype={filetype(props.input.filePath!)} syntaxStyle={syntaxTheme} content={code()} />
|
||||
<code
|
||||
filetype={filetype(props.input.filePath!)}
|
||||
syntaxStyle={syntaxTheme}
|
||||
content={code()}
|
||||
/>
|
||||
</box>
|
||||
</box>
|
||||
</>
|
||||
|
|
@ -926,7 +987,8 @@ ToolRegistry.register<typeof GlobTool>({
|
|||
return (
|
||||
<>
|
||||
<ToolTitle icon="✱" fallback="Finding files..." when={props.input.pattern}>
|
||||
Glob "{props.input.pattern}" <Show when={props.input.path}>in {normalizePath(props.input.path)} </Show>
|
||||
Glob "{props.input.pattern}"{" "}
|
||||
<Show when={props.input.path}>in {normalizePath(props.input.path)} </Show>
|
||||
<Show when={props.metadata.count}>({props.metadata.count} matches)</Show>
|
||||
</ToolTitle>
|
||||
</>
|
||||
|
|
@ -940,7 +1002,8 @@ ToolRegistry.register<typeof GrepTool>({
|
|||
render(props) {
|
||||
return (
|
||||
<ToolTitle icon="✱" fallback="Searching content..." when={props.input.pattern}>
|
||||
Grep "{props.input.pattern}" <Show when={props.input.path}>in {normalizePath(props.input.path)} </Show>
|
||||
Grep "{props.input.pattern}"{" "}
|
||||
<Show when={props.input.path}>in {normalizePath(props.input.path)} </Show>
|
||||
<Show when={props.metadata.matches}>({props.metadata.matches} matches)</Show>
|
||||
</ToolTitle>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,11 @@ import { NamedError } from "../util/error"
|
|||
import matter from "gray-matter"
|
||||
import { Flag } from "../flag/flag"
|
||||
import { Auth } from "../auth"
|
||||
import { type ParseError as JsoncParseError, parse as parseJsonc, printParseErrorCode } from "jsonc-parser"
|
||||
import {
|
||||
type ParseError as JsoncParseError,
|
||||
parse as parseJsonc,
|
||||
printParseErrorCode,
|
||||
} from "jsonc-parser"
|
||||
import { Instance } from "../project/instance"
|
||||
import { LSPServer } from "../lsp/server"
|
||||
import { BunProc } from "@/bun"
|
||||
|
|
@ -46,7 +50,10 @@ export namespace Config {
|
|||
if (value.type === "wellknown") {
|
||||
process.env[value.key] = value.token
|
||||
const wellknown = (await fetch(`${key}/.well-known/opencode`).then((x) => x.json())) as any
|
||||
result = mergeDeep(result, await load(JSON.stringify(wellknown.config ?? {}), process.cwd()))
|
||||
result = mergeDeep(
|
||||
result,
|
||||
await load(JSON.stringify(wellknown.config ?? {}), process.cwd()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -57,7 +64,11 @@ export namespace Config {
|
|||
const directories = [
|
||||
Global.Path.config,
|
||||
...(await Array.fromAsync(
|
||||
Filesystem.up({ targets: [".opencode"], start: Instance.directory, stop: Instance.worktree }),
|
||||
Filesystem.up({
|
||||
targets: [".opencode"],
|
||||
start: Instance.directory,
|
||||
stop: Instance.worktree,
|
||||
}),
|
||||
)),
|
||||
]
|
||||
|
||||
|
|
@ -95,29 +106,13 @@ export namespace Config {
|
|||
if (result.autoshare === true && !result.share) {
|
||||
result.share = "auto"
|
||||
}
|
||||
if (result.keybinds?.messages_revert && !result.keybinds.messages_undo) {
|
||||
result.keybinds.messages_undo = result.keybinds.messages_revert
|
||||
}
|
||||
|
||||
// Handle migration from autoshare to share field
|
||||
if (result.autoshare === true && !result.share) {
|
||||
result.share = "auto"
|
||||
}
|
||||
if (result.keybinds?.messages_revert && !result.keybinds.messages_undo) {
|
||||
result.keybinds.messages_undo = result.keybinds.messages_revert
|
||||
}
|
||||
if (result.keybinds?.switch_mode && !result.keybinds.switch_agent) {
|
||||
result.keybinds.switch_agent = result.keybinds.switch_mode
|
||||
}
|
||||
if (result.keybinds?.switch_mode_reverse && !result.keybinds.switch_agent_reverse) {
|
||||
result.keybinds.switch_agent_reverse = result.keybinds.switch_mode_reverse
|
||||
}
|
||||
if (result.keybinds?.switch_agent && !result.keybinds.agent_cycle) {
|
||||
result.keybinds.agent_cycle = result.keybinds.switch_agent
|
||||
}
|
||||
if (result.keybinds?.switch_agent_reverse && !result.keybinds.agent_cycle_reverse) {
|
||||
result.keybinds.agent_cycle_reverse = result.keybinds.switch_agent_reverse
|
||||
}
|
||||
|
||||
if (!result.keybinds) result.keybinds = Info.shape.keybinds.parse({})
|
||||
|
||||
return {
|
||||
config: result,
|
||||
|
|
@ -153,10 +148,18 @@ export namespace Config {
|
|||
|
||||
const gitignore = path.join(dir, ".gitignore")
|
||||
const hasGitIgnore = await Bun.file(gitignore).exists()
|
||||
if (!hasGitIgnore) await Bun.write(gitignore, ["node_modules", "package.json", "bun.lock", ".gitignore"].join("\n"))
|
||||
if (!hasGitIgnore)
|
||||
await Bun.write(
|
||||
gitignore,
|
||||
["node_modules", "package.json", "bun.lock", ".gitignore"].join("\n"),
|
||||
)
|
||||
|
||||
await BunProc.run(
|
||||
["add", "@opencode-ai/plugin@" + (Installation.isLocal() ? "latest" : Installation.VERSION), "--exact"],
|
||||
[
|
||||
"add",
|
||||
"@opencode-ai/plugin@" + (Installation.isLocal() ? "latest" : Installation.VERSION),
|
||||
"--exact",
|
||||
],
|
||||
{
|
||||
cwd: dir,
|
||||
},
|
||||
|
|
@ -166,7 +169,12 @@ export namespace Config {
|
|||
const COMMAND_GLOB = new Bun.Glob("command/**/*.md")
|
||||
async function loadCommand(dir: string) {
|
||||
const result: Record<string, Command> = {}
|
||||
for await (const item of COMMAND_GLOB.scan({ absolute: true, followSymlinks: true, dot: true, cwd: dir })) {
|
||||
for await (const item of COMMAND_GLOB.scan({
|
||||
absolute: true,
|
||||
followSymlinks: true,
|
||||
dot: true,
|
||||
cwd: dir,
|
||||
})) {
|
||||
const content = await Bun.file(item).text()
|
||||
const md = matter(content)
|
||||
if (!md.data) continue
|
||||
|
|
@ -201,7 +209,12 @@ export namespace Config {
|
|||
async function loadAgent(dir: string) {
|
||||
const result: Record<string, Agent> = {}
|
||||
|
||||
for await (const item of AGENT_GLOB.scan({ absolute: true, followSymlinks: true, dot: true, cwd: dir })) {
|
||||
for await (const item of AGENT_GLOB.scan({
|
||||
absolute: true,
|
||||
followSymlinks: true,
|
||||
dot: true,
|
||||
cwd: dir,
|
||||
})) {
|
||||
const content = await Bun.file(item).text()
|
||||
const md = matter(content)
|
||||
if (!md.data) continue
|
||||
|
|
@ -239,7 +252,12 @@ export namespace Config {
|
|||
const MODE_GLOB = new Bun.Glob("mode/*.md")
|
||||
async function loadMode(dir: string) {
|
||||
const result: Record<string, Agent> = {}
|
||||
for await (const item of MODE_GLOB.scan({ absolute: true, followSymlinks: true, dot: true, cwd: dir })) {
|
||||
for await (const item of MODE_GLOB.scan({
|
||||
absolute: true,
|
||||
followSymlinks: true,
|
||||
dot: true,
|
||||
cwd: dir,
|
||||
})) {
|
||||
const content = await Bun.file(item).text()
|
||||
const md = matter(content)
|
||||
if (!md.data) continue
|
||||
|
|
@ -265,7 +283,12 @@ export namespace Config {
|
|||
async function loadPlugin(dir: string) {
|
||||
const plugins: string[] = []
|
||||
|
||||
for await (const item of PLUGIN_GLOB.scan({ absolute: true, followSymlinks: true, dot: true, cwd: dir })) {
|
||||
for await (const item of PLUGIN_GLOB.scan({
|
||||
absolute: true,
|
||||
followSymlinks: true,
|
||||
dot: true,
|
||||
cwd: dir,
|
||||
})) {
|
||||
plugins.push("file://" + item)
|
||||
}
|
||||
return plugins
|
||||
|
|
@ -291,7 +314,10 @@ export namespace Config {
|
|||
type: z.literal("remote").describe("Type of MCP server connection"),
|
||||
url: z.string().describe("URL of the remote MCP server"),
|
||||
enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"),
|
||||
headers: z.record(z.string(), z.string()).optional().describe("Headers to send with the request"),
|
||||
headers: z
|
||||
.record(z.string(), z.string())
|
||||
.optional()
|
||||
.describe("Headers to send with the request"),
|
||||
})
|
||||
.strict()
|
||||
.meta({
|
||||
|
|
@ -339,75 +365,78 @@ export namespace Config {
|
|||
|
||||
export const Keybinds = z
|
||||
.object({
|
||||
leader: z.string().optional().default("ctrl+x").describe("Leader key for keybind combinations"),
|
||||
app_help: z.string().optional().default("<leader>h").describe("Show help dialog"),
|
||||
leader: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("ctrl+x")
|
||||
.describe("Leader key for keybind combinations"),
|
||||
app_exit: z.string().optional().default("ctrl+c,<leader>q").describe("Exit the application"),
|
||||
editor_open: z.string().optional().default("<leader>e").describe("Open external editor"),
|
||||
theme_list: z.string().optional().default("<leader>t").describe("List available themes"),
|
||||
project_init: z.string().optional().default("<leader>i").describe("Create/update AGENTS.md"),
|
||||
tool_details: z.string().optional().default("<leader>d").describe("Toggle tool details"),
|
||||
thinking_blocks: z.string().optional().default("<leader>b").describe("Toggle thinking blocks"),
|
||||
sidebar_toggle: z.string().optional().default("<leader>b").describe("Toggle sidebar"),
|
||||
status_view: z.string().optional().default("<leader>s").describe("View status"),
|
||||
session_export: z.string().optional().default("<leader>x").describe("Export session to editor"),
|
||||
session_export: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("<leader>x")
|
||||
.describe("Export session to editor"),
|
||||
session_new: z.string().optional().default("<leader>n").describe("Create a new session"),
|
||||
session_list: z.string().optional().default("<leader>l").describe("List all sessions"),
|
||||
session_timeline: z.string().optional().default("<leader>g").describe("Show session timeline"),
|
||||
session_timeline: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("<leader>g")
|
||||
.describe("Show session timeline"),
|
||||
session_share: z.string().optional().default("none").describe("Share current session"),
|
||||
session_unshare: z.string().optional().default("none").describe("Unshare current session"),
|
||||
session_interrupt: z.string().optional().default("esc").describe("Interrupt current session"),
|
||||
session_compact: z.string().optional().default("<leader>c").describe("Compact the session"),
|
||||
session_child_cycle: z.string().optional().default("ctrl+right").describe("Cycle to next child session"),
|
||||
session_child_cycle_reverse: z
|
||||
messages_page_up: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("ctrl+left")
|
||||
.describe("Cycle to previous child session"),
|
||||
messages_page_up: z.string().optional().default("pgup").describe("Scroll messages up by one page"),
|
||||
messages_page_down: z.string().optional().default("pgdown").describe("Scroll messages down by one page"),
|
||||
messages_half_page_up: z.string().optional().default("ctrl+alt+u").describe("Scroll messages up by half page"),
|
||||
.default("pageup")
|
||||
.describe("Scroll messages up by one page"),
|
||||
messages_page_down: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("pagedown")
|
||||
.describe("Scroll messages down by one page"),
|
||||
messages_half_page_up: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("ctrl+alt+u")
|
||||
.describe("Scroll messages up by half page"),
|
||||
messages_half_page_down: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("ctrl+alt+d")
|
||||
.describe("Scroll messages down by half page"),
|
||||
messages_first: z.string().optional().default("ctrl+g").describe("Navigate to first message"),
|
||||
messages_last: z.string().optional().default("ctrl+alt+g").describe("Navigate to last message"),
|
||||
messages_first: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("ctrl+g,home")
|
||||
.describe("Navigate to first message"),
|
||||
messages_last: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("ctrl+alt+g,end")
|
||||
.describe("Navigate to last message"),
|
||||
messages_copy: z.string().optional().default("<leader>y").describe("Copy message"),
|
||||
messages_undo: z.string().optional().default("<leader>u").describe("Undo message"),
|
||||
messages_redo: z.string().optional().default("<leader>r").describe("Redo message"),
|
||||
model_list: z.string().optional().default("<leader>m").describe("List available models"),
|
||||
command_list: z.string().optional().default("ctrl+p").describe("List available commands"),
|
||||
model_cycle_recent: z.string().optional().default("f2").describe("Next recent model"),
|
||||
model_cycle_recent_reverse: z.string().optional().default("shift+f2").describe("Previous recent model"),
|
||||
agent_list: z.string().optional().default("<leader>a").describe("List agents"),
|
||||
agent_cycle: z.string().optional().default("tab").describe("Next agent"),
|
||||
agent_cycle_reverse: z.string().optional().default("shift+tab").describe("Previous agent"),
|
||||
input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"),
|
||||
input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"),
|
||||
input_submit: z.string().optional().default("enter").describe("Submit input"),
|
||||
input_newline: z.string().optional().default("shift+enter,ctrl+j").describe("Insert newline in input"),
|
||||
// Deprecated commands
|
||||
switch_mode: z.string().optional().default("none").describe("@deprecated use agent_cycle. Next mode"),
|
||||
switch_mode_reverse: z
|
||||
input_newline: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("none")
|
||||
.describe("@deprecated use agent_cycle_reverse. Previous mode"),
|
||||
switch_agent: z.string().optional().default("tab").describe("@deprecated use agent_cycle. Next agent"),
|
||||
switch_agent_reverse: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("shift+tab")
|
||||
.describe("@deprecated use agent_cycle_reverse. Previous agent"),
|
||||
file_list: z.string().optional().default("none").describe("@deprecated Currently not available. List files"),
|
||||
file_close: z.string().optional().default("none").describe("@deprecated Close file"),
|
||||
file_search: z.string().optional().default("none").describe("@deprecated Search file"),
|
||||
file_diff_toggle: z.string().optional().default("none").describe("@deprecated Split/unified diff"),
|
||||
messages_previous: z.string().optional().default("none").describe("@deprecated Navigate to previous message"),
|
||||
messages_next: z.string().optional().default("none").describe("@deprecated Navigate to next message"),
|
||||
messages_layout_toggle: z.string().optional().default("none").describe("@deprecated Toggle layout"),
|
||||
messages_revert: z.string().optional().default("none").describe("@deprecated use messages_undo. Revert message"),
|
||||
.default("shift+enter,ctrl+j")
|
||||
.describe("Insert newline in input"),
|
||||
})
|
||||
.strict()
|
||||
.meta({
|
||||
|
|
@ -449,13 +478,23 @@ export namespace Config {
|
|||
autoshare: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("@deprecated Use 'share' field instead. Share newly created sessions automatically"),
|
||||
.describe(
|
||||
"@deprecated Use 'share' field instead. Share newly created sessions automatically",
|
||||
),
|
||||
autoupdate: z.boolean().optional().describe("Automatically update to the latest version"),
|
||||
disabled_providers: z.array(z.string()).optional().describe("Disable providers that are loaded automatically"),
|
||||
model: z.string().describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(),
|
||||
disabled_providers: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe("Disable providers that are loaded automatically"),
|
||||
model: z
|
||||
.string()
|
||||
.describe("Model to use in the format of provider/model, eg anthropic/claude-2")
|
||||
.optional(),
|
||||
small_model: z
|
||||
.string()
|
||||
.describe("Small model to use for tasks like title generation in the format of provider/model")
|
||||
.describe(
|
||||
"Small model to use for tasks like title generation in the format of provider/model",
|
||||
)
|
||||
.optional(),
|
||||
username: z
|
||||
.string()
|
||||
|
|
@ -511,7 +550,10 @@ export namespace Config {
|
|||
)
|
||||
.optional()
|
||||
.describe("Custom provider configurations and model overrides"),
|
||||
mcp: z.record(z.string(), Mcp).optional().describe("MCP (Model Context Protocol) server configurations"),
|
||||
mcp: z
|
||||
.record(z.string(), Mcp)
|
||||
.optional()
|
||||
.describe("MCP (Model Context Protocol) server configurations"),
|
||||
formatter: z
|
||||
.record(
|
||||
z.string(),
|
||||
|
|
@ -555,7 +597,10 @@ export namespace Config {
|
|||
error: "For custom LSP servers, 'extensions' array is required.",
|
||||
},
|
||||
),
|
||||
instructions: z.array(z.string()).optional().describe("Additional instruction files or patterns to include"),
|
||||
instructions: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe("Additional instruction files or patterns to include"),
|
||||
layout: Layout.optional().describe("@deprecated Always uses stretch layout."),
|
||||
permission: z
|
||||
.object({
|
||||
|
|
@ -589,7 +634,10 @@ export namespace Config {
|
|||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
chatMaxRetries: z.number().optional().describe("Number of retries for chat completions on failure"),
|
||||
chatMaxRetries: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe("Number of retries for chat completions on failure"),
|
||||
disable_paste_summary: z.boolean().optional(),
|
||||
})
|
||||
.optional(),
|
||||
|
|
@ -619,7 +667,10 @@ export namespace Config {
|
|||
if (provider && model) result.model = `${provider}/${model}`
|
||||
result["$schema"] = "https://opencode.ai/config.json"
|
||||
result = mergeDeep(result, rest)
|
||||
await Bun.write(path.join(Global.Path.config, "config.json"), JSON.stringify(result, null, 2))
|
||||
await Bun.write(
|
||||
path.join(Global.Path.config, "config.json"),
|
||||
JSON.stringify(result, null, 2),
|
||||
)
|
||||
await fs.unlink(path.join(Global.Path.config, "config"))
|
||||
})
|
||||
.catch(() => {})
|
||||
|
|
@ -658,7 +709,9 @@ export namespace Config {
|
|||
if (filePath.startsWith("~/")) {
|
||||
filePath = path.join(os.homedir(), filePath.slice(2))
|
||||
}
|
||||
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(configDir, filePath)
|
||||
const resolvedPath = path.isAbsolute(filePath)
|
||||
? filePath
|
||||
: path.resolve(configDir, filePath)
|
||||
const fileContent = (
|
||||
await Bun.file(resolvedPath)
|
||||
.text()
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ export namespace Keybind {
|
|||
info.ctrl = true
|
||||
break
|
||||
case "alt":
|
||||
case "meta":
|
||||
case "option":
|
||||
info.meta = true
|
||||
break
|
||||
|
|
|
|||
|
|
@ -130,6 +130,10 @@ import type {
|
|||
TuiPublishData,
|
||||
TuiPublishResponses,
|
||||
TuiPublishErrors,
|
||||
TuiControlNextData,
|
||||
TuiControlNextResponses,
|
||||
TuiControlResponseData,
|
||||
TuiControlResponseResponses,
|
||||
AuthSetData,
|
||||
AuthSetResponses,
|
||||
AuthSetErrors,
|
||||
|
|
@ -759,7 +763,9 @@ class Lsp extends _HeyApiClient {
|
|||
/**
|
||||
* Get LSP server status
|
||||
*/
|
||||
public status<ThrowOnError extends boolean = false>(options?: Options<LspStatusData, ThrowOnError>) {
|
||||
public status<ThrowOnError extends boolean = false>(
|
||||
options?: Options<LspStatusData, ThrowOnError>,
|
||||
) {
|
||||
return (options?.client ?? this._client).get<LspStatusResponses, unknown, ThrowOnError>({
|
||||
url: "/lsp",
|
||||
...options,
|
||||
|
|
@ -767,6 +773,40 @@ class Lsp extends _HeyApiClient {
|
|||
}
|
||||
}
|
||||
|
||||
class Control extends _HeyApiClient {
|
||||
/**
|
||||
* Get the next TUI request from the queue
|
||||
*/
|
||||
public next<ThrowOnError extends boolean = false>(
|
||||
options?: Options<TuiControlNextData, ThrowOnError>,
|
||||
) {
|
||||
return (options?.client ?? this._client).get<TuiControlNextResponses, unknown, ThrowOnError>({
|
||||
url: "/tui/control/next",
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit a response to the TUI request queue
|
||||
*/
|
||||
public response<ThrowOnError extends boolean = false>(
|
||||
options?: Options<TuiControlResponseData, ThrowOnError>,
|
||||
) {
|
||||
return (options?.client ?? this._client).post<
|
||||
TuiControlResponseResponses,
|
||||
unknown,
|
||||
ThrowOnError
|
||||
>({
|
||||
url: "/tui/control/response",
|
||||
...options,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...options?.headers,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class Tui extends _HeyApiClient {
|
||||
/**
|
||||
* Append prompt to the TUI
|
||||
|
|
@ -899,8 +939,14 @@ class Tui extends _HeyApiClient {
|
|||
/**
|
||||
* Publish a TUI event
|
||||
*/
|
||||
public publish<ThrowOnError extends boolean = false>(options?: Options<TuiPublishData, ThrowOnError>) {
|
||||
return (options?.client ?? this._client).post<TuiPublishResponses, TuiPublishErrors, ThrowOnError>({
|
||||
public publish<ThrowOnError extends boolean = false>(
|
||||
options?: Options<TuiPublishData, ThrowOnError>,
|
||||
) {
|
||||
return (options?.client ?? this._client).post<
|
||||
TuiPublishResponses,
|
||||
TuiPublishErrors,
|
||||
ThrowOnError
|
||||
>({
|
||||
url: "/tui/publish",
|
||||
...options,
|
||||
headers: {
|
||||
|
|
@ -909,6 +955,7 @@ class Tui extends _HeyApiClient {
|
|||
},
|
||||
})
|
||||
}
|
||||
control = new Control({ client: this._client })
|
||||
}
|
||||
|
||||
class Auth extends _HeyApiClient {
|
||||
|
|
|
|||
|
|
@ -2764,6 +2764,46 @@ export type TuiPublishResponses = {
|
|||
|
||||
export type TuiPublishResponse = TuiPublishResponses[keyof TuiPublishResponses]
|
||||
|
||||
export type TuiControlNextData = {
|
||||
body?: never
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
}
|
||||
url: "/tui/control/next"
|
||||
}
|
||||
|
||||
export type TuiControlNextResponses = {
|
||||
/**
|
||||
* Next TUI request
|
||||
*/
|
||||
200: {
|
||||
path: string
|
||||
body: unknown
|
||||
}
|
||||
}
|
||||
|
||||
export type TuiControlNextResponse = TuiControlNextResponses[keyof TuiControlNextResponses]
|
||||
|
||||
export type TuiControlResponseData = {
|
||||
body?: unknown
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
}
|
||||
url: "/tui/control/response"
|
||||
}
|
||||
|
||||
export type TuiControlResponseResponses = {
|
||||
/**
|
||||
* Response submitted successfully
|
||||
*/
|
||||
200: boolean
|
||||
}
|
||||
|
||||
export type TuiControlResponseResponse =
|
||||
TuiControlResponseResponses[keyof TuiControlResponseResponses]
|
||||
|
||||
export type AuthSetData = {
|
||||
body?: Auth
|
||||
path: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue