Skip to content

Commit ee4baff

Browse files
committed
Add /tools dialog
1 parent 85ffa69 commit ee4baff

4 files changed

Lines changed: 106 additions & 1 deletion

File tree

packages/opencode/src/cli/cmd/tui/app.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { SyncProvider, useSync } from "@tui/context/sync"
1212
import { LocalProvider, useLocal } from "@tui/context/local"
1313
import { DialogModel, useConnected } from "@tui/component/dialog-model"
1414
import { DialogMcp } from "@tui/component/dialog-mcp"
15+
import { DialogTools } from "@tui/component/dialog-tools"
1516
import { DialogIde } from "@tui/component/dialog-ide"
1617
import { DialogStatus } from "@tui/component/dialog-status"
1718
import { DialogThemeList } from "@tui/component/dialog-theme-list"
@@ -350,6 +351,14 @@ function App() {
350351
dialog.replace(() => <DialogMcp />)
351352
},
352353
},
354+
{
355+
title: "List tools",
356+
value: "tool.list",
357+
category: "Agent",
358+
onSelect: () => {
359+
dialog.replace(() => <DialogTools />)
360+
},
361+
},
353362
{
354363
title: "Toggle IDEs",
355364
value: "ide.list",
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { createMemo, createResource } from "solid-js"
2+
import { map, pipe, sortBy } from "remeda"
3+
import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select"
4+
import { useSDK } from "@tui/context/sdk"
5+
6+
type ToolInfo = {
7+
id: string
8+
enabled: boolean
9+
}
10+
11+
export function DialogTools() {
12+
const sdk = useSDK()
13+
14+
const [tools] = createResource(async () => {
15+
const response = await fetch(`${sdk.url}/tool/list`)
16+
if (!response.ok) return []
17+
return (await response.json()) as ToolInfo[]
18+
})
19+
20+
const options = createMemo((): DialogSelectOption<string>[] => {
21+
const toolList = tools() ?? []
22+
23+
return pipe(
24+
toolList,
25+
sortBy((t) => t.id),
26+
map((t) => ({
27+
value: t.id,
28+
title: t.id,
29+
footer: t.enabled ? undefined : "disabled",
30+
category: undefined,
31+
})),
32+
)
33+
})
34+
35+
return (
36+
<DialogSelect
37+
title="Tools"
38+
options={options()}
39+
onSelect={() => {
40+
// Don't close on select, only on escape
41+
}}
42+
/>
43+
)
44+
}

packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,11 @@ export function Autocomplete(props: {
356356
description: "toggle MCPs",
357357
onSelect: () => command.trigger("mcp.list"),
358358
},
359+
{
360+
display: "/tools",
361+
description: "list tools",
362+
onSelect: () => command.trigger("tool.list"),
363+
},
359364
{
360365
display: "/ide",
361366
description: "toggle IDEs",

packages/opencode/src/server/server.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { proxy } from "hono/proxy"
1010
import { Session } from "../session"
1111
import z from "zod"
1212
import { Provider } from "../provider/provider"
13-
import { filter, mapValues, sortBy, pipe } from "remeda"
13+
import { filter, mapValues, sortBy, pipe, mergeDeep } from "remeda"
1414
import { NamedError } from "@opencode-ai/util/error"
1515
import { ModelsDev } from "../provider/models"
1616
import { Ripgrep } from "../file/ripgrep"
@@ -43,6 +43,7 @@ import { Ide } from "../ide"
4343
import { Storage } from "../storage/storage"
4444
import type { ContentfulStatusCode } from "hono/utils/http-status"
4545
import { TuiEvent } from "@/cli/cmd/tui/event"
46+
import { Wildcard } from "@/util/wildcard"
4647
import { Snapshot } from "@/snapshot"
4748
import { SessionSummary } from "@/session/summary"
4849
import { SessionStatus } from "@/session/status"
@@ -453,6 +454,52 @@ export namespace Server {
453454
return c.json(config)
454455
},
455456
)
457+
.get(
458+
"/tool/list",
459+
describeRoute({
460+
summary: "List tools",
461+
description: "Get a list of all available tools with their enabled status.",
462+
operationId: "tool.list",
463+
responses: {
464+
200: {
465+
description: "Tool list",
466+
content: {
467+
"application/json": {
468+
schema: resolver(
469+
z.array(
470+
z.object({
471+
id: z.string(),
472+
enabled: z.boolean(),
473+
}),
474+
),
475+
),
476+
},
477+
},
478+
},
479+
},
480+
}),
481+
async (c) => {
482+
const builtinIds = await ToolRegistry.ids()
483+
const mcpTools = await MCP.tools()
484+
const mcpIds = Object.keys(mcpTools)
485+
// Combine and filter out 'invalid' tool
486+
const allIds = [...builtinIds.filter((id) => id !== "invalid"), ...mcpIds]
487+
488+
// Get enabled status based on agent tools config
489+
const defaultAgentName = await Agent.defaultAgent()
490+
const agent = await Agent.get(defaultAgentName)
491+
const enabledTools = agent
492+
? mergeDeep(agent.tools, await ToolRegistry.enabled(agent))
493+
: ({} as Record<string, boolean>)
494+
495+
return c.json(
496+
allIds.map((id) => ({
497+
id,
498+
enabled: Wildcard.all(id, enabledTools) !== false,
499+
})),
500+
)
501+
},
502+
)
456503
.get(
457504
"/experimental/tool/ids",
458505
describeRoute({

0 commit comments

Comments
 (0)