refactor(core): replace bash tool with shell tool
This commit is contained in:
parent
595c6bd4a7
commit
5ae93092aa
37 changed files with 2260 additions and 901 deletions
|
|
@ -164,6 +164,9 @@ export function Prompt(props: PromptProps) {
|
|||
.filter((session) => session.parentID === props.sessionID && data.session.status(session.id) === "running")
|
||||
.length,
|
||||
)
|
||||
const runningShells = createMemo(
|
||||
() => data.shell.list().filter((shell) => shell.metadata.sessionID === props.sessionID).length,
|
||||
)
|
||||
const history = usePromptHistory()
|
||||
const stash = usePromptStash()
|
||||
const keymap = useOpencodeKeymap()
|
||||
|
|
@ -284,6 +287,20 @@ export function Prompt(props: PromptProps) {
|
|||
}
|
||||
})
|
||||
|
||||
// Far-right footer cluster: live work counts lead, then context/cost usage, all dot-joined.
|
||||
// When empty, the cluster falls back to the hotkey hints.
|
||||
const statusItems = createMemo(() => {
|
||||
const agents = activeSubagents()
|
||||
const shells = runningShells()
|
||||
const stats = usage()
|
||||
return [
|
||||
agents ? `${agents} subagent${agents === 1 ? "" : "s"}` : undefined,
|
||||
shells ? `${shells} shell${shells === 1 ? "" : "s"}` : undefined,
|
||||
stats?.context,
|
||||
stats?.cost,
|
||||
].filter(Boolean)
|
||||
})
|
||||
|
||||
const [store, setStore] = createStore<{
|
||||
prompt: PromptInfo
|
||||
mode: "normal" | "shell"
|
||||
|
|
@ -1548,13 +1565,6 @@ export function Prompt(props: PromptProps) {
|
|||
<spinner color={spinnerDef().color} frames={spinnerDef().frames} interval={40} />
|
||||
</Show>
|
||||
</box>
|
||||
<Show when={activeSubagents()}>
|
||||
{(count) => (
|
||||
<Spinner color={theme.textMuted}>
|
||||
{count()} active subagent{count() === 1 ? "" : "s"}
|
||||
</Spinner>
|
||||
)}
|
||||
</Show>
|
||||
<text fg={store.interrupt > 0 ? theme.primary : theme.text}>
|
||||
esc{" "}
|
||||
<span style={{ fg: store.interrupt > 0 ? theme.primary : theme.textMuted }}>
|
||||
|
|
@ -1624,22 +1634,20 @@ export function Prompt(props: PromptProps) {
|
|||
<Switch>
|
||||
<Match when={store.mode === "normal"}>
|
||||
<Switch>
|
||||
<Match when={usage()}>
|
||||
{(item) => (
|
||||
<text fg={theme.textMuted} wrapMode="none">
|
||||
{[item().context, item().cost].filter(Boolean).join(" · ")}
|
||||
</text>
|
||||
)}
|
||||
<Match when={statusItems().length > 0}>
|
||||
<text fg={theme.textMuted} wrapMode="none">
|
||||
{statusItems().join(" · ")}
|
||||
</text>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<text fg={theme.text}>
|
||||
{agentShortcut()} <span style={{ fg: theme.textMuted }}>agents</span>
|
||||
</text>
|
||||
<text fg={theme.text}>
|
||||
{paletteShortcut()} <span style={{ fg: theme.textMuted }}>commands</span>
|
||||
</text>
|
||||
</Match>
|
||||
</Switch>
|
||||
<text fg={theme.text}>
|
||||
{paletteShortcut()} <span style={{ fg: theme.textMuted }}>commands</span>
|
||||
</text>
|
||||
</Match>
|
||||
<Match when={store.mode === "shell"}>
|
||||
<text fg={theme.text}>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import type {
|
|||
SessionMessageAssistantText,
|
||||
SessionMessageAssistantTool,
|
||||
SessionV2Info,
|
||||
Shell,
|
||||
SkillV2Info,
|
||||
V2Event,
|
||||
} from "@opencode-ai/sdk/v2"
|
||||
|
|
@ -47,6 +48,9 @@ type Data = {
|
|||
permission: Record<string, PermissionSavedInfo[]>
|
||||
}
|
||||
location: Record<string, LocationData>
|
||||
// Currently running shell commands, keyed by shell id. Entries are removed once the command
|
||||
// exits or is deleted, so this only ever holds in-flight shells.
|
||||
shell: Record<string, Shell>
|
||||
}
|
||||
|
||||
function locationKey(location: LocationRef) {
|
||||
|
|
@ -72,6 +76,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
permission: {},
|
||||
},
|
||||
location: {},
|
||||
shell: {},
|
||||
})
|
||||
|
||||
const sdk = useSDK()
|
||||
|
|
@ -467,6 +472,18 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
),
|
||||
)
|
||||
break
|
||||
case "shell.created":
|
||||
setStore("shell", event.data.info.id, event.data.info)
|
||||
break
|
||||
case "shell.exited":
|
||||
case "shell.deleted":
|
||||
setStore(
|
||||
"shell",
|
||||
produce((draft) => {
|
||||
delete draft[event.data.id]
|
||||
}),
|
||||
)
|
||||
break
|
||||
case "reference.updated":
|
||||
void result.location.reference.refresh()
|
||||
break
|
||||
|
|
@ -567,6 +584,23 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
},
|
||||
},
|
||||
},
|
||||
shell: {
|
||||
list() {
|
||||
return Object.values(store.shell)
|
||||
},
|
||||
get(id: string) {
|
||||
return store.shell[id]
|
||||
},
|
||||
async refresh(ref?: LocationRef) {
|
||||
const result = await sdk.client.v2.shell.list({ location: locationQuery(ref) }, { throwOnError: true })
|
||||
setStore(
|
||||
"shell",
|
||||
produce((draft) => {
|
||||
for (const info of result.data.data) draft[info.id] = info
|
||||
}),
|
||||
)
|
||||
},
|
||||
},
|
||||
location: {
|
||||
default() {
|
||||
return defaultLocation()
|
||||
|
|
@ -682,6 +716,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
result.location.reference.refresh(),
|
||||
result.location.command.refresh(),
|
||||
result.location.skill.refresh(),
|
||||
result.shell.refresh(),
|
||||
])
|
||||
for (const failure of settled.filter((item) => item.status === "rejected"))
|
||||
console.error("Failed to refresh default location data", failure.reason)
|
||||
|
|
|
|||
|
|
@ -221,8 +221,8 @@ const TIPS: Tip[] = [
|
|||
"Use {highlight}$ARGUMENTS{/highlight}, {highlight}$1{/highlight}, {highlight}$2{/highlight} in custom commands for dynamic input",
|
||||
"Use backticks in commands to inject shell output (e.g., {highlight}`git status`{/highlight})",
|
||||
"Add {highlight}.md{/highlight} files to {highlight}.opencode/agents/{/highlight} for specialized AI personas",
|
||||
"Configure per-agent permissions for {highlight}edit{/highlight}, {highlight}bash{/highlight}, and {highlight}webfetch{/highlight} tools",
|
||||
'Use patterns like {highlight}"git *": "allow"{/highlight} for granular bash permissions',
|
||||
"Configure per-agent permissions for {highlight}edit{/highlight}, {highlight}shell{/highlight}, and {highlight}webfetch{/highlight} tools",
|
||||
'Use patterns like {highlight}"git *": "allow"{/highlight} for granular shell permissions',
|
||||
'Set {highlight}"rm -rf *": "deny"{/highlight} to block destructive commands',
|
||||
'Configure {highlight}"git push": "ask"{/highlight} to require approval before pushing',
|
||||
'Set {highlight}"formatter": true{/highlight} in config to enable built-in formatters like prettier, gofmt, and ruff',
|
||||
|
|
@ -256,7 +256,7 @@ const TIPS: Tip[] = [
|
|||
"Use {highlight}instructions{/highlight} in config to load additional rules files",
|
||||
"Set agent {highlight}temperature{/highlight} from 0.0 (focused) to 1.0 (creative)",
|
||||
"Configure {highlight}steps{/highlight} to limit agentic iterations per request",
|
||||
'Set {highlight}"tools": {"bash": false}{/highlight} to disable specific tools',
|
||||
'Set {highlight}"tools": {"shell": false}{/highlight} to disable specific tools',
|
||||
'Set {highlight}"mcp_*": false{/highlight} to disable all tools from an MCP server',
|
||||
"Override global tool settings per agent configuration",
|
||||
'Set {highlight}"share": "auto"{/highlight} to automatically share all sessions',
|
||||
|
|
|
|||
|
|
@ -1622,7 +1622,7 @@ function ToolPart(props: { part: SessionMessageAssistantTool }) {
|
|||
return (
|
||||
<Show when={!shouldHide()}>
|
||||
<Switch>
|
||||
<Match when={display() === "bash"}>
|
||||
<Match when={display() === "shell"}>
|
||||
<Shell {...toolprops} />
|
||||
</Match>
|
||||
<Match when={display() === "glob"}>
|
||||
|
|
@ -1646,7 +1646,7 @@ function ToolPart(props: { part: SessionMessageAssistantTool }) {
|
|||
<Match when={display() === "edit"}>
|
||||
<Edit {...toolprops} />
|
||||
</Match>
|
||||
<Match when={display() === "task"}>
|
||||
<Match when={display() === "subagent"}>
|
||||
<Task {...toolprops} />
|
||||
</Match>
|
||||
<Match when={display() === "apply_patch"}>
|
||||
|
|
@ -2111,8 +2111,8 @@ function Task(props: ToolProps) {
|
|||
}}
|
||||
>
|
||||
{formatSubagentTitle(
|
||||
Locale.titlecase(stringValue(props.input.subagent_type) ?? "General"),
|
||||
description() ?? "Task",
|
||||
Locale.titlecase(stringValue(props.input.agent) ?? stringValue(props.input.subagent_type) ?? "General"),
|
||||
description() ?? "Subagent",
|
||||
props.metadata.background === true,
|
||||
)}
|
||||
</InlineTool>
|
||||
|
|
@ -2124,7 +2124,7 @@ export function formatSubagentToolcalls(count: number) {
|
|||
}
|
||||
|
||||
export function formatSubagentTitle(agent: string, description: string, background: boolean) {
|
||||
return `${agent} Task${background ? " (background)" : ""} — ${description}`
|
||||
return `${agent} Subagent${background ? " (background)" : ""} — ${description}`
|
||||
}
|
||||
|
||||
export function formatSubagentRetry(attempt: number, message: string) {
|
||||
|
|
@ -2402,7 +2402,7 @@ function numberValue(value: unknown) {
|
|||
}
|
||||
|
||||
const toolDisplays = new Set([
|
||||
"bash",
|
||||
"shell",
|
||||
"glob",
|
||||
"read",
|
||||
"grep",
|
||||
|
|
@ -2410,7 +2410,7 @@ const toolDisplays = new Set([
|
|||
"websearch",
|
||||
"write",
|
||||
"edit",
|
||||
"task",
|
||||
"subagent",
|
||||
"apply_patch",
|
||||
"todowrite",
|
||||
"question",
|
||||
|
|
@ -2418,7 +2418,10 @@ const toolDisplays = new Set([
|
|||
])
|
||||
|
||||
export function toolDisplay(tool: string) {
|
||||
return toolDisplays.has(tool) ? tool : "generic"
|
||||
// Legacy transcripts recorded the shell tool as "bash" and the subagent tool as "task"; render
|
||||
// them with the renamed views.
|
||||
const normalized = tool === "bash" ? "shell" : tool === "task" ? "subagent" : tool
|
||||
return toolDisplays.has(normalized) ? normalized : "generic"
|
||||
}
|
||||
|
||||
function recordValue(value: unknown): Record<string, unknown> | undefined {
|
||||
|
|
|
|||
|
|
@ -287,7 +287,7 @@ export function PermissionPrompt(props: { request: PermissionV2Request; director
|
|||
}
|
||||
}
|
||||
|
||||
if (permission === "bash") {
|
||||
if (permission === "shell") {
|
||||
const command = typeof data.command === "string" ? data.command : ""
|
||||
return {
|
||||
body: (
|
||||
|
|
@ -300,12 +300,17 @@ export function PermissionPrompt(props: { request: PermissionV2Request; director
|
|||
}
|
||||
}
|
||||
|
||||
if (permission === "task") {
|
||||
const type = typeof data.subagent_type === "string" ? data.subagent_type : "Unknown"
|
||||
if (permission === "subagent" || permission === "task") {
|
||||
const agent =
|
||||
typeof data.agent === "string"
|
||||
? data.agent
|
||||
: typeof data.subagent_type === "string"
|
||||
? data.subagent_type
|
||||
: "Unknown"
|
||||
const desc = typeof data.description === "string" ? data.description : ""
|
||||
return {
|
||||
icon: "#",
|
||||
title: `${Locale.titlecase(type)} Task`,
|
||||
title: `${Locale.titlecase(agent)} Subagent`,
|
||||
body: (
|
||||
<Show when={desc}>
|
||||
<box paddingLeft={1}>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue