feat(rename): implement rename suggestion handling
This commit is contained in:
parent
979f804623
commit
72c3078912
@ -35,6 +35,20 @@ export interface ProjectResponse {
|
||||
binaryCount?: number;
|
||||
}
|
||||
|
||||
export interface RenameSuggestionResponse {
|
||||
id?: string;
|
||||
functionId?: string;
|
||||
binaryId?: string;
|
||||
projectId?: string;
|
||||
oldName?: string;
|
||||
newName?: string;
|
||||
source?: string;
|
||||
sessionId?: string;
|
||||
status?: string;
|
||||
createdAt?: string;
|
||||
resolvedAt?: string;
|
||||
}
|
||||
|
||||
export interface BinaryDependencyResponse {
|
||||
id: string;
|
||||
/** @minLength 1 */
|
||||
@ -103,6 +117,26 @@ export interface JobResponse {
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface RenameRequest {
|
||||
/**
|
||||
* @minLength 0
|
||||
* @maxLength 500
|
||||
*/
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface FunctionSummaryResponse {
|
||||
id?: string;
|
||||
analysisId?: string;
|
||||
name?: string;
|
||||
resolvedName?: string;
|
||||
address?: string;
|
||||
size?: number;
|
||||
flags?: string;
|
||||
renamed?: boolean;
|
||||
originalName?: string;
|
||||
}
|
||||
|
||||
export interface SessionResponse {
|
||||
id?: string;
|
||||
binaryId?: string;
|
||||
@ -149,8 +183,8 @@ export interface PageableObject {
|
||||
}
|
||||
|
||||
export interface PageLibrarySymbolResponse {
|
||||
totalElements?: number;
|
||||
totalPages?: number;
|
||||
totalElements?: number;
|
||||
size?: number;
|
||||
content?: LibrarySymbolResponse[];
|
||||
number?: number;
|
||||
@ -197,6 +231,13 @@ export interface LabelResponse {
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export interface RenameHistoryEntry {
|
||||
oldName?: string;
|
||||
newName?: string;
|
||||
source?: string;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
export interface FunctionDetailResponse {
|
||||
id?: string;
|
||||
analysisId?: string;
|
||||
@ -214,6 +255,9 @@ export interface FunctionDetailResponse {
|
||||
stackFrame?: FrameMemberResponse[];
|
||||
comments?: CommentResponse[];
|
||||
cfgBlocks?: CfgBlockResponse[];
|
||||
renamed?: boolean;
|
||||
originalName?: string;
|
||||
renameHistory?: RenameHistoryEntry[];
|
||||
}
|
||||
|
||||
export interface XrefResponse {
|
||||
@ -312,19 +356,9 @@ export interface DataLabelResponse {
|
||||
dataXrefs?: DataXrefResponse[];
|
||||
}
|
||||
|
||||
export interface FunctionSummaryResponse {
|
||||
id?: string;
|
||||
analysisId?: string;
|
||||
name?: string;
|
||||
resolvedName?: string;
|
||||
address?: string;
|
||||
size?: number;
|
||||
flags?: string;
|
||||
}
|
||||
|
||||
export interface PageFunctionSummaryResponse {
|
||||
totalElements?: number;
|
||||
totalPages?: number;
|
||||
totalElements?: number;
|
||||
size?: number;
|
||||
content?: FunctionSummaryResponse[];
|
||||
number?: number;
|
||||
@ -470,6 +504,10 @@ export const GetJobsType = {
|
||||
GENERATE_EMBEDDINGS: "GENERATE_EMBEDDINGS",
|
||||
} as const;
|
||||
|
||||
export type ListRenameSuggestionsParams = {
|
||||
status?: string;
|
||||
};
|
||||
|
||||
export type ListFunctionsParams = {
|
||||
pageable: Pageable;
|
||||
};
|
||||
|
||||
@ -4,25 +4,272 @@
|
||||
* OpenAPI definition
|
||||
* OpenAPI spec version: v0
|
||||
*/
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import type {
|
||||
DataTag,
|
||||
DefinedInitialDataOptions,
|
||||
DefinedUseQueryResult,
|
||||
MutationFunction,
|
||||
QueryClient,
|
||||
QueryFunction,
|
||||
QueryKey,
|
||||
UndefinedInitialDataOptions,
|
||||
UseMutationOptions,
|
||||
UseMutationResult,
|
||||
UseQueryOptions,
|
||||
UseQueryResult,
|
||||
} from "@tanstack/react-query";
|
||||
|
||||
import type { FunctionDetailResponse, XrefResponse } from "../api.schemas";
|
||||
import type {
|
||||
FunctionDetailResponse,
|
||||
FunctionSummaryResponse,
|
||||
ListRenameSuggestionsParams,
|
||||
RenameRequest,
|
||||
RenameSuggestionResponse,
|
||||
XrefResponse,
|
||||
} from "../api.schemas";
|
||||
|
||||
import { customInstance } from "../../api";
|
||||
|
||||
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
|
||||
|
||||
/**
|
||||
* Reject the suggested rename without applying it.
|
||||
* @summary Reject a rename suggestion
|
||||
*/
|
||||
export const rejectRename = (
|
||||
renameId: string,
|
||||
options?: SecondParameter<typeof customInstance>,
|
||||
signal?: AbortSignal
|
||||
) => {
|
||||
return customInstance<void>(
|
||||
{
|
||||
url: `/rename-suggestions/${encodeURIComponent(String(renameId))}/reject`,
|
||||
method: "POST",
|
||||
signal,
|
||||
},
|
||||
options
|
||||
);
|
||||
};
|
||||
|
||||
export const getRejectRenameMutationOptions = <TError = unknown, TContext = unknown>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof rejectRename>>,
|
||||
TError,
|
||||
{ renameId: string },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customInstance>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof rejectRename>>,
|
||||
TError,
|
||||
{ renameId: string },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ["rejectRename"];
|
||||
const { mutation: mutationOptions, request: requestOptions } = options
|
||||
? options.mutation && "mutationKey" in options.mutation && options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey }, request: undefined };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof rejectRename>>,
|
||||
{ renameId: string }
|
||||
> = (props) => {
|
||||
const { renameId } = props ?? {};
|
||||
|
||||
return rejectRename(renameId, requestOptions);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type RejectRenameMutationResult = NonNullable<Awaited<ReturnType<typeof rejectRename>>>;
|
||||
|
||||
export type RejectRenameMutationError = unknown;
|
||||
|
||||
/**
|
||||
* @summary Reject a rename suggestion
|
||||
*/
|
||||
export const useRejectRename = <TError = unknown, TContext = unknown>(
|
||||
options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof rejectRename>>,
|
||||
TError,
|
||||
{ renameId: string },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customInstance>;
|
||||
},
|
||||
queryClient?: QueryClient
|
||||
): UseMutationResult<
|
||||
Awaited<ReturnType<typeof rejectRename>>,
|
||||
TError,
|
||||
{ renameId: string },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getRejectRenameMutationOptions(options), queryClient);
|
||||
};
|
||||
/**
|
||||
* Apply the suggested rename to the function.
|
||||
* @summary Approve a rename suggestion
|
||||
*/
|
||||
export const approveRename = (
|
||||
renameId: string,
|
||||
options?: SecondParameter<typeof customInstance>,
|
||||
signal?: AbortSignal
|
||||
) => {
|
||||
return customInstance<RenameSuggestionResponse>(
|
||||
{
|
||||
url: `/rename-suggestions/${encodeURIComponent(String(renameId))}/approve`,
|
||||
method: "POST",
|
||||
signal,
|
||||
},
|
||||
options
|
||||
);
|
||||
};
|
||||
|
||||
export const getApproveRenameMutationOptions = <TError = unknown, TContext = unknown>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof approveRename>>,
|
||||
TError,
|
||||
{ renameId: string },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customInstance>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof approveRename>>,
|
||||
TError,
|
||||
{ renameId: string },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ["approveRename"];
|
||||
const { mutation: mutationOptions, request: requestOptions } = options
|
||||
? options.mutation && "mutationKey" in options.mutation && options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey }, request: undefined };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof approveRename>>,
|
||||
{ renameId: string }
|
||||
> = (props) => {
|
||||
const { renameId } = props ?? {};
|
||||
|
||||
return approveRename(renameId, requestOptions);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type ApproveRenameMutationResult = NonNullable<Awaited<ReturnType<typeof approveRename>>>;
|
||||
|
||||
export type ApproveRenameMutationError = unknown;
|
||||
|
||||
/**
|
||||
* @summary Approve a rename suggestion
|
||||
*/
|
||||
export const useApproveRename = <TError = unknown, TContext = unknown>(
|
||||
options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof approveRename>>,
|
||||
TError,
|
||||
{ renameId: string },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customInstance>;
|
||||
},
|
||||
queryClient?: QueryClient
|
||||
): UseMutationResult<
|
||||
Awaited<ReturnType<typeof approveRename>>,
|
||||
TError,
|
||||
{ renameId: string },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getApproveRenameMutationOptions(options), queryClient);
|
||||
};
|
||||
/**
|
||||
* Manually rename a function without going through the approval flow.
|
||||
* @summary Rename a function directly
|
||||
*/
|
||||
export const renameFunction = (
|
||||
functionId: string,
|
||||
renameRequest: RenameRequest,
|
||||
options?: SecondParameter<typeof customInstance>,
|
||||
signal?: AbortSignal
|
||||
) => {
|
||||
return customInstance<FunctionSummaryResponse>(
|
||||
{
|
||||
url: `/functions/${encodeURIComponent(String(functionId))}/rename`,
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
data: renameRequest,
|
||||
signal,
|
||||
},
|
||||
options
|
||||
);
|
||||
};
|
||||
|
||||
export const getRenameFunctionMutationOptions = <TError = unknown, TContext = unknown>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof renameFunction>>,
|
||||
TError,
|
||||
{ functionId: string; data: RenameRequest },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customInstance>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof renameFunction>>,
|
||||
TError,
|
||||
{ functionId: string; data: RenameRequest },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ["renameFunction"];
|
||||
const { mutation: mutationOptions, request: requestOptions } = options
|
||||
? options.mutation && "mutationKey" in options.mutation && options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey }, request: undefined };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof renameFunction>>,
|
||||
{ functionId: string; data: RenameRequest }
|
||||
> = (props) => {
|
||||
const { functionId, data } = props ?? {};
|
||||
|
||||
return renameFunction(functionId, data, requestOptions);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type RenameFunctionMutationResult = NonNullable<Awaited<ReturnType<typeof renameFunction>>>;
|
||||
export type RenameFunctionMutationBody = RenameRequest;
|
||||
export type RenameFunctionMutationError = unknown;
|
||||
|
||||
/**
|
||||
* @summary Rename a function directly
|
||||
*/
|
||||
export const useRenameFunction = <TError = unknown, TContext = unknown>(
|
||||
options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof renameFunction>>,
|
||||
TError,
|
||||
{ functionId: string; data: RenameRequest },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customInstance>;
|
||||
},
|
||||
queryClient?: QueryClient
|
||||
): UseMutationResult<
|
||||
Awaited<ReturnType<typeof renameFunction>>,
|
||||
TError,
|
||||
{ functionId: string; data: RenameRequest },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getRenameFunctionMutationOptions(options), queryClient);
|
||||
};
|
||||
/**
|
||||
* Retrieve full details of a function including assembly, decompiled code, and xref counts.
|
||||
* @summary Get function details
|
||||
@ -352,3 +599,151 @@ export function useGetCallees<TData = Awaited<ReturnType<typeof getCallees>>, TE
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve pending or resolved rename suggestions.
|
||||
* @summary List rename suggestions for a binary
|
||||
*/
|
||||
export const listRenameSuggestions = (
|
||||
binaryId: string,
|
||||
params?: ListRenameSuggestionsParams,
|
||||
options?: SecondParameter<typeof customInstance>,
|
||||
signal?: AbortSignal
|
||||
) => {
|
||||
return customInstance<RenameSuggestionResponse[]>(
|
||||
{
|
||||
url: `/binaries/${encodeURIComponent(String(binaryId))}/rename-suggestions`,
|
||||
method: "GET",
|
||||
params,
|
||||
signal,
|
||||
},
|
||||
options
|
||||
);
|
||||
};
|
||||
|
||||
export const getListRenameSuggestionsQueryKey = (
|
||||
binaryId: string,
|
||||
params?: ListRenameSuggestionsParams
|
||||
) => {
|
||||
return [`/binaries/${binaryId}/rename-suggestions`, ...(params ? [params] : [])] as const;
|
||||
};
|
||||
|
||||
export const getListRenameSuggestionsQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof listRenameSuggestions>>,
|
||||
TError = unknown,
|
||||
>(
|
||||
binaryId: string,
|
||||
params?: ListRenameSuggestionsParams,
|
||||
options?: {
|
||||
query?: Partial<
|
||||
UseQueryOptions<Awaited<ReturnType<typeof listRenameSuggestions>>, TError, TData>
|
||||
>;
|
||||
request?: SecondParameter<typeof customInstance>;
|
||||
}
|
||||
) => {
|
||||
const { query: queryOptions, request: requestOptions } = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getListRenameSuggestionsQueryKey(binaryId, params);
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof listRenameSuggestions>>> = ({ signal }) =>
|
||||
listRenameSuggestions(binaryId, params, requestOptions, signal);
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
queryFn,
|
||||
enabled: binaryId !== null && binaryId !== undefined,
|
||||
...queryOptions,
|
||||
} as UseQueryOptions<Awaited<ReturnType<typeof listRenameSuggestions>>, TError, TData> & {
|
||||
queryKey: DataTag<QueryKey, TData, TError>;
|
||||
};
|
||||
};
|
||||
|
||||
export type ListRenameSuggestionsQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof listRenameSuggestions>>
|
||||
>;
|
||||
export type ListRenameSuggestionsQueryError = unknown;
|
||||
|
||||
export function useListRenameSuggestions<
|
||||
TData = Awaited<ReturnType<typeof listRenameSuggestions>>,
|
||||
TError = unknown,
|
||||
>(
|
||||
binaryId: string,
|
||||
params: undefined | ListRenameSuggestionsParams,
|
||||
options: {
|
||||
query: Partial<
|
||||
UseQueryOptions<Awaited<ReturnType<typeof listRenameSuggestions>>, TError, TData>
|
||||
> &
|
||||
Pick<
|
||||
DefinedInitialDataOptions<
|
||||
Awaited<ReturnType<typeof listRenameSuggestions>>,
|
||||
TError,
|
||||
Awaited<ReturnType<typeof listRenameSuggestions>>
|
||||
>,
|
||||
"initialData"
|
||||
>;
|
||||
request?: SecondParameter<typeof customInstance>;
|
||||
},
|
||||
queryClient?: QueryClient
|
||||
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
|
||||
export function useListRenameSuggestions<
|
||||
TData = Awaited<ReturnType<typeof listRenameSuggestions>>,
|
||||
TError = unknown,
|
||||
>(
|
||||
binaryId: string,
|
||||
params?: ListRenameSuggestionsParams,
|
||||
options?: {
|
||||
query?: Partial<
|
||||
UseQueryOptions<Awaited<ReturnType<typeof listRenameSuggestions>>, TError, TData>
|
||||
> &
|
||||
Pick<
|
||||
UndefinedInitialDataOptions<
|
||||
Awaited<ReturnType<typeof listRenameSuggestions>>,
|
||||
TError,
|
||||
Awaited<ReturnType<typeof listRenameSuggestions>>
|
||||
>,
|
||||
"initialData"
|
||||
>;
|
||||
request?: SecondParameter<typeof customInstance>;
|
||||
},
|
||||
queryClient?: QueryClient
|
||||
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
|
||||
export function useListRenameSuggestions<
|
||||
TData = Awaited<ReturnType<typeof listRenameSuggestions>>,
|
||||
TError = unknown,
|
||||
>(
|
||||
binaryId: string,
|
||||
params?: ListRenameSuggestionsParams,
|
||||
options?: {
|
||||
query?: Partial<
|
||||
UseQueryOptions<Awaited<ReturnType<typeof listRenameSuggestions>>, TError, TData>
|
||||
>;
|
||||
request?: SecondParameter<typeof customInstance>;
|
||||
},
|
||||
queryClient?: QueryClient
|
||||
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
|
||||
/**
|
||||
* @summary List rename suggestions for a binary
|
||||
*/
|
||||
|
||||
export function useListRenameSuggestions<
|
||||
TData = Awaited<ReturnType<typeof listRenameSuggestions>>,
|
||||
TError = unknown,
|
||||
>(
|
||||
binaryId: string,
|
||||
params?: ListRenameSuggestionsParams,
|
||||
options?: {
|
||||
query?: Partial<
|
||||
UseQueryOptions<Awaited<ReturnType<typeof listRenameSuggestions>>, TError, TData>
|
||||
>;
|
||||
request?: SecondParameter<typeof customInstance>;
|
||||
},
|
||||
queryClient?: QueryClient
|
||||
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
|
||||
const queryOptions = getListRenameSuggestionsQueryOptions(binaryId, params, options);
|
||||
|
||||
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & {
|
||||
queryKey: DataTag<QueryKey, TData, TError>;
|
||||
};
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import * as React from "react"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||
import * as React from "react";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider
|
||||
const TooltipProvider = TooltipPrimitive.Provider;
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root
|
||||
const Tooltip = TooltipPrimitive.Root;
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
@ -17,12 +17,12 @@ const TooltipContent = React.forwardRef<
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-tooltip-content-transform-origin]",
|
||||
"bg-popover text-popover-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 origin-[--radix-tooltip-content-transform-origin] overflow-hidden rounded-md border px-3 py-1.5 text-sm shadow-md",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||
));
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { useState, useRef, useEffect, useMemo } from "react";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { Send, Bot, User, Loader2, Sparkles, Plus, Trash2 } from "lucide-react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
@ -6,11 +6,14 @@ import remarkGfm from "remark-gfm";
|
||||
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";
|
||||
import { RenameSuggestionCard } from "@/features/binary/RenameSuggestionCard";
|
||||
import { useListRenameSuggestions } from "@/api/generated/function/function.ts";
|
||||
import { analyzingMessages } from "@/features/binary/analyzingMessages";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import type { ComponentPropsWithoutRef } from "react";
|
||||
import type { RenameSuggestionResponse } from "@/api/generated/api.schemas";
|
||||
|
||||
const MAX_INPUT_LENGTH = 4000;
|
||||
|
||||
@ -64,10 +67,7 @@ const markdownComponents: ComponentPropsWithoutRef<typeof ReactMarkdown>["compon
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="max-w-full rounded-lg"
|
||||
style={{ background: "oklch(0.1 0.01 260)" }}
|
||||
>
|
||||
<div className="max-w-full rounded-lg" style={{ background: "oklch(0.1 0.01 260)" }}>
|
||||
<SyntaxHighlighter
|
||||
language={match[1]}
|
||||
style={codeBlockTheme}
|
||||
@ -89,7 +89,7 @@ function normalizeMarkdown(content: string) {
|
||||
return content.replace(/\r\n/g, "\n").replace(/([^\n])\n([^\n])/g, "$1 \n$2");
|
||||
}
|
||||
|
||||
export function AIChatPanel() {
|
||||
export function AIChatPanel({ onRenameApplied }: { onRenameApplied?: () => void }) {
|
||||
const {
|
||||
binary,
|
||||
sessions,
|
||||
@ -101,6 +101,8 @@ export function AIChatPanel() {
|
||||
streamingContent,
|
||||
toolStatus,
|
||||
isToolRunning,
|
||||
renameSuggestions,
|
||||
previousRenameSuggestions,
|
||||
streamError,
|
||||
isCreatingSession,
|
||||
createSession,
|
||||
@ -131,6 +133,32 @@ export function AIChatPanel() {
|
||||
|
||||
const analyzingMessage = analyzingMessages[analyzingIdx];
|
||||
|
||||
const { data: pendingSuggestions } = useListRenameSuggestions(
|
||||
binary?.id ?? "",
|
||||
{ status: "PENDING" },
|
||||
{ query: { enabled: !!binary?.id } }
|
||||
);
|
||||
|
||||
const mergedMessages = useMemo(() => {
|
||||
const items: Array<
|
||||
| { kind: "message"; data: (typeof messages)[number] }
|
||||
| { kind: "suggestion"; data: RenameSuggestionResponse }
|
||||
> = messages.map((m) => ({ kind: "message" as const, data: m }));
|
||||
|
||||
for (const s of pendingSuggestions ?? []) {
|
||||
if (!s.createdAt) continue;
|
||||
items.push({ kind: "suggestion" as const, data: s });
|
||||
}
|
||||
|
||||
items.sort((a, b) => {
|
||||
const aDate = a.kind === "message" ? a.data.createdAt : a.data.createdAt;
|
||||
const bDate = b.kind === "message" ? b.data.createdAt : b.data.createdAt;
|
||||
return (aDate ?? "").localeCompare(bDate ?? "");
|
||||
});
|
||||
|
||||
return items;
|
||||
}, [messages, pendingSuggestions]);
|
||||
|
||||
// Track manual scroll to respect user reading position
|
||||
useEffect(() => {
|
||||
const el = scrollContainerRef.current;
|
||||
@ -268,7 +296,7 @@ export function AIChatPanel() {
|
||||
ref={scrollContainerRef}
|
||||
className="flex min-h-0 flex-1 flex-col overflow-x-hidden overflow-y-auto"
|
||||
>
|
||||
{messages.length === 0 && !isStreaming ? (
|
||||
{messages.length === 0 && !pendingSuggestions?.length && !isStreaming ? (
|
||||
<div className="flex flex-1 flex-col items-center justify-center p-4">
|
||||
<Bot className="text-muted-foreground mx-auto mb-4 h-12 w-12" />
|
||||
<h3 className="text-foreground mb-2 font-medium">AI Analysis Assistant</h3>
|
||||
@ -300,12 +328,28 @@ export function AIChatPanel() {
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{messages.map((message, idx) => {
|
||||
{mergedMessages.map((item, idx) => {
|
||||
if (item.kind === "suggestion") {
|
||||
const s = item.data;
|
||||
if (!s.id || !s.functionId || !s.oldName || !s.newName) return null;
|
||||
return (
|
||||
<RenameSuggestionCard
|
||||
key={s.id}
|
||||
suggestionId={s.id}
|
||||
functionId={s.functionId}
|
||||
oldName={s.oldName}
|
||||
newName={s.newName}
|
||||
onRenameApplied={onRenameApplied}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const message = item.data;
|
||||
const isUser = message.role === "USER";
|
||||
|
||||
return (
|
||||
<div
|
||||
key={message.id ?? `streaming-${idx}`}
|
||||
key={message.id ?? `msg-${idx}`}
|
||||
className={cn("mb-4 flex gap-3", isUser && "flex-row-reverse")}
|
||||
>
|
||||
<div
|
||||
@ -354,6 +398,16 @@ export function AIChatPanel() {
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
{previousRenameSuggestions.map((s) => (
|
||||
<RenameSuggestionCard
|
||||
key={s.suggestionId}
|
||||
suggestionId={s.suggestionId}
|
||||
functionId={s.functionId}
|
||||
oldName={s.oldName}
|
||||
newName={s.newName}
|
||||
onRenameApplied={onRenameApplied}
|
||||
/>
|
||||
))}
|
||||
{isStreaming && (
|
||||
<>
|
||||
<div className="mb-4 flex gap-3">
|
||||
@ -388,6 +442,16 @@ export function AIChatPanel() {
|
||||
<span>{toolStatus}</span>
|
||||
</div>
|
||||
)}
|
||||
{renameSuggestions.map((s) => (
|
||||
<RenameSuggestionCard
|
||||
key={s.suggestionId}
|
||||
suggestionId={s.suggestionId}
|
||||
functionId={s.functionId}
|
||||
oldName={s.oldName}
|
||||
newName={s.newName}
|
||||
onRenameApplied={onRenameApplied}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
|
||||
@ -1,20 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { useRef, useCallback, useMemo, useState } from "react";
|
||||
import { Copy, Code, ArrowUpRight, ArrowDownRight, Link } from "lucide-react";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { Copy, Code, ArrowUpRight, ArrowDownRight, Link, Pencil, Check, X } from "lucide-react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import type { FunctionDetailResponse, CommentResponse } from "@/api/generated/api.schemas";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { CfgGraph } from "@/features/binary/CfgGraph";
|
||||
import { useGetCallees } from "@/api/generated/function/function.ts";
|
||||
import { useGetCallees, useRenameFunction } from "@/api/generated/function/function.ts";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface CodeViewerProps {
|
||||
func: FunctionDetailResponse | undefined;
|
||||
isLoading?: boolean;
|
||||
onSelectFunction: (id: string) => void;
|
||||
functionNameToId: Record<string, string>;
|
||||
onRenameApplied?: () => void;
|
||||
}
|
||||
|
||||
const FLAG_COLORS: Record<string, string> = {
|
||||
@ -59,7 +64,9 @@ export function CodeViewer({
|
||||
isLoading,
|
||||
onSelectFunction,
|
||||
functionNameToId,
|
||||
onRenameApplied,
|
||||
}: CodeViewerProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const assemblyRef = useRef<HTMLTableElement>(null);
|
||||
const pseudoRef = useRef<HTMLTableElement>(null);
|
||||
|
||||
@ -69,6 +76,21 @@ export function CodeViewer({
|
||||
end: string;
|
||||
} | null>(null);
|
||||
|
||||
const [isEditingName, setIsEditingName] = useState(false);
|
||||
const [editName, setEditName] = useState("");
|
||||
|
||||
const renameMutation = useRenameFunction({
|
||||
mutation: {
|
||||
onSuccess: () => {
|
||||
setIsEditingName(false);
|
||||
if (func?.id) {
|
||||
queryClient.invalidateQueries({ queryKey: [`/functions/${func.id}`] });
|
||||
}
|
||||
onRenameApplied?.();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const handleCfgHighlight = useCallback((start: string, end: string) => {
|
||||
setHighlightedRange((prev) =>
|
||||
prev?.start === start && prev?.end === end ? null : { start, end }
|
||||
@ -167,21 +189,109 @@ export function CodeViewer({
|
||||
)}
|
||||
</TabsList>
|
||||
|
||||
{func?.resolvedName && (
|
||||
<div className="flex flex-col items-start leading-tight">
|
||||
<span className="max-w-80 truncate font-mono text-xs font-semibold">
|
||||
{func.resolvedName}
|
||||
</span>
|
||||
{func.name && func.resolvedName !== func.name && (
|
||||
<span className="text-muted-foreground max-w-80 truncate font-mono text-[10px]">
|
||||
{func.name}
|
||||
</span>
|
||||
)}
|
||||
{isEditingName && func ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<Input
|
||||
value={editName}
|
||||
onChange={(e) => setEditName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
if (editName.trim() && editName.trim() !== (func.resolvedName || func.name)) {
|
||||
renameMutation.mutate({
|
||||
functionId: func.id ?? "",
|
||||
data: { name: editName.trim() },
|
||||
});
|
||||
} else {
|
||||
setIsEditingName(false);
|
||||
}
|
||||
} else if (e.key === "Escape") {
|
||||
setIsEditingName(false);
|
||||
}
|
||||
}}
|
||||
className="h-7 w-60 font-mono text-xs"
|
||||
autoFocus
|
||||
maxLength={500}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6"
|
||||
onClick={() => {
|
||||
if (editName.trim() && editName.trim() !== (func.resolvedName || func.name)) {
|
||||
renameMutation.mutate({
|
||||
functionId: func.id ?? "",
|
||||
data: { name: editName.trim() },
|
||||
});
|
||||
} else {
|
||||
setIsEditingName(false);
|
||||
}
|
||||
}}
|
||||
disabled={renameMutation.isPending}
|
||||
>
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6"
|
||||
onClick={() => setIsEditingName(false)}
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{!func?.resolvedName && func?.name && (
|
||||
<span className="font-mono text-xs">{func.name}</span>
|
||||
)}
|
||||
) : func?.resolvedName ? (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="flex flex-col items-start leading-tight">
|
||||
<span
|
||||
className={cn(
|
||||
"max-w-80 truncate font-mono text-xs font-semibold",
|
||||
func.renamed && "text-primary"
|
||||
)}
|
||||
>
|
||||
{func.resolvedName}
|
||||
</span>
|
||||
{func.renamed && func.originalName && (
|
||||
<span className="text-muted-foreground max-w-80 truncate font-mono text-[10px] line-through">
|
||||
{func.originalName}
|
||||
</span>
|
||||
)}
|
||||
{!func.renamed && func.name && func.resolvedName !== func.name && (
|
||||
<span className="text-muted-foreground max-w-80 truncate font-mono text-[10px]">
|
||||
{func.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-muted-foreground hover:text-foreground h-6 w-6"
|
||||
onClick={() => {
|
||||
setEditName(func.resolvedName || func.name || "");
|
||||
setIsEditingName(true);
|
||||
}}
|
||||
>
|
||||
<Pencil className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
) : func?.name ? (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className={cn("font-mono text-xs", func.renamed && "text-primary")}>
|
||||
{func.name}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-muted-foreground hover:text-foreground h-6 w-6"
|
||||
onClick={() => {
|
||||
setEditName(func.name || "");
|
||||
setIsEditingName(true);
|
||||
}}
|
||||
>
|
||||
<Pencil className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
@ -246,6 +356,36 @@ export function CodeViewer({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{func?.renameHistory && func.renameHistory.length > 0 && (
|
||||
<div className="border-border shrink-0 border-b px-4 py-1">
|
||||
<details>
|
||||
<summary className="text-muted-foreground cursor-pointer text-xs select-none">
|
||||
Rename History ({func.renameHistory.length})
|
||||
</summary>
|
||||
<div className="mt-1 flex max-h-40 flex-col gap-1 overflow-y-auto">
|
||||
{func.renameHistory.map((entry, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="even:bg-muted/30 flex items-center gap-2 px-1 py-0.5 font-mono text-xs"
|
||||
>
|
||||
<span className="text-muted-foreground line-through">{entry.oldName}</span>
|
||||
<span className="text-muted-foreground">→</span>
|
||||
<span>{entry.newName}</span>
|
||||
<span className="text-muted-foreground ml-auto shrink-0 text-[10px]">
|
||||
by {entry.source === "AI" ? "AI" : "you"}
|
||||
</span>
|
||||
{entry.createdAt && (
|
||||
<span className="text-muted-foreground shrink-0 text-[10px]">
|
||||
{formatDistanceToNow(entry.createdAt, { addSuffix: true })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<TabsContent value="assembly" className="mt-0 flex-1 overflow-hidden">
|
||||
{hasData ? (
|
||||
<ScrollArea className="h-full w-full">
|
||||
|
||||
@ -1,16 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { FunctionSquare, Search, Loader2 } from "lucide-react";
|
||||
import { FunctionSquare, Search, Loader2, Pencil } from "lucide-react";
|
||||
import type { FunctionSummaryResponse, SegmentResponse } from "@/api/generated/api.schemas";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
|
||||
interface FunctionTreeProps {
|
||||
functions: FunctionSummaryResponse[];
|
||||
@ -162,8 +158,8 @@ export function FunctionTree({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="min-h-0 flex-1 min-w-0">
|
||||
<div className="p-2 overflow-x-hidden">
|
||||
<ScrollArea className="min-h-0 min-w-0 flex-1">
|
||||
<div className="overflow-x-hidden p-2">
|
||||
{nameFiltered.map((func) => {
|
||||
const funcFlags = parseFlags(func.flags);
|
||||
const segName = func.address ? segmentByAddress[func.address.toLowerCase()] : undefined;
|
||||
@ -172,7 +168,7 @@ export function FunctionTree({
|
||||
key={func.id}
|
||||
onClick={() => onSelectFunction(func)}
|
||||
className={cn(
|
||||
"grid min-w-0 w-full items-center gap-1.5 rounded-md px-2 py-1 text-left transition-colors",
|
||||
"grid w-full min-w-0 items-center gap-1.5 rounded-md px-2 py-1 text-left transition-colors",
|
||||
"grid-cols-[auto_1fr]",
|
||||
selectedFunctionId === func.id
|
||||
? "bg-primary/10 text-primary"
|
||||
@ -185,26 +181,75 @@ export function FunctionTree({
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="min-w-0 flex-1 overflow-hidden">
|
||||
<span className="block truncate font-mono text-xs font-semibold">
|
||||
<span
|
||||
className={cn(
|
||||
"block truncate font-mono text-xs font-semibold",
|
||||
func.renamed && "text-primary"
|
||||
)}
|
||||
>
|
||||
{func.resolvedName}
|
||||
</span>
|
||||
<span className="text-muted-foreground block truncate font-mono text-[10px]">
|
||||
<span
|
||||
className={cn(
|
||||
"text-muted-foreground block truncate font-mono text-[10px]",
|
||||
func.renamed && "line-through"
|
||||
)}
|
||||
>
|
||||
{func.name}
|
||||
</span>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" className="max-w-none overflow-visible font-mono text-xs">
|
||||
<TooltipContent
|
||||
side="top"
|
||||
className="max-w-none overflow-visible font-mono text-xs"
|
||||
>
|
||||
<p>{func.resolvedName}</p>
|
||||
<p className="text-muted-foreground">{func.name}</p>
|
||||
{func.renamed && func.originalName && (
|
||||
<p className="text-muted-foreground mt-0.5">
|
||||
Originally: {func.originalName}
|
||||
</p>
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="min-w-0 flex-1 truncate font-mono text-xs">{func.name}</span>
|
||||
<span
|
||||
className={cn(
|
||||
"min-w-0 flex-1 truncate font-mono text-xs",
|
||||
func.renamed && "text-primary"
|
||||
)}
|
||||
>
|
||||
{func.name}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" className="max-w-none overflow-visible font-mono text-xs">
|
||||
<TooltipContent
|
||||
side="top"
|
||||
className="max-w-none overflow-visible font-mono text-xs"
|
||||
>
|
||||
<p>{func.name}</p>
|
||||
{func.renamed && func.originalName && (
|
||||
<p className="text-muted-foreground mt-0.5">
|
||||
Originally: {func.originalName}
|
||||
</p>
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{func.renamed && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Pencil className="text-primary h-3 w-3 shrink-0" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="top"
|
||||
className="max-w-none overflow-visible font-mono text-xs"
|
||||
>
|
||||
<p>Renamed</p>
|
||||
{func.originalName && (
|
||||
<p className="text-muted-foreground">Originally: {func.originalName}</p>
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
@ -215,7 +260,9 @@ export function FunctionTree({
|
||||
key={f}
|
||||
className={cn(
|
||||
"rounded px-1 font-mono text-[9px] leading-relaxed",
|
||||
selectedFlags.has(f) ? "bg-primary/15" : "bg-muted text-muted-foreground"
|
||||
selectedFlags.has(f)
|
||||
? "bg-primary/15"
|
||||
: "bg-muted text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{FLAG_LABELS[f] ?? f}
|
||||
@ -262,6 +309,6 @@ export function FunctionTree({
|
||||
? `${nameFiltered.length} of ${totalCount ?? functions.length} functions`
|
||||
: `${totalCount ?? functions.length} functions`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
126
src/features/binary/RenameSuggestionCard.tsx
Normal file
126
src/features/binary/RenameSuggestionCard.tsx
Normal file
@ -0,0 +1,126 @@
|
||||
import { useState } from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { Check, X, Sparkles, Loader2 } from "lucide-react";
|
||||
import { useApproveRename, useRejectRename } from "@/api/generated/function/function.ts";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type SuggestionStatus = "PENDING" | "APPROVED" | "REJECTED";
|
||||
|
||||
interface RenameSuggestionCardProps {
|
||||
suggestionId: string;
|
||||
functionId: string;
|
||||
oldName: string;
|
||||
newName: string;
|
||||
onRenameApplied?: () => void;
|
||||
}
|
||||
|
||||
export function RenameSuggestionCard({
|
||||
suggestionId,
|
||||
functionId,
|
||||
oldName,
|
||||
newName,
|
||||
onRenameApplied,
|
||||
}: RenameSuggestionCardProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const [status, setStatus] = useState<SuggestionStatus>("PENDING");
|
||||
|
||||
const approveMutation = useApproveRename({
|
||||
mutation: {
|
||||
onSuccess: () => {
|
||||
setStatus("APPROVED");
|
||||
queryClient.invalidateQueries({ queryKey: [`/functions/${functionId}`] });
|
||||
onRenameApplied?.();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const rejectMutation = useRejectRename({
|
||||
mutation: {
|
||||
onSuccess: () => {
|
||||
setStatus("REJECTED");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const handleApprove = () => {
|
||||
if (approveMutation.isPending || status !== "PENDING") return;
|
||||
approveMutation.mutate({ renameId: suggestionId });
|
||||
};
|
||||
|
||||
const handleReject = () => {
|
||||
if (rejectMutation.isPending || status !== "PENDING") return;
|
||||
rejectMutation.mutate({ renameId: suggestionId });
|
||||
};
|
||||
|
||||
const isPending = status === "PENDING";
|
||||
const isApproved = status === "APPROVED";
|
||||
const isRejected = status === "REJECTED";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"border-l-primary/60 bg-muted/60 my-3 rounded-lg border-l-[3px] px-3 py-2.5 text-sm",
|
||||
isApproved && "bg-muted/40 border-l-(--success)",
|
||||
isRejected && "border-l-muted-foreground/40 bg-muted/30"
|
||||
)}
|
||||
>
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<Sparkles className="text-primary h-4 w-4 shrink-0" />
|
||||
<span className="text-foreground text-xs font-medium">AI suggests renaming:</span>
|
||||
</div>
|
||||
|
||||
<div className="mb-3 font-mono text-sm">
|
||||
<span className="text-muted-foreground line-through">{oldName}</span>
|
||||
<span className="text-muted-foreground mx-1.5">→</span>
|
||||
<span
|
||||
className={cn(
|
||||
"font-semibold",
|
||||
isApproved && "text-(--success)",
|
||||
isRejected && "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{newName}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{isApproved && (
|
||||
<p className="mb-2 flex items-center gap-1 text-xs text-(--success)">
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
Approved
|
||||
</p>
|
||||
)}
|
||||
{isRejected && (
|
||||
<p className="text-muted-foreground mb-2 flex items-center gap-1 text-xs">
|
||||
<X className="h-3.5 w-3.5" />
|
||||
Rejected
|
||||
</p>
|
||||
)}
|
||||
|
||||
{isPending && (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="h-7 text-xs"
|
||||
onClick={handleApprove}
|
||||
disabled={approveMutation.isPending}
|
||||
>
|
||||
{approveMutation.isPending && <Loader2 className="mr-1 h-3 w-3 animate-spin" />}
|
||||
Approve
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-7 text-xs"
|
||||
onClick={handleReject}
|
||||
disabled={rejectMutation.isPending}
|
||||
>
|
||||
{rejectMutation.isPending && <Loader2 className="mr-1 h-3 w-3 animate-spin" />}
|
||||
Reject
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -32,6 +32,21 @@ interface ThinkingEvent {
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface RenameSuggestionEvent {
|
||||
type: "rename_suggestion";
|
||||
suggestionId: string;
|
||||
functionId: string;
|
||||
oldName: string;
|
||||
newName: string;
|
||||
}
|
||||
|
||||
interface FunctionRenamedEvent {
|
||||
type: "function_renamed";
|
||||
functionId: string;
|
||||
oldName: string;
|
||||
newName: string;
|
||||
}
|
||||
|
||||
interface DoneEvent {
|
||||
type: "done";
|
||||
sessionId: string;
|
||||
@ -47,6 +62,8 @@ type SSEEvent =
|
||||
| ToolCallStartEvent
|
||||
| ToolCallEndEvent
|
||||
| ThinkingEvent
|
||||
| RenameSuggestionEvent
|
||||
| FunctionRenamedEvent
|
||||
| DoneEvent
|
||||
| ErrorEvent;
|
||||
|
||||
@ -62,6 +79,10 @@ export function useAIChat() {
|
||||
const [streamError, setStreamError] = useState<string | null>(null);
|
||||
const [toolStatus, setToolStatus] = useState<string | null>(null);
|
||||
const [isToolRunning, setIsToolRunning] = useState(false);
|
||||
const [renameSuggestions, setRenameSuggestions] = useState<RenameSuggestionEvent[]>([]);
|
||||
const [previousRenameSuggestions, setPreviousRenameSuggestions] = useState<
|
||||
RenameSuggestionEvent[]
|
||||
>([]);
|
||||
const [pendingUserMessage, setPendingUserMessage] = useState<MessageResponse | null>(null);
|
||||
const [optimisticAssistantMessage, setOptimisticAssistantMessage] =
|
||||
useState<MessageResponse | null>(null);
|
||||
@ -104,6 +125,15 @@ export function useAIChat() {
|
||||
|
||||
const createSession = useCallback(
|
||||
(model?: string) => {
|
||||
setStreamingContent("");
|
||||
setIsStreaming(false);
|
||||
setStreamError(null);
|
||||
setToolStatus(null);
|
||||
setIsToolRunning(false);
|
||||
setRenameSuggestions([]);
|
||||
setPreviousRenameSuggestions([]);
|
||||
setPendingUserMessage(null);
|
||||
setOptimisticAssistantMessage(null);
|
||||
createSessionMutation.mutate({
|
||||
binaryId,
|
||||
params: {
|
||||
@ -116,8 +146,16 @@ export function useAIChat() {
|
||||
);
|
||||
|
||||
const selectSession = useCallback((sessionId: string) => {
|
||||
setActiveSessionId(sessionId);
|
||||
setStreamingContent("");
|
||||
setIsStreaming(false);
|
||||
setStreamError(null);
|
||||
setToolStatus(null);
|
||||
setIsToolRunning(false);
|
||||
setRenameSuggestions([]);
|
||||
setPreviousRenameSuggestions([]);
|
||||
setPendingUserMessage(null);
|
||||
setOptimisticAssistantMessage(null);
|
||||
setActiveSessionId(sessionId);
|
||||
}, []);
|
||||
|
||||
const deleteSession = useCallback(
|
||||
@ -135,6 +173,10 @@ export function useAIChat() {
|
||||
setStreamingContent("");
|
||||
setToolStatus(null);
|
||||
setIsToolRunning(false);
|
||||
setRenameSuggestions((current) => {
|
||||
setPreviousRenameSuggestions((prev) => [...prev, ...current]);
|
||||
return [];
|
||||
});
|
||||
setOptimisticAssistantMessage(null);
|
||||
setStreamError(null);
|
||||
setPendingUserMessage({
|
||||
@ -206,6 +248,15 @@ export function useAIChat() {
|
||||
setToolStatus(sseEvent.message);
|
||||
setIsToolRunning(true);
|
||||
break;
|
||||
case "rename_suggestion":
|
||||
setRenameSuggestions((prev) => [...prev, sseEvent]);
|
||||
break;
|
||||
case "function_renamed":
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [`/functions/${sseEvent.functionId}`],
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ["functions"] });
|
||||
break;
|
||||
case "done":
|
||||
setOptimisticAssistantMessage({
|
||||
role: "ASSISTANT",
|
||||
@ -216,12 +267,19 @@ export function useAIChat() {
|
||||
setStreamingContent("");
|
||||
setToolStatus(null);
|
||||
setIsToolRunning(false);
|
||||
setRenameSuggestions((current) => {
|
||||
setPreviousRenameSuggestions((prev) => [...prev, ...current]);
|
||||
return [];
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [`/chat/sessions/${activeSessionId}/messages`],
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [`/binaries/${binaryId}/chat/sessions`],
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [`/binaries/${binaryId}/rename-suggestions`],
|
||||
});
|
||||
break;
|
||||
case "error":
|
||||
setIsStreaming(false);
|
||||
@ -263,6 +321,15 @@ export function useAIChat() {
|
||||
setToolStatus(sseEvent.message);
|
||||
setIsToolRunning(true);
|
||||
break;
|
||||
case "rename_suggestion":
|
||||
setRenameSuggestions((prev) => [...prev, sseEvent]);
|
||||
break;
|
||||
case "function_renamed":
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [`/functions/${sseEvent.functionId}`],
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ["functions"] });
|
||||
break;
|
||||
case "done":
|
||||
setOptimisticAssistantMessage({
|
||||
role: "ASSISTANT",
|
||||
@ -273,12 +340,19 @@ export function useAIChat() {
|
||||
setStreamingContent("");
|
||||
setToolStatus(null);
|
||||
setIsToolRunning(false);
|
||||
setRenameSuggestions((current) => {
|
||||
setPreviousRenameSuggestions((prev) => [...prev, ...current]);
|
||||
return [];
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [`/chat/sessions/${activeSessionId}/messages`],
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [`/binaries/${binaryId}/chat/sessions`],
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [`/binaries/${binaryId}/rename-suggestions`],
|
||||
});
|
||||
break;
|
||||
case "error":
|
||||
setIsStreaming(false);
|
||||
@ -354,6 +428,8 @@ export function useAIChat() {
|
||||
streamingContent,
|
||||
toolStatus,
|
||||
isToolRunning,
|
||||
renameSuggestions,
|
||||
previousRenameSuggestions,
|
||||
streamError: userFriendlyError,
|
||||
isCreatingSession: createSessionMutation.isPending,
|
||||
createSession,
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import { useState, useCallback } from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { ArrowLeft, ChevronDown } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { EmptyState } from "@/components/EmptyState";
|
||||
@ -81,6 +82,15 @@ const Binary = () => {
|
||||
analysisId,
|
||||
} = useBinaryPage();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const handleRenameApplied = useCallback(() => {
|
||||
queryClient.invalidateQueries({ queryKey: ["functions", analysisId] });
|
||||
if (selectedFunctionId) {
|
||||
queryClient.invalidateQueries({ queryKey: [`/functions/${selectedFunctionId}`] });
|
||||
}
|
||||
}, [queryClient, analysisId, selectedFunctionId]);
|
||||
|
||||
const navigateToEntryPoint = () => {
|
||||
if (!analysis?.entryPoint) return;
|
||||
const match = analysis.entryPoint.match(/^(0x[0-9a-fA-F]+)/);
|
||||
@ -313,6 +323,7 @@ const Binary = () => {
|
||||
isLoading={selectedFunctionLoading}
|
||||
onSelectFunction={setSelectedFunctionId}
|
||||
functionNameToId={functionNameToId}
|
||||
onRenameApplied={handleRenameApplied}
|
||||
/>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
@ -321,7 +332,7 @@ const Binary = () => {
|
||||
|
||||
<ResizablePanel id="chat-panel" defaultSize="30%" minSize="10%" maxSize="40%">
|
||||
<div className="border-border bg-card h-full overflow-hidden border-l">
|
||||
<AIChatPanel />
|
||||
<AIChatPanel onRenameApplied={handleRenameApplied} />
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user