pull/6319/head
Dax Raad 2025-12-31 12:01:57 -05:00
parent 32c3fcbe61
commit f77e1d9734
4 changed files with 88 additions and 47 deletions

View File

@ -11,7 +11,12 @@
"options": {},
},
},
"mcp": {},
"mcp": {
"context7": {
"type": "remote",
"url": "https://mcp.context7.com/mcp",
},
},
"tools": {
"github-triage": false,
},

View File

@ -75,14 +75,16 @@ function EditBody(props: { request: PermissionRequest }) {
)
}
function TextBody(props: { title: string; description?: string; icon: string }) {
function TextBody(props: { title: string; description?: string; icon?: string }) {
const { theme } = useTheme()
return (
<>
<box flexDirection="row" gap={1} paddingLeft={1}>
<text fg={theme.textMuted} flexShrink={0}>
{props.icon}
</text>
<Show when={props.icon}>
<text fg={theme.textMuted} flexShrink={0}>
{props.icon}
</text>
</Show>
<text fg={theme.textMuted}>{props.title}</text>
</box>
<Show when={props.description}>
@ -113,12 +115,33 @@ export function PermissionPrompt(props: { request: PermissionRequest }) {
return {}
})
const { theme } = useTheme()
return (
<Switch>
<Match when={store.always}>
<Prompt
title="Always allow"
body={<TextBody icon="→" title={props.request.always.join("\n")} />}
body={
<Switch>
<Match when={props.request.always.length === 1 && props.request.always[0] === "*"}>
<TextBody title={"Are you sure you want to always allow " + props.request.permission + "?"} />
</Match>
<Match when={true}>
<box paddingLeft={1} gap={1}>
<text fg={theme.textMuted}>Applies to the following patterns</text>
<For each={props.request.always}>
{(pattern) => (
<text fg={theme.text}>
{"- "}
{pattern}
</text>
)}
</For>
</box>
</Match>
</Switch>
}
options={{ confirm: "Confirm", cancel: "Cancel" }}
onSelect={(option) => {
if (option === "cancel") {

View File

@ -9,7 +9,7 @@ import { SessionRevert } from "./revert"
import { Session } from "."
import { Agent } from "../agent/agent"
import { Provider } from "../provider/provider"
import { type Tool as AITool, tool, jsonSchema } from "ai"
import { type Tool as AITool, tool, jsonSchema, type ToolCallOptions } from "ai"
import { SessionCompaction } from "./compaction"
import { Instance } from "../project/instance"
import { Bus } from "../bus"
@ -596,6 +596,41 @@ export namespace SessionPrompt {
}) {
using _ = log.time("resolveTools")
const tools: Record<string, AITool> = {}
const context = (args: any, options: ToolCallOptions): Tool.Context => ({
sessionID: input.session.id,
abort: options.abortSignal!,
messageID: input.processor.message.id,
callID: options.toolCallId,
extra: { model: input.model },
agent: input.agent.name,
metadata: async (val: { title?: string; metadata?: any }) => {
const match = input.processor.partFromToolCall(options.toolCallId)
if (match && match.state.status === "running") {
await Session.updatePart({
...match,
state: {
title: val.title,
metadata: val.metadata,
status: "running",
input: args,
time: {
start: Date.now(),
},
},
})
}
},
async ask(req) {
await PermissionNext.ask({
...req,
sessionID: input.session.parentID ?? input.session.id,
tool: { messageID: input.processor.message.id, callID: options.toolCallId },
ruleset: input.agent.permission,
})
},
})
for (const item of await ToolRegistry.tools(input.model.providerID)) {
const schema = ProviderTransform.schema(input.model, z.toJSONSchema(item.parameters))
tools[item.id] = tool({
@ -603,57 +638,25 @@ export namespace SessionPrompt {
description: item.description,
inputSchema: jsonSchema(schema as any),
async execute(args, options) {
const ctx = context(args, options)
await Plugin.trigger(
"tool.execute.before",
{
tool: item.id,
sessionID: input.session.id,
callID: options.toolCallId,
sessionID: ctx.sessionID,
callID: ctx.callID,
},
{
args,
},
)
const ctx: Tool.Context = {
sessionID: input.session.id,
abort: options.abortSignal!,
messageID: input.processor.message.id,
callID: options.toolCallId,
extra: { model: input.model },
agent: input.agent.name,
metadata: async (val: { title?: string; metadata?: any }) => {
const match = input.processor.partFromToolCall(options.toolCallId)
if (match && match.state.status === "running") {
await Session.updatePart({
...match,
state: {
title: val.title,
metadata: val.metadata,
status: "running",
input: args,
time: {
start: Date.now(),
},
},
})
}
},
async ask(req) {
await PermissionNext.ask({
...req,
sessionID: input.session.parentID ?? input.session.id,
tool: { messageID: input.processor.message.id, callID: options.toolCallId },
ruleset: input.agent.permission,
})
},
}
const result = await item.execute(args, ctx)
await Plugin.trigger(
"tool.execute.after",
{
tool: item.id,
sessionID: input.session.id,
callID: options.toolCallId,
sessionID: ctx.sessionID,
callID: ctx.callID,
},
result,
)
@ -667,30 +670,41 @@ export namespace SessionPrompt {
},
})
}
for (const [key, item] of Object.entries(await MCP.tools())) {
const execute = item.execute
if (!execute) continue
// Wrap execute to add plugin hooks and format output
item.execute = async (args, opts) => {
const ctx = context(args, opts)
await Plugin.trigger(
"tool.execute.before",
{
tool: key,
sessionID: input.session.id,
sessionID: ctx.sessionID,
callID: opts.toolCallId,
},
{
args,
},
)
await ctx.ask({
permission: key,
metadata: {},
patterns: ["*"],
always: ["*"],
})
const result = await execute(args, opts)
await Plugin.trigger(
"tool.execute.after",
{
tool: key,
sessionID: input.session.id,
sessionID: ctx.sessionID,
callID: opts.toolCallId,
},
result,

View File

@ -2,7 +2,6 @@ import { BashTool } from "./bash"
import { EditTool } from "./edit"
import { GlobTool } from "./glob"
import { GrepTool } from "./grep"
import { ListTool } from "./ls"
import { BatchTool } from "./batch"
import { ReadTool } from "./read"
import { TaskTool } from "./task"