fix(app): websearch and codesearch tool rendering

This commit is contained in:
Adam
2026-03-05 07:46:31 -06:00
parent 4c185c70f2
commit 1a420a1a71
20 changed files with 213 additions and 2 deletions

View File

@@ -203,6 +203,41 @@ export function BasicTool(props: BasicToolProps) {
)
}
export function GenericTool(props: { tool: string; status?: string; hideDetails?: boolean }) {
return <BasicTool icon="mcp" status={props.status} trigger={{ title: props.tool }} hideDetails={props.hideDetails} />
function label(input: Record<string, unknown> | undefined) {
const keys = ["description", "query", "url", "filePath", "path", "pattern", "name"]
return keys.map((key) => input?.[key]).find((value): value is string => typeof value === "string" && value.length > 0)
}
function args(input: Record<string, unknown> | undefined) {
if (!input) return []
const skip = new Set(["description", "query", "url", "filePath", "path", "pattern", "name"])
return Object.entries(input)
.filter(([key]) => !skip.has(key))
.flatMap(([key, value]) => {
if (typeof value === "string") return [`${key}=${value}`]
if (typeof value === "number") return [`${key}=${value}`]
if (typeof value === "boolean") return [`${key}=${value}`]
return []
})
.slice(0, 3)
}
export function GenericTool(props: {
tool: string
status?: string
hideDetails?: boolean
input?: Record<string, unknown>
}) {
return (
<BasicTool
icon="mcp"
status={props.status}
trigger={{
title: `Called \`${props.tool}\``,
subtitle: label(props.input),
args: args(props.input),
}}
hideDetails={props.hideDetails}
/>
)
}

View File

@@ -577,6 +577,46 @@
justify-content: center;
}
[data-component="exa-tool-output"] {
width: 100%;
padding-top: 8px;
display: flex;
flex-direction: column;
}
[data-slot="basic-tool-tool-subtitle"].exa-tool-query {
display: block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
[data-slot="exa-tool-links"] {
display: flex;
flex-direction: column;
gap: 4px;
}
[data-slot="exa-tool-link"] {
display: block;
max-width: 100%;
color: var(--text-interactive-base);
text-decoration: underline;
text-underline-offset: 2px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&:hover {
color: var(--text-interactive-base);
}
&:visited {
color: var(--text-interactive-base);
}
}
[data-component="todos"] {
padding: 10px 0 24px 0;
display: flex;

View File

@@ -243,6 +243,18 @@ export function getToolInfo(tool: string, input: any = {}): ToolInfo {
title: i18n.t("ui.tool.webfetch"),
subtitle: input.url,
}
case "websearch":
return {
icon: "window-cursor",
title: i18n.t("ui.tool.websearch"),
subtitle: input.query,
}
case "codesearch":
return {
icon: "code",
title: i18n.t("ui.tool.codesearch"),
subtitle: input.query,
}
case "task":
return {
icon: "task",
@@ -303,6 +315,18 @@ export function getToolInfo(tool: string, input: any = {}): ToolInfo {
}
}
function urls(text: string | undefined) {
if (!text) return []
const seen = new Set<string>()
return [...text.matchAll(/https?:\/\/[^\s<>"'`)\]]+/g)]
.map((item) => item[0].replace(/[),.;:!?]+$/g, ""))
.filter((item) => {
if (seen.has(item)) return false
seen.add(item)
return true
})
}
const CONTEXT_GROUP_TOOLS = new Set(["read", "glob", "grep", "list"])
const HIDDEN_TOOLS = new Set(["todowrite", "todoread"])
@@ -598,6 +622,32 @@ function contextToolSummary(parts: ToolPart[]) {
return { read, search, list }
}
function ExaOutput(props: { output?: string }) {
const links = createMemo(() => urls(props.output))
return (
<Show when={links().length > 0}>
<div data-component="exa-tool-output">
<div data-slot="exa-tool-links">
<For each={links()}>
{(url) => (
<a
data-slot="exa-tool-link"
href={url}
target="_blank"
rel="noopener noreferrer"
onClick={(event) => event.stopPropagation()}
>
{url}
</a>
)}
</For>
</div>
</div>
</Show>
)
}
export function registerPartComponent(type: string, component: PartComponent) {
PART_MAPPING[type] = component
}
@@ -1467,6 +1517,58 @@ ToolRegistry.register({
},
})
ToolRegistry.register({
name: "websearch",
render(props) {
const i18n = useI18n()
const query = createMemo(() => {
const value = props.input.query
if (typeof value !== "string") return ""
return value
})
return (
<BasicTool
{...props}
icon="window-cursor"
trigger={{
title: i18n.t("ui.tool.websearch"),
subtitle: query(),
subtitleClass: "exa-tool-query",
}}
>
<ExaOutput output={props.output} />
</BasicTool>
)
},
})
ToolRegistry.register({
name: "codesearch",
render(props) {
const i18n = useI18n()
const query = createMemo(() => {
const value = props.input.query
if (typeof value !== "string") return ""
return value
})
return (
<BasicTool
{...props}
icon="code"
trigger={{
title: i18n.t("ui.tool.codesearch"),
subtitle: query(),
subtitleClass: "exa-tool-query",
}}
>
<ExaOutput output={props.output} />
</BasicTool>
)
},
})
ToolRegistry.register({
name: "task",
render(props) {