feat(rename): implement rename suggestion handling
This commit is contained in:
parent
979f804623
commit
72c3078912
@ -35,6 +35,20 @@ export interface ProjectResponse {
|
|||||||
binaryCount?: number;
|
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 {
|
export interface BinaryDependencyResponse {
|
||||||
id: string;
|
id: string;
|
||||||
/** @minLength 1 */
|
/** @minLength 1 */
|
||||||
@ -103,6 +117,26 @@ export interface JobResponse {
|
|||||||
updatedAt: string;
|
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 {
|
export interface SessionResponse {
|
||||||
id?: string;
|
id?: string;
|
||||||
binaryId?: string;
|
binaryId?: string;
|
||||||
@ -149,8 +183,8 @@ export interface PageableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface PageLibrarySymbolResponse {
|
export interface PageLibrarySymbolResponse {
|
||||||
totalElements?: number;
|
|
||||||
totalPages?: number;
|
totalPages?: number;
|
||||||
|
totalElements?: number;
|
||||||
size?: number;
|
size?: number;
|
||||||
content?: LibrarySymbolResponse[];
|
content?: LibrarySymbolResponse[];
|
||||||
number?: number;
|
number?: number;
|
||||||
@ -197,6 +231,13 @@ export interface LabelResponse {
|
|||||||
type?: string;
|
type?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RenameHistoryEntry {
|
||||||
|
oldName?: string;
|
||||||
|
newName?: string;
|
||||||
|
source?: string;
|
||||||
|
createdAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface FunctionDetailResponse {
|
export interface FunctionDetailResponse {
|
||||||
id?: string;
|
id?: string;
|
||||||
analysisId?: string;
|
analysisId?: string;
|
||||||
@ -214,6 +255,9 @@ export interface FunctionDetailResponse {
|
|||||||
stackFrame?: FrameMemberResponse[];
|
stackFrame?: FrameMemberResponse[];
|
||||||
comments?: CommentResponse[];
|
comments?: CommentResponse[];
|
||||||
cfgBlocks?: CfgBlockResponse[];
|
cfgBlocks?: CfgBlockResponse[];
|
||||||
|
renamed?: boolean;
|
||||||
|
originalName?: string;
|
||||||
|
renameHistory?: RenameHistoryEntry[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface XrefResponse {
|
export interface XrefResponse {
|
||||||
@ -312,19 +356,9 @@ export interface DataLabelResponse {
|
|||||||
dataXrefs?: DataXrefResponse[];
|
dataXrefs?: DataXrefResponse[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FunctionSummaryResponse {
|
|
||||||
id?: string;
|
|
||||||
analysisId?: string;
|
|
||||||
name?: string;
|
|
||||||
resolvedName?: string;
|
|
||||||
address?: string;
|
|
||||||
size?: number;
|
|
||||||
flags?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PageFunctionSummaryResponse {
|
export interface PageFunctionSummaryResponse {
|
||||||
totalElements?: number;
|
|
||||||
totalPages?: number;
|
totalPages?: number;
|
||||||
|
totalElements?: number;
|
||||||
size?: number;
|
size?: number;
|
||||||
content?: FunctionSummaryResponse[];
|
content?: FunctionSummaryResponse[];
|
||||||
number?: number;
|
number?: number;
|
||||||
@ -470,6 +504,10 @@ export const GetJobsType = {
|
|||||||
GENERATE_EMBEDDINGS: "GENERATE_EMBEDDINGS",
|
GENERATE_EMBEDDINGS: "GENERATE_EMBEDDINGS",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export type ListRenameSuggestionsParams = {
|
||||||
|
status?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type ListFunctionsParams = {
|
export type ListFunctionsParams = {
|
||||||
pageable: Pageable;
|
pageable: Pageable;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,25 +4,272 @@
|
|||||||
* OpenAPI definition
|
* OpenAPI definition
|
||||||
* OpenAPI spec version: v0
|
* OpenAPI spec version: v0
|
||||||
*/
|
*/
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import type {
|
import type {
|
||||||
DataTag,
|
DataTag,
|
||||||
DefinedInitialDataOptions,
|
DefinedInitialDataOptions,
|
||||||
DefinedUseQueryResult,
|
DefinedUseQueryResult,
|
||||||
|
MutationFunction,
|
||||||
QueryClient,
|
QueryClient,
|
||||||
QueryFunction,
|
QueryFunction,
|
||||||
QueryKey,
|
QueryKey,
|
||||||
UndefinedInitialDataOptions,
|
UndefinedInitialDataOptions,
|
||||||
|
UseMutationOptions,
|
||||||
|
UseMutationResult,
|
||||||
UseQueryOptions,
|
UseQueryOptions,
|
||||||
UseQueryResult,
|
UseQueryResult,
|
||||||
} from "@tanstack/react-query";
|
} 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";
|
import { customInstance } from "../../api";
|
||||||
|
|
||||||
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
|
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.
|
* Retrieve full details of a function including assembly, decompiled code, and xref counts.
|
||||||
* @summary Get function details
|
* @summary Get function details
|
||||||
@ -352,3 +599,151 @@ export function useGetCallees<TData = Awaited<ReturnType<typeof getCallees>>, TE
|
|||||||
|
|
||||||
return { ...query, queryKey: queryOptions.queryKey };
|
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 React from "react";
|
||||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
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<
|
const TooltipContent = React.forwardRef<
|
||||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||||
@ -17,12 +17,12 @@ const TooltipContent = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...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 { formatDistanceToNow } from "date-fns";
|
||||||
import { Send, Bot, User, Loader2, Sparkles, Plus, Trash2 } from "lucide-react";
|
import { Send, Bot, User, Loader2, Sparkles, Plus, Trash2 } from "lucide-react";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
@ -6,11 +6,14 @@ import remarkGfm from "remark-gfm";
|
|||||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||||
import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
|
import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||||
import { useAIChat } from "@/features/binary/hooks/useAIChat";
|
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 { analyzingMessages } from "@/features/binary/analyzingMessages";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import type { ComponentPropsWithoutRef } from "react";
|
import type { ComponentPropsWithoutRef } from "react";
|
||||||
|
import type { RenameSuggestionResponse } from "@/api/generated/api.schemas";
|
||||||
|
|
||||||
const MAX_INPUT_LENGTH = 4000;
|
const MAX_INPUT_LENGTH = 4000;
|
||||||
|
|
||||||
@ -64,10 +67,7 @@ const markdownComponents: ComponentPropsWithoutRef<typeof ReactMarkdown>["compon
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="max-w-full rounded-lg" style={{ background: "oklch(0.1 0.01 260)" }}>
|
||||||
className="max-w-full rounded-lg"
|
|
||||||
style={{ background: "oklch(0.1 0.01 260)" }}
|
|
||||||
>
|
|
||||||
<SyntaxHighlighter
|
<SyntaxHighlighter
|
||||||
language={match[1]}
|
language={match[1]}
|
||||||
style={codeBlockTheme}
|
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");
|
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 {
|
const {
|
||||||
binary,
|
binary,
|
||||||
sessions,
|
sessions,
|
||||||
@ -101,6 +101,8 @@ export function AIChatPanel() {
|
|||||||
streamingContent,
|
streamingContent,
|
||||||
toolStatus,
|
toolStatus,
|
||||||
isToolRunning,
|
isToolRunning,
|
||||||
|
renameSuggestions,
|
||||||
|
previousRenameSuggestions,
|
||||||
streamError,
|
streamError,
|
||||||
isCreatingSession,
|
isCreatingSession,
|
||||||
createSession,
|
createSession,
|
||||||
@ -131,6 +133,32 @@ export function AIChatPanel() {
|
|||||||
|
|
||||||
const analyzingMessage = analyzingMessages[analyzingIdx];
|
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
|
// Track manual scroll to respect user reading position
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const el = scrollContainerRef.current;
|
const el = scrollContainerRef.current;
|
||||||
@ -268,7 +296,7 @@ export function AIChatPanel() {
|
|||||||
ref={scrollContainerRef}
|
ref={scrollContainerRef}
|
||||||
className="flex min-h-0 flex-1 flex-col overflow-x-hidden overflow-y-auto"
|
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">
|
<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" />
|
<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>
|
<h3 className="text-foreground mb-2 font-medium">AI Analysis Assistant</h3>
|
||||||
@ -300,12 +328,28 @@ export function AIChatPanel() {
|
|||||||
</div>
|
</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";
|
const isUser = message.role === "USER";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={message.id ?? `streaming-${idx}`}
|
key={message.id ?? `msg-${idx}`}
|
||||||
className={cn("mb-4 flex gap-3", isUser && "flex-row-reverse")}
|
className={cn("mb-4 flex gap-3", isUser && "flex-row-reverse")}
|
||||||
>
|
>
|
||||||
<div
|
<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 && (
|
{isStreaming && (
|
||||||
<>
|
<>
|
||||||
<div className="mb-4 flex gap-3">
|
<div className="mb-4 flex gap-3">
|
||||||
@ -388,6 +442,16 @@ export function AIChatPanel() {
|
|||||||
<span>{toolStatus}</span>
|
<span>{toolStatus}</span>
|
||||||
</div>
|
</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} />
|
<div ref={messagesEndRef} />
|
||||||
|
|||||||
@ -1,20 +1,25 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useRef, useCallback, useMemo, useState } from "react";
|
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 type { FunctionDetailResponse, CommentResponse } from "@/api/generated/api.schemas";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
import { CfgGraph } from "@/features/binary/CfgGraph";
|
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 {
|
interface CodeViewerProps {
|
||||||
func: FunctionDetailResponse | undefined;
|
func: FunctionDetailResponse | undefined;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
onSelectFunction: (id: string) => void;
|
onSelectFunction: (id: string) => void;
|
||||||
functionNameToId: Record<string, string>;
|
functionNameToId: Record<string, string>;
|
||||||
|
onRenameApplied?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FLAG_COLORS: Record<string, string> = {
|
const FLAG_COLORS: Record<string, string> = {
|
||||||
@ -59,7 +64,9 @@ export function CodeViewer({
|
|||||||
isLoading,
|
isLoading,
|
||||||
onSelectFunction,
|
onSelectFunction,
|
||||||
functionNameToId,
|
functionNameToId,
|
||||||
|
onRenameApplied,
|
||||||
}: CodeViewerProps) {
|
}: CodeViewerProps) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const assemblyRef = useRef<HTMLTableElement>(null);
|
const assemblyRef = useRef<HTMLTableElement>(null);
|
||||||
const pseudoRef = useRef<HTMLTableElement>(null);
|
const pseudoRef = useRef<HTMLTableElement>(null);
|
||||||
|
|
||||||
@ -69,6 +76,21 @@ export function CodeViewer({
|
|||||||
end: string;
|
end: string;
|
||||||
} | null>(null);
|
} | 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) => {
|
const handleCfgHighlight = useCallback((start: string, end: string) => {
|
||||||
setHighlightedRange((prev) =>
|
setHighlightedRange((prev) =>
|
||||||
prev?.start === start && prev?.end === end ? null : { start, end }
|
prev?.start === start && prev?.end === end ? null : { start, end }
|
||||||
@ -167,21 +189,109 @@ export function CodeViewer({
|
|||||||
)}
|
)}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
{func?.resolvedName && (
|
{isEditingName && func ? (
|
||||||
<div className="flex flex-col items-start leading-tight">
|
<div className="flex items-center gap-1">
|
||||||
<span className="max-w-80 truncate font-mono text-xs font-semibold">
|
<Input
|
||||||
{func.resolvedName}
|
value={editName}
|
||||||
</span>
|
onChange={(e) => setEditName(e.target.value)}
|
||||||
{func.name && func.resolvedName !== func.name && (
|
onKeyDown={(e) => {
|
||||||
<span className="text-muted-foreground max-w-80 truncate font-mono text-[10px]">
|
if (e.key === "Enter") {
|
||||||
{func.name}
|
e.preventDefault();
|
||||||
</span>
|
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>
|
</div>
|
||||||
)}
|
) : func?.resolvedName ? (
|
||||||
{!func?.resolvedName && func?.name && (
|
<div className="flex items-center gap-1.5">
|
||||||
<span className="font-mono text-xs">{func.name}</span>
|
<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>
|
||||||
|
|
||||||
<div className="flex shrink-0 items-center gap-2">
|
<div className="flex shrink-0 items-center gap-2">
|
||||||
@ -246,6 +356,36 @@ export function CodeViewer({
|
|||||||
</div>
|
</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">
|
<TabsContent value="assembly" className="mt-0 flex-1 overflow-hidden">
|
||||||
{hasData ? (
|
{hasData ? (
|
||||||
<ScrollArea className="h-full w-full">
|
<ScrollArea className="h-full w-full">
|
||||||
|
|||||||
@ -1,16 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useRef, useEffect } from "react";
|
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 type { FunctionSummaryResponse, SegmentResponse } from "@/api/generated/api.schemas";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import {
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
|
|
||||||
interface FunctionTreeProps {
|
interface FunctionTreeProps {
|
||||||
functions: FunctionSummaryResponse[];
|
functions: FunctionSummaryResponse[];
|
||||||
@ -162,8 +158,8 @@ export function FunctionTree({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ScrollArea className="min-h-0 flex-1 min-w-0">
|
<ScrollArea className="min-h-0 min-w-0 flex-1">
|
||||||
<div className="p-2 overflow-x-hidden">
|
<div className="overflow-x-hidden p-2">
|
||||||
{nameFiltered.map((func) => {
|
{nameFiltered.map((func) => {
|
||||||
const funcFlags = parseFlags(func.flags);
|
const funcFlags = parseFlags(func.flags);
|
||||||
const segName = func.address ? segmentByAddress[func.address.toLowerCase()] : undefined;
|
const segName = func.address ? segmentByAddress[func.address.toLowerCase()] : undefined;
|
||||||
@ -172,7 +168,7 @@ export function FunctionTree({
|
|||||||
key={func.id}
|
key={func.id}
|
||||||
onClick={() => onSelectFunction(func)}
|
onClick={() => onSelectFunction(func)}
|
||||||
className={cn(
|
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]",
|
"grid-cols-[auto_1fr]",
|
||||||
selectedFunctionId === func.id
|
selectedFunctionId === func.id
|
||||||
? "bg-primary/10 text-primary"
|
? "bg-primary/10 text-primary"
|
||||||
@ -185,26 +181,75 @@ export function FunctionTree({
|
|||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<span className="min-w-0 flex-1 overflow-hidden">
|
<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}
|
{func.resolvedName}
|
||||||
</span>
|
</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}
|
{func.name}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</TooltipTrigger>
|
</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>{func.resolvedName}</p>
|
||||||
<p className="text-muted-foreground">{func.name}</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>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : (
|
) : (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<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>
|
</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>
|
<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>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
@ -215,7 +260,9 @@ export function FunctionTree({
|
|||||||
key={f}
|
key={f}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded px-1 font-mono text-[9px] leading-relaxed",
|
"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}
|
{FLAG_LABELS[f] ?? f}
|
||||||
@ -262,6 +309,6 @@ export function FunctionTree({
|
|||||||
? `${nameFiltered.length} of ${totalCount ?? functions.length} functions`
|
? `${nameFiltered.length} of ${totalCount ?? functions.length} functions`
|
||||||
: `${totalCount ?? functions.length} functions`}
|
: `${totalCount ?? functions.length} functions`}
|
||||||
</div>
|
</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;
|
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 {
|
interface DoneEvent {
|
||||||
type: "done";
|
type: "done";
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
@ -47,6 +62,8 @@ type SSEEvent =
|
|||||||
| ToolCallStartEvent
|
| ToolCallStartEvent
|
||||||
| ToolCallEndEvent
|
| ToolCallEndEvent
|
||||||
| ThinkingEvent
|
| ThinkingEvent
|
||||||
|
| RenameSuggestionEvent
|
||||||
|
| FunctionRenamedEvent
|
||||||
| DoneEvent
|
| DoneEvent
|
||||||
| ErrorEvent;
|
| ErrorEvent;
|
||||||
|
|
||||||
@ -62,6 +79,10 @@ export function useAIChat() {
|
|||||||
const [streamError, setStreamError] = useState<string | null>(null);
|
const [streamError, setStreamError] = useState<string | null>(null);
|
||||||
const [toolStatus, setToolStatus] = useState<string | null>(null);
|
const [toolStatus, setToolStatus] = useState<string | null>(null);
|
||||||
const [isToolRunning, setIsToolRunning] = useState(false);
|
const [isToolRunning, setIsToolRunning] = useState(false);
|
||||||
|
const [renameSuggestions, setRenameSuggestions] = useState<RenameSuggestionEvent[]>([]);
|
||||||
|
const [previousRenameSuggestions, setPreviousRenameSuggestions] = useState<
|
||||||
|
RenameSuggestionEvent[]
|
||||||
|
>([]);
|
||||||
const [pendingUserMessage, setPendingUserMessage] = useState<MessageResponse | null>(null);
|
const [pendingUserMessage, setPendingUserMessage] = useState<MessageResponse | null>(null);
|
||||||
const [optimisticAssistantMessage, setOptimisticAssistantMessage] =
|
const [optimisticAssistantMessage, setOptimisticAssistantMessage] =
|
||||||
useState<MessageResponse | null>(null);
|
useState<MessageResponse | null>(null);
|
||||||
@ -104,6 +125,15 @@ export function useAIChat() {
|
|||||||
|
|
||||||
const createSession = useCallback(
|
const createSession = useCallback(
|
||||||
(model?: string) => {
|
(model?: string) => {
|
||||||
|
setStreamingContent("");
|
||||||
|
setIsStreaming(false);
|
||||||
|
setStreamError(null);
|
||||||
|
setToolStatus(null);
|
||||||
|
setIsToolRunning(false);
|
||||||
|
setRenameSuggestions([]);
|
||||||
|
setPreviousRenameSuggestions([]);
|
||||||
|
setPendingUserMessage(null);
|
||||||
|
setOptimisticAssistantMessage(null);
|
||||||
createSessionMutation.mutate({
|
createSessionMutation.mutate({
|
||||||
binaryId,
|
binaryId,
|
||||||
params: {
|
params: {
|
||||||
@ -116,8 +146,16 @@ export function useAIChat() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const selectSession = useCallback((sessionId: string) => {
|
const selectSession = useCallback((sessionId: string) => {
|
||||||
setActiveSessionId(sessionId);
|
setStreamingContent("");
|
||||||
|
setIsStreaming(false);
|
||||||
setStreamError(null);
|
setStreamError(null);
|
||||||
|
setToolStatus(null);
|
||||||
|
setIsToolRunning(false);
|
||||||
|
setRenameSuggestions([]);
|
||||||
|
setPreviousRenameSuggestions([]);
|
||||||
|
setPendingUserMessage(null);
|
||||||
|
setOptimisticAssistantMessage(null);
|
||||||
|
setActiveSessionId(sessionId);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const deleteSession = useCallback(
|
const deleteSession = useCallback(
|
||||||
@ -135,6 +173,10 @@ export function useAIChat() {
|
|||||||
setStreamingContent("");
|
setStreamingContent("");
|
||||||
setToolStatus(null);
|
setToolStatus(null);
|
||||||
setIsToolRunning(false);
|
setIsToolRunning(false);
|
||||||
|
setRenameSuggestions((current) => {
|
||||||
|
setPreviousRenameSuggestions((prev) => [...prev, ...current]);
|
||||||
|
return [];
|
||||||
|
});
|
||||||
setOptimisticAssistantMessage(null);
|
setOptimisticAssistantMessage(null);
|
||||||
setStreamError(null);
|
setStreamError(null);
|
||||||
setPendingUserMessage({
|
setPendingUserMessage({
|
||||||
@ -206,6 +248,15 @@ export function useAIChat() {
|
|||||||
setToolStatus(sseEvent.message);
|
setToolStatus(sseEvent.message);
|
||||||
setIsToolRunning(true);
|
setIsToolRunning(true);
|
||||||
break;
|
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":
|
case "done":
|
||||||
setOptimisticAssistantMessage({
|
setOptimisticAssistantMessage({
|
||||||
role: "ASSISTANT",
|
role: "ASSISTANT",
|
||||||
@ -216,12 +267,19 @@ export function useAIChat() {
|
|||||||
setStreamingContent("");
|
setStreamingContent("");
|
||||||
setToolStatus(null);
|
setToolStatus(null);
|
||||||
setIsToolRunning(false);
|
setIsToolRunning(false);
|
||||||
|
setRenameSuggestions((current) => {
|
||||||
|
setPreviousRenameSuggestions((prev) => [...prev, ...current]);
|
||||||
|
return [];
|
||||||
|
});
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: [`/chat/sessions/${activeSessionId}/messages`],
|
queryKey: [`/chat/sessions/${activeSessionId}/messages`],
|
||||||
});
|
});
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: [`/binaries/${binaryId}/chat/sessions`],
|
queryKey: [`/binaries/${binaryId}/chat/sessions`],
|
||||||
});
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: [`/binaries/${binaryId}/rename-suggestions`],
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "error":
|
case "error":
|
||||||
setIsStreaming(false);
|
setIsStreaming(false);
|
||||||
@ -263,6 +321,15 @@ export function useAIChat() {
|
|||||||
setToolStatus(sseEvent.message);
|
setToolStatus(sseEvent.message);
|
||||||
setIsToolRunning(true);
|
setIsToolRunning(true);
|
||||||
break;
|
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":
|
case "done":
|
||||||
setOptimisticAssistantMessage({
|
setOptimisticAssistantMessage({
|
||||||
role: "ASSISTANT",
|
role: "ASSISTANT",
|
||||||
@ -273,12 +340,19 @@ export function useAIChat() {
|
|||||||
setStreamingContent("");
|
setStreamingContent("");
|
||||||
setToolStatus(null);
|
setToolStatus(null);
|
||||||
setIsToolRunning(false);
|
setIsToolRunning(false);
|
||||||
|
setRenameSuggestions((current) => {
|
||||||
|
setPreviousRenameSuggestions((prev) => [...prev, ...current]);
|
||||||
|
return [];
|
||||||
|
});
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: [`/chat/sessions/${activeSessionId}/messages`],
|
queryKey: [`/chat/sessions/${activeSessionId}/messages`],
|
||||||
});
|
});
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: [`/binaries/${binaryId}/chat/sessions`],
|
queryKey: [`/binaries/${binaryId}/chat/sessions`],
|
||||||
});
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: [`/binaries/${binaryId}/rename-suggestions`],
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "error":
|
case "error":
|
||||||
setIsStreaming(false);
|
setIsStreaming(false);
|
||||||
@ -354,6 +428,8 @@ export function useAIChat() {
|
|||||||
streamingContent,
|
streamingContent,
|
||||||
toolStatus,
|
toolStatus,
|
||||||
isToolRunning,
|
isToolRunning,
|
||||||
|
renameSuggestions,
|
||||||
|
previousRenameSuggestions,
|
||||||
streamError: userFriendlyError,
|
streamError: userFriendlyError,
|
||||||
isCreatingSession: createSessionMutation.isPending,
|
isCreatingSession: createSessionMutation.isPending,
|
||||||
createSession,
|
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 { ArrowLeft, ChevronDown } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { EmptyState } from "@/components/EmptyState";
|
import { EmptyState } from "@/components/EmptyState";
|
||||||
@ -81,6 +82,15 @@ const Binary = () => {
|
|||||||
analysisId,
|
analysisId,
|
||||||
} = useBinaryPage();
|
} = useBinaryPage();
|
||||||
|
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const handleRenameApplied = useCallback(() => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["functions", analysisId] });
|
||||||
|
if (selectedFunctionId) {
|
||||||
|
queryClient.invalidateQueries({ queryKey: [`/functions/${selectedFunctionId}`] });
|
||||||
|
}
|
||||||
|
}, [queryClient, analysisId, selectedFunctionId]);
|
||||||
|
|
||||||
const navigateToEntryPoint = () => {
|
const navigateToEntryPoint = () => {
|
||||||
if (!analysis?.entryPoint) return;
|
if (!analysis?.entryPoint) return;
|
||||||
const match = analysis.entryPoint.match(/^(0x[0-9a-fA-F]+)/);
|
const match = analysis.entryPoint.match(/^(0x[0-9a-fA-F]+)/);
|
||||||
@ -313,6 +323,7 @@ const Binary = () => {
|
|||||||
isLoading={selectedFunctionLoading}
|
isLoading={selectedFunctionLoading}
|
||||||
onSelectFunction={setSelectedFunctionId}
|
onSelectFunction={setSelectedFunctionId}
|
||||||
functionNameToId={functionNameToId}
|
functionNameToId={functionNameToId}
|
||||||
|
onRenameApplied={handleRenameApplied}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
@ -321,7 +332,7 @@ const Binary = () => {
|
|||||||
|
|
||||||
<ResizablePanel id="chat-panel" defaultSize="30%" minSize="10%" maxSize="40%">
|
<ResizablePanel id="chat-panel" defaultSize="30%" minSize="10%" maxSize="40%">
|
||||||
<div className="border-border bg-card h-full overflow-hidden border-l">
|
<div className="border-border bg-card h-full overflow-hidden border-l">
|
||||||
<AIChatPanel />
|
<AIChatPanel onRenameApplied={handleRenameApplied} />
|
||||||
</div>
|
</div>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
</ResizablePanelGroup>
|
</ResizablePanelGroup>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user