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:
parent
15eb2e6249
commit
4ac0b52882
4 changed files with 114 additions and 73 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
88
web/frontend/src/components/chat/compact-tool-call.tsx
Normal file
88
web/frontend/src/components/chat/compact-tool-call.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue