fix(chat): fix mutation card rendering, reappearance after approve, and pseudocode tab switching

- MutationSuggestionCard: call onResolved immediately on approve/reject so
  resolvedIds is set even if component unmounts before dismiss timer fires.
  Invalidate mutations list query to refresh pendingSuggestions.
- AIChatPanel: use explicit null checks instead of falsy for oldValue/newValue
  so empty-string fields (e.g. decompiledCode) render correctly. Remove
  resolvedIds filter from streaming mutationSuggestions — the card manages
  its own lifecycle internally.
- CodeViewer: auto-switch to assembly tab when selecting a function that has
  no pseudocode (derived effectiveTab via useMemo).
This commit is contained in:
Rodrigo Verdiani 2026-07-04 18:01:30 -03:00
parent 914005e00e
commit e67a99671b
3 changed files with 45 additions and 18 deletions

View File

@ -3,6 +3,8 @@ import { formatDistanceToNow } from "date-fns";
import { Send, Bot, User, Loader2, Sparkles, Plus, Trash2 } from "lucide-react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import remarkMath from "remark-math";
import rehypeKatex from "rehype-katex";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
import { useAIChat } from "@/features/binary/hooks/useAIChat";
@ -25,7 +27,7 @@ const quickPrompts = [
"Summarize the binary",
];
const remarkPlugins = [remarkGfm];
const remarkPlugins = [remarkGfm, remarkMath];
const codeBlockTheme = {
...oneDark,
@ -352,7 +354,13 @@ export function AIChatPanel({
{mergedMessages.map((item, idx) => {
if (item.kind === "suggestion") {
const s = item.data;
if (!s.id || !s.targetId || !s.oldValue || !s.newValue) return null;
if (
s.id == null ||
s.targetId == null ||
s.oldValue == null ||
s.newValue == null
)
return null;
if (resolvedIds.has(s.id)) return null;
return (
<MutationSuggestionCard
@ -403,6 +411,7 @@ export function AIChatPanel({
<div className={markdownContainerClass}>
<ReactMarkdown
remarkPlugins={remarkPlugins}
rehypePlugins={[rehypeKatex]}
components={markdownComponents}
>
{normalizeMarkdown(message.content ?? "")}
@ -477,21 +486,19 @@ export function AIChatPanel({
<span>{toolStatus}</span>
</div>
)}
{mutationSuggestions
.filter((s) => !resolvedIds.has(s.mutationId))
.map((s) => (
<MutationSuggestionCard
key={s.mutationId}
mutationId={s.mutationId}
targetType={s.targetType}
targetId={s.targetId}
fieldName={s.fieldName}
oldValue={s.oldValue}
newValue={s.newValue}
onRenameApplied={onRenameApplied}
onResolved={handleResolved}
/>
))}
{mutationSuggestions.map((s) => (
<MutationSuggestionCard
key={s.mutationId}
mutationId={s.mutationId}
targetType={s.targetType}
targetId={s.targetId}
fieldName={s.fieldName}
oldValue={s.oldValue}
newValue={s.newValue}
onRenameApplied={onRenameApplied}
onResolved={handleResolved}
/>
))}
</>
)}
<div ref={messagesEndRef} />

View File

@ -153,8 +153,14 @@ export function CodeViewer({
const hasCfg = cfgBlocks.length > 0;
const hasPseudocode = !!func?.decompiledCode;
const effectiveTab = useMemo(() => {
if (activeTab === "pseudocode" && !hasPseudocode) return "assembly";
if (activeTab === "cfg" && !hasCfg) return "assembly";
return activeTab;
}, [activeTab, hasPseudocode, hasCfg]);
return (
<Tabs value={activeTab} onValueChange={setActiveTab} className="flex h-full flex-col">
<Tabs value={effectiveTab} onValueChange={setActiveTab} className="flex h-full flex-col">
<div className="border-border flex shrink-0 items-center justify-between gap-2 border-b px-4 py-2">
<div className="flex items-center gap-3">
<TabsList className="h-8">

View File

@ -158,6 +158,13 @@ export function MutationSuggestionCard({
if (invalidationKey) {
queryClient.invalidateQueries({ queryKey: [invalidationKey] });
}
queryClient.invalidateQueries({
predicate: (query) =>
typeof query.queryKey[0] === "string" &&
query.queryKey[0].startsWith("/binaries/") &&
query.queryKey[0].endsWith("/mutations"),
});
onResolved?.(mutationId);
onRenameApplied?.();
dismissTimerRef.current = setTimeout(resolve, 1500);
},
@ -168,6 +175,13 @@ export function MutationSuggestionCard({
mutation: {
onSuccess: () => {
setStatus("REJECTED");
queryClient.invalidateQueries({
predicate: (query) =>
typeof query.queryKey[0] === "string" &&
query.queryKey[0].startsWith("/binaries/") &&
query.queryKey[0].endsWith("/mutations"),
});
onResolved?.(mutationId);
onRenameApplied?.();
dismissTimerRef.current = setTimeout(resolve, 1500);
},