redesign: compact tool display and anchored permission prompt

- Replace heavy card layout with compact inline tool calls
- Move permission prompt near input box (only shows when needed)
- Add request_permission tool to tool catalog for frontend display
- Create CompactToolCall component with expandable details
- Fix TypeScript errors (removed unused props, simplified icons)
- Preserve showAssistantDetails filter for non-request_permission tools
This commit is contained in:
Dark aura 2026-05-06 04:57:18 +01:00
parent 15eb2e6249
commit 4ac0b52882
4 changed files with 114 additions and 73 deletions

View file

@ -193,6 +193,12 @@ var toolCatalog = []toolCatalogEntry{
Category: "discovery",
ConfigKey: "mcp.discovery.use_bm25",
},
{
Name: "request_permission",
Description: "Request user permission for accessing paths outside workspace.",
Category: "system",
ConfigKey: "request_permission",
},
}
func (h *Handler) registerToolRoutes(mux *http.ServeMux) {

View file

@ -23,6 +23,7 @@ import {
type ChatAttachment,
type ChatToolCall,
} from "@/store/chat"
import { CompactToolCall } from "@/components/chat/compact-tool-call"
interface AssistantMessageProps {
content: string
@ -114,71 +115,17 @@ export function AssistantMessage({
</div>
)}
{(!isCollapsedBlock || isExpanded) && isToolCalls && hasToolCalls && (
<div className="space-y-3 px-3 pt-0 pb-3">
<div className="px-3 pt-0 pb-2">
{toolCalls.map((toolCall, index) => {
const explanation =
toolCall.extraContent?.toolFeedbackExplanation?.trim() ?? ""
const toolName = toolCall.function?.name?.trim() ?? ""
const toolArguments = toolCall.function?.arguments?.trim() ?? ""
const hasFunctionSummary = toolName || toolArguments
if (!explanation && !hasFunctionSummary) {
return null
}
const isRequestPermission = toolName === "request_permission"
return (
<div
<CompactToolCall
key={toolCall.id ?? `${toolName}-${index}`}
className={cn(
"space-y-3",
index > 0 && "border-border/20 border-t pt-3",
)}
>
{explanation && (
<div className="space-y-1.5">
<div className="text-muted-foreground/55 text-[11px] font-medium tracking-wide uppercase">
{t("chat.toolCallExplanationLabel")}
</div>
<div className="prose dark:prose-invert prose-p:my-1.5 prose-p:whitespace-pre-wrap max-w-none text-[13px] leading-relaxed [overflow-wrap:anywhere] break-words opacity-75">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[
rehypeRaw,
rehypeSanitize,
rehypeHighlight,
]}
>
{explanation}
</ReactMarkdown>
</div>
</div>
)}
{hasFunctionSummary && (
<div
className={cn(
"space-y-1.5",
explanation && "border-border/20 border-t pt-3",
)}
>
<div className="text-muted-foreground/55 text-[11px] font-medium tracking-wide uppercase">
{t("chat.toolCallFunctionLabel")}
</div>
<div className="bg-background/55 border-border/25 space-y-2 rounded-lg border px-3 py-2.5">
{toolName && (
<div className="text-foreground/75 font-mono text-[12px] font-semibold">
{toolName}
</div>
)}
{toolArguments && (
<pre className="text-muted-foreground/75 overflow-x-auto font-mono text-[12px] leading-relaxed break-words whitespace-pre-wrap">
{toolArguments}
</pre>
)}
</div>
</div>
)}
</div>
toolCall={toolCall}
isRequestPermission={isRequestPermission}
/>
)
})}
</div>

View file

@ -369,18 +369,6 @@ export function ChatPage() {
/>
)}
{permissionRequest && (
<div className="flex w-full">
<PermissionPrompt
toolName={permissionRequest.toolName}
path={permissionRequest.path}
originalCommand={permissionRequest.originalCommand}
onPermissionGranted={handlePermissionGranted}
onPermissionDenied={handlePermissionDenied}
/>
</div>
)}
{messages.map((msg) => {
if (
!showAssistantDetails &&
@ -421,6 +409,18 @@ export function ChatPage() {
onChange={handleImageSelection}
/>
{permissionRequest && (
<div className="px-4 pb-2">
<PermissionPrompt
toolName={permissionRequest.toolName}
path={permissionRequest.path}
originalCommand={permissionRequest.originalCommand}
onPermissionGranted={handlePermissionGranted}
onPermissionDenied={handlePermissionDenied}
/>
</div>
)}
<ChatComposer
input={input}
attachments={attachments}

View file

@ -0,0 +1,88 @@
import { useState } from "react"
import type { ChatToolCall } from "@/store/chat"
interface CompactToolCallProps {
toolCall: ChatToolCall
isRequestPermission?: boolean
}
export function CompactToolCall({
toolCall,
isRequestPermission = false,
}: CompactToolCallProps) {
const [isExpanded, setIsExpanded] = useState(isRequestPermission)
const toolName = toolCall.function?.name?.trim() ?? ""
const toolArguments = toolCall.function?.arguments?.trim() ?? ""
const explanation =
toolCall.extraContent?.toolFeedbackExplanation?.trim() ?? ""
// Try to parse args for display
let parsedArgs: Record<string, unknown> | null = null
if (toolArguments) {
try {
parsedArgs = JSON.parse(toolArguments)
} catch {
// ignore
}
}
// Status icon
const isError = explanation.toLowerCase().includes("error") || explanation.toLowerCase().includes("denied")
return (
<div
className={`flex items-start gap-2 py-1.5 text-[13px] border-b border-border/20 last:border-0 ${
isRequestPermission ? "bg-yellow-500/5 -mx-2 px-2 rounded-md" : ""
}`}
>
<button
onClick={() => setIsExpanded(!isExpanded)}
className="flex items-center gap-1.5 flex-1 text-left hover:opacity-80 transition-opacity"
>
<div
className={`flex items-center justify-center w-5 h-5 rounded shrink-0 mt-0.5 text-xs ${
isRequestPermission
? "bg-yellow-500/20 text-yellow-600"
: isError
? "bg-red-500/20 text-red-600"
: "bg-green-500/20 text-green-600"
}`}
>
{isExpanded ? "▼" : "🔧"}
</div>
<span className="font-medium text-foreground/90">{toolName}</span>
{!isExpanded && parsedArgs && (
<span className="text-muted-foreground/70 truncate">
{String(parsedArgs.path ?? parsedArgs.command ?? "")}
</span>
)}
{!isExpanded && !parsedArgs && explanation && (
<span className="text-muted-foreground/70 truncate max-w-[200px]">
{explanation}
</span>
)}
{!isExpanded && (
<span className={`shrink-0 ml-auto text-xs ${isError ? "text-red-500" : "text-green-500"}`}>
{isError ? "✗" : "✓"}
</span>
)}
</button>
{isExpanded && (explanation || toolArguments) && (
<div className="mt-1 space-y-2 w-full">
{explanation && (
<div className="text-muted-foreground/80 text-[12px] leading-relaxed whitespace-pre-wrap">
{explanation}
</div>
)}
{toolArguments && (
<div className="bg-muted/30 rounded-md p-2 font-mono text-[11px] overflow-x-auto">
<pre className="whitespace-pre-wrap break-all">{toolArguments}</pre>
</div>
)}
</div>
)}
</div>
)
}