-
Notifications
You must be signed in to change notification settings - Fork 960
fix(chat): prevent ask_user question from shadowing sandbox access prompt #3662
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4f8896b
1b1d732
072b70e
189b3bc
7de5fe2
eaab818
4cf09ff
89db3b9
d7b74f0
8aab6d0
7003f18
f8148e5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| import { ToolCallRow } from "@superset/ui/ai-elements/tool-call-row"; | ||
| import { | ||
| CheckIcon, | ||
| CircleXIcon, | ||
| ClockIcon, | ||
| FolderLockIcon, | ||
| XIcon, | ||
| } from "lucide-react"; | ||
| import type { ComponentType } from "react"; | ||
| import type { ToolPart } from "../../../../utils/tool-helpers"; | ||
| import type { ToolStatusBadgeVariant } from "../ToolStatusBadge"; | ||
| import { ToolStatusBadge } from "../ToolStatusBadge"; | ||
|
|
||
| interface RequestSandboxAccessToolCallProps { | ||
| part: ToolPart; | ||
| args: Record<string, unknown>; | ||
| result: Record<string, unknown>; | ||
| isInterrupted?: boolean; | ||
| } | ||
|
|
||
| type AccessStatus = "pending" | "granted" | "denied" | "cancelled" | "error"; | ||
|
|
||
| const ACCESS_STATUS_CONFIG: Record< | ||
| AccessStatus, | ||
| { | ||
| icon: ComponentType<{ className?: string }>; | ||
| label: string; | ||
| variant?: ToolStatusBadgeVariant; | ||
| } | ||
| > = { | ||
| pending: { icon: ClockIcon, label: "Awaiting Response" }, | ||
| granted: { icon: CheckIcon, label: "Access Granted" }, | ||
| denied: { icon: XIcon, label: "Access Denied" }, | ||
| cancelled: { icon: XIcon, label: "Cancelled" }, | ||
| error: { icon: CircleXIcon, label: "Error", variant: "danger" }, | ||
| }; | ||
|
|
||
| function toAccessDecision(content: string): "granted" | "denied" | null { | ||
| if (content.startsWith("Access already granted")) return "granted"; | ||
| if (content.startsWith("Access granted")) return "granted"; | ||
| if (content.startsWith("Access denied")) return "denied"; | ||
| return null; | ||
| } | ||
|
|
||
| function toAccessStatus( | ||
| part: ToolPart, | ||
| result: Record<string, unknown>, | ||
| isInterrupted: boolean, | ||
| ): AccessStatus { | ||
| if ( | ||
| isInterrupted && | ||
| part.state !== "output-available" && | ||
| part.state !== "output-error" | ||
| ) { | ||
| return "cancelled"; | ||
| } | ||
| if (part.state !== "output-available" && part.state !== "output-error") { | ||
| return "pending"; | ||
| } | ||
| if (part.state === "output-error" || result.isError === true) { | ||
| return "error"; | ||
| } | ||
| const content = | ||
| (typeof result.content === "string" && result.content.trim()) || | ||
| (typeof result.text === "string" && result.text.trim()) || | ||
| ""; | ||
| return toAccessDecision(content) ?? "error"; | ||
| } | ||
|
|
||
| export function RequestSandboxAccessToolCall({ | ||
| part, | ||
| args, | ||
| result, | ||
| isInterrupted = false, | ||
| }: RequestSandboxAccessToolCallProps) { | ||
| const requestedPath = typeof args.path === "string" ? args.path.trim() : null; | ||
| const reason = typeof args.reason === "string" ? args.reason.trim() : null; | ||
|
|
||
| const status = toAccessStatus(part, result, isInterrupted); | ||
| const { icon, label, variant } = ACCESS_STATUS_CONFIG[status]; | ||
| const statusBadge = ( | ||
| <ToolStatusBadge icon={icon} label={label} variant={variant} /> | ||
| ); | ||
|
|
||
| const isPending = status === "pending"; | ||
| const isCancelledOrError = status === "cancelled" || status === "error"; | ||
| const hasContext = Boolean(requestedPath || reason); | ||
|
|
||
| return ( | ||
| <ToolCallRow | ||
| icon={FolderLockIcon} | ||
| isPending={false} | ||
| isError={false} | ||
| title="Request Access" | ||
| description={statusBadge} | ||
| > | ||
| {!isPending && hasContext ? ( | ||
| <div className="space-y-1 px-3 py-2"> | ||
| {requestedPath ? ( | ||
| <div className="text-xs text-muted-foreground"> | ||
| Path: {requestedPath} | ||
| </div> | ||
| ) : null} | ||
| {reason ? ( | ||
| <div className="text-xs text-muted-foreground"> | ||
| Reason: {reason} | ||
| </div> | ||
| ) : null} | ||
| {!isCancelledOrError ? ( | ||
| <div className="text-sm text-foreground"> | ||
| {status === "granted" ? "Access granted" : "Access denied"} | ||
| </div> | ||
| ) : ( | ||
| <div className="flex items-center gap-1 text-sm text-destructive"> | ||
| <CircleXIcon className="h-3 w-3 shrink-0" /> | ||
| Aborted | ||
| </div> | ||
| )} | ||
| </div> | ||
| ) : undefined} | ||
| </ToolCallRow> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { RequestSandboxAccessToolCall } from "./RequestSandboxAccessToolCall"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import { cn } from "@superset/ui/lib/utils"; | ||
| import type { ComponentType } from "react"; | ||
|
|
||
| const VARIANT_CLASSES = { | ||
| default: "", | ||
| success: "text-emerald-500", | ||
| danger: "text-destructive", | ||
| } as const; | ||
|
|
||
| export type ToolStatusBadgeVariant = keyof typeof VARIANT_CLASSES; | ||
|
|
||
| interface ToolStatusBadgeProps { | ||
| icon: ComponentType<{ className?: string }>; | ||
| label: string; | ||
| variant?: ToolStatusBadgeVariant; | ||
| } | ||
|
|
||
| export function ToolStatusBadge({ | ||
| icon: Icon, | ||
| label, | ||
| variant = "default", | ||
| }: ToolStatusBadgeProps) { | ||
| return ( | ||
| <span | ||
| className={cn( | ||
| "ml-2 flex items-center gap-1 font-medium uppercase tracking-wide", | ||
| VARIANT_CLASSES[variant], | ||
| )} | ||
| > | ||
| <Icon className="h-3 w-3 shrink-0" /> | ||
| {label} | ||
| </span> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export type { ToolStatusBadgeVariant } from "./ToolStatusBadge"; | ||
| export { ToolStatusBadge } from "./ToolStatusBadge"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,7 +29,8 @@ const TOOL_NAME_ALIASES: Record<string, string> = { | |
|
|
||
| // Keep explicit passthroughs for newer Mastra tool names | ||
| ast_smart_edit: "ast_smart_edit", | ||
| request_sandbox_access: "request_sandbox_access", | ||
| request_access: "request_access", | ||
| request_sandbox_access: "request_access", | ||
|
Comment on lines
+32
to
+33
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Verify stale expectations for request_sandbox_access normalization.
# Expected after the fix: any normalizeToolName("request_sandbox_access") expectation should be "request_access".
rg -n -C3 'request_sandbox_access' --iglob '*tool-helpers.test.ts'Repository: superset-sh/superset Length of output: 1168 🏁 Script executed: #!/bin/bash
set -euo pipefail
# 1. Verify the implementation shows the mapping
echo "=== Implementation in tool-helpers.ts (lines 32-33) ==="
sed -n '30,35p' apps/desktop/src/renderer/components/Chat/ChatInterface/utils/tool-helpers.ts
# 2. Check for all references to this mapping in tests
echo ""
echo "=== All test expectations for request_access in tool-helpers.test.ts ==="
rg -n 'request_access' --iglob '*tool-helpers.test.ts'Repository: superset-sh/superset Length of output: 419 Update the normalization test for the new canonical alias. The implementation now maps 🤖 Prompt for AI Agents |
||
| task_write: "task_write", | ||
| task_check: "task_check", | ||
| submit_plan: "submit_plan", | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isPending={false}hardcoded suppresses spinner for the pending stateToolCallRowrenders aBrailleSpinnerin the icon slot whenisPending={true}and the row is not hovered, giving users a visual cue that work is in progress. By hardcodingisPending={false}, the icon always stays asFolderLockIcon— even while the sandbox request is waiting for a response — forfeiting the built-in loading indicator.The component's own
ToolStatusBadge("Awaiting Response" + clock icon) does convey the pending state, so this isn't a correctness bug, but it diverges from the rest of the tool call rows that useisPendingto drive the spinner. Consider passingisPending={isPending}to stay consistent with the pattern:Prompt To Fix With AI