Support exclude paths in manual context management (#774)
This commit is contained in:
@@ -16,29 +16,33 @@ import {
|
||||
} from "./ui/tooltip";
|
||||
import { useSettings } from "@/hooks/useSettings";
|
||||
import { useContextPaths } from "@/hooks/useContextPaths";
|
||||
import type { ContextPathResult } from "@/lib/schemas";
|
||||
|
||||
export function ContextFilesPicker() {
|
||||
const { settings } = useSettings();
|
||||
const {
|
||||
contextPaths,
|
||||
smartContextAutoIncludes,
|
||||
excludePaths,
|
||||
updateContextPaths,
|
||||
updateSmartContextAutoIncludes,
|
||||
updateExcludePaths,
|
||||
} = useContextPaths();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [newPath, setNewPath] = useState("");
|
||||
const [newAutoIncludePath, setNewAutoIncludePath] = useState("");
|
||||
const [newExcludePath, setNewExcludePath] = useState("");
|
||||
|
||||
const addPath = () => {
|
||||
if (
|
||||
newPath.trim() === "" ||
|
||||
contextPaths.find((p) => p.globPath === newPath)
|
||||
contextPaths.find((p: ContextPathResult) => p.globPath === newPath)
|
||||
) {
|
||||
setNewPath("");
|
||||
return;
|
||||
}
|
||||
const newPaths = [
|
||||
...contextPaths.map(({ globPath }) => ({ globPath })),
|
||||
...contextPaths.map(({ globPath }: ContextPathResult) => ({ globPath })),
|
||||
{
|
||||
globPath: newPath,
|
||||
},
|
||||
@@ -49,21 +53,25 @@ export function ContextFilesPicker() {
|
||||
|
||||
const removePath = (pathToRemove: string) => {
|
||||
const newPaths = contextPaths
|
||||
.filter((p) => p.globPath !== pathToRemove)
|
||||
.map(({ globPath }) => ({ globPath }));
|
||||
.filter((p: ContextPathResult) => p.globPath !== pathToRemove)
|
||||
.map(({ globPath }: ContextPathResult) => ({ globPath }));
|
||||
updateContextPaths(newPaths);
|
||||
};
|
||||
|
||||
const addAutoIncludePath = () => {
|
||||
if (
|
||||
newAutoIncludePath.trim() === "" ||
|
||||
smartContextAutoIncludes.find((p) => p.globPath === newAutoIncludePath)
|
||||
smartContextAutoIncludes.find(
|
||||
(p: ContextPathResult) => p.globPath === newAutoIncludePath,
|
||||
)
|
||||
) {
|
||||
setNewAutoIncludePath("");
|
||||
return;
|
||||
}
|
||||
const newPaths = [
|
||||
...smartContextAutoIncludes.map(({ globPath }) => ({ globPath })),
|
||||
...smartContextAutoIncludes.map(({ globPath }: ContextPathResult) => ({
|
||||
globPath,
|
||||
})),
|
||||
{
|
||||
globPath: newAutoIncludePath,
|
||||
},
|
||||
@@ -74,11 +82,36 @@ export function ContextFilesPicker() {
|
||||
|
||||
const removeAutoIncludePath = (pathToRemove: string) => {
|
||||
const newPaths = smartContextAutoIncludes
|
||||
.filter((p) => p.globPath !== pathToRemove)
|
||||
.map(({ globPath }) => ({ globPath }));
|
||||
.filter((p: ContextPathResult) => p.globPath !== pathToRemove)
|
||||
.map(({ globPath }: ContextPathResult) => ({ globPath }));
|
||||
updateSmartContextAutoIncludes(newPaths);
|
||||
};
|
||||
|
||||
const addExcludePath = () => {
|
||||
if (
|
||||
newExcludePath.trim() === "" ||
|
||||
excludePaths.find((p: ContextPathResult) => p.globPath === newExcludePath)
|
||||
) {
|
||||
setNewExcludePath("");
|
||||
return;
|
||||
}
|
||||
const newPaths = [
|
||||
...excludePaths.map(({ globPath }: ContextPathResult) => ({ globPath })),
|
||||
{
|
||||
globPath: newExcludePath,
|
||||
},
|
||||
];
|
||||
updateExcludePaths(newPaths);
|
||||
setNewExcludePath("");
|
||||
};
|
||||
|
||||
const removeExcludePath = (pathToRemove: string) => {
|
||||
const newPaths = excludePaths
|
||||
.filter((p: ContextPathResult) => p.globPath !== pathToRemove)
|
||||
.map(({ globPath }: ContextPathResult) => ({ globPath }));
|
||||
updateExcludePaths(newPaths);
|
||||
};
|
||||
|
||||
const isSmartContextEnabled =
|
||||
settings?.enableDyadPro && settings?.enableProSmartFilesContextMode;
|
||||
|
||||
@@ -100,7 +133,10 @@ export function ContextFilesPicker() {
|
||||
<TooltipContent>Codebase Context</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<PopoverContent className="w-96" align="start">
|
||||
<PopoverContent
|
||||
className="w-96 max-h-[80vh] overflow-y-auto"
|
||||
align="start"
|
||||
>
|
||||
<div className="relative space-y-4">
|
||||
<div>
|
||||
<h3 className="font-medium">Codebase Context</h3>
|
||||
@@ -153,7 +189,7 @@ export function ContextFilesPicker() {
|
||||
<TooltipProvider>
|
||||
{contextPaths.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{contextPaths.map((p) => (
|
||||
{contextPaths.map((p: ContextPathResult) => (
|
||||
<div
|
||||
key={p.globPath}
|
||||
className="flex items-center justify-between gap-2 rounded-md border p-2"
|
||||
@@ -197,6 +233,91 @@ export function ContextFilesPicker() {
|
||||
)}
|
||||
</TooltipProvider>
|
||||
|
||||
<div className="pt-2">
|
||||
<div>
|
||||
<h3 className="font-medium">Exclude Paths</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="flex items-center gap-1 cursor-help">
|
||||
These files will be excluded from the context.{" "}
|
||||
<InfoIcon className="ml-2 size-4" />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-[300px]">
|
||||
<p>
|
||||
Exclude paths take precedence - files that match both
|
||||
include and exclude patterns will be excluded.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full max-w-sm items-center space-x-2 mt-4">
|
||||
<Input
|
||||
data-testid="exclude-context-files-input"
|
||||
type="text"
|
||||
placeholder="node_modules/**/*"
|
||||
value={newExcludePath}
|
||||
onChange={(e) => setNewExcludePath(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
addExcludePath();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
onClick={addExcludePath}
|
||||
data-testid="exclude-context-files-add-button"
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<TooltipProvider>
|
||||
{excludePaths.length > 0 && (
|
||||
<div className="space-y-2 mt-4">
|
||||
{excludePaths.map((p: ContextPathResult) => (
|
||||
<div
|
||||
key={p.globPath}
|
||||
className="flex items-center justify-between gap-2 rounded-md border p-2 border-red-200"
|
||||
>
|
||||
<div className="flex flex-1 flex-col overflow-hidden">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="truncate font-mono text-sm text-red-600">
|
||||
{p.globPath}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{p.globPath}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{p.files} files, ~{p.tokens} tokens
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => removeExcludePath(p.globPath)}
|
||||
data-testid="exclude-context-files-remove-button"
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
|
||||
{isSmartContextEnabled && (
|
||||
<div className="pt-2">
|
||||
<div>
|
||||
@@ -247,7 +368,7 @@ export function ContextFilesPicker() {
|
||||
<TooltipProvider>
|
||||
{smartContextAutoIncludes.length > 0 && (
|
||||
<div className="space-y-2 mt-4">
|
||||
{smartContextAutoIncludes.map((p) => (
|
||||
{smartContextAutoIncludes.map((p: ContextPathResult) => (
|
||||
<div
|
||||
key={p.globPath}
|
||||
className="flex items-center justify-between gap-2 rounded-md border p-2"
|
||||
|
||||
@@ -15,7 +15,12 @@ export function useContextPaths() {
|
||||
} = useQuery<ContextPathResults, Error>({
|
||||
queryKey: ["context-paths", appId],
|
||||
queryFn: async () => {
|
||||
if (!appId) return { contextPaths: [], smartContextAutoIncludes: [] };
|
||||
if (!appId)
|
||||
return {
|
||||
contextPaths: [],
|
||||
smartContextAutoIncludes: [],
|
||||
excludePaths: [],
|
||||
};
|
||||
const ipcClient = IpcClient.getInstance();
|
||||
return ipcClient.getChatContextResults({ appId });
|
||||
},
|
||||
@@ -25,9 +30,17 @@ export function useContextPaths() {
|
||||
const updateContextPathsMutation = useMutation<
|
||||
unknown,
|
||||
Error,
|
||||
{ contextPaths: GlobPath[]; smartContextAutoIncludes?: GlobPath[] }
|
||||
{
|
||||
contextPaths: GlobPath[];
|
||||
smartContextAutoIncludes?: GlobPath[];
|
||||
excludePaths?: GlobPath[];
|
||||
}
|
||||
>({
|
||||
mutationFn: async ({ contextPaths, smartContextAutoIncludes }) => {
|
||||
mutationFn: async ({
|
||||
contextPaths,
|
||||
smartContextAutoIncludes,
|
||||
excludePaths,
|
||||
}) => {
|
||||
if (!appId) throw new Error("No app selected");
|
||||
const ipcClient = IpcClient.getInstance();
|
||||
return ipcClient.setChatContext({
|
||||
@@ -35,6 +48,7 @@ export function useContextPaths() {
|
||||
chatContext: {
|
||||
contextPaths,
|
||||
smartContextAutoIncludes: smartContextAutoIncludes || [],
|
||||
excludePaths: excludePaths || [],
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -46,28 +60,63 @@ export function useContextPaths() {
|
||||
const updateContextPaths = async (paths: GlobPath[]) => {
|
||||
const currentAutoIncludes =
|
||||
contextPathsData?.smartContextAutoIncludes || [];
|
||||
const currentExcludePaths = contextPathsData?.excludePaths || [];
|
||||
return updateContextPathsMutation.mutateAsync({
|
||||
contextPaths: paths,
|
||||
smartContextAutoIncludes: currentAutoIncludes.map(({ globPath }) => ({
|
||||
globPath,
|
||||
})),
|
||||
smartContextAutoIncludes: currentAutoIncludes.map(
|
||||
({ globPath }: { globPath: string }) => ({
|
||||
globPath,
|
||||
}),
|
||||
),
|
||||
excludePaths: currentExcludePaths.map(
|
||||
({ globPath }: { globPath: string }) => ({
|
||||
globPath,
|
||||
}),
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
const updateSmartContextAutoIncludes = async (paths: GlobPath[]) => {
|
||||
const currentContextPaths = contextPathsData?.contextPaths || [];
|
||||
const currentExcludePaths = contextPathsData?.excludePaths || [];
|
||||
return updateContextPathsMutation.mutateAsync({
|
||||
contextPaths: currentContextPaths.map(({ globPath }) => ({ globPath })),
|
||||
contextPaths: currentContextPaths.map(
|
||||
({ globPath }: { globPath: string }) => ({ globPath }),
|
||||
),
|
||||
smartContextAutoIncludes: paths,
|
||||
excludePaths: currentExcludePaths.map(
|
||||
({ globPath }: { globPath: string }) => ({
|
||||
globPath,
|
||||
}),
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
const updateExcludePaths = async (paths: GlobPath[]) => {
|
||||
const currentContextPaths = contextPathsData?.contextPaths || [];
|
||||
const currentAutoIncludes =
|
||||
contextPathsData?.smartContextAutoIncludes || [];
|
||||
return updateContextPathsMutation.mutateAsync({
|
||||
contextPaths: currentContextPaths.map(
|
||||
({ globPath }: { globPath: string }) => ({ globPath }),
|
||||
),
|
||||
smartContextAutoIncludes: currentAutoIncludes.map(
|
||||
({ globPath }: { globPath: string }) => ({
|
||||
globPath,
|
||||
}),
|
||||
),
|
||||
excludePaths: paths,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
contextPaths: contextPathsData?.contextPaths || [],
|
||||
smartContextAutoIncludes: contextPathsData?.smartContextAutoIncludes || [],
|
||||
excludePaths: contextPathsData?.excludePaths || [],
|
||||
isLoading,
|
||||
error,
|
||||
updateContextPaths,
|
||||
updateSmartContextAutoIncludes,
|
||||
updateExcludePaths,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -39,10 +39,10 @@ export function registerContextPathsHandlers() {
|
||||
const results: ContextPathResults = {
|
||||
contextPaths: [],
|
||||
smartContextAutoIncludes: [],
|
||||
excludePaths: [],
|
||||
};
|
||||
const { contextPaths, smartContextAutoIncludes } = validateChatContext(
|
||||
app.chatContext,
|
||||
);
|
||||
const { contextPaths, smartContextAutoIncludes, excludePaths } =
|
||||
validateChatContext(app.chatContext);
|
||||
for (const contextPath of contextPaths) {
|
||||
const { formattedOutput, files } = await extractCodebase({
|
||||
appPath,
|
||||
@@ -76,6 +76,23 @@ export function registerContextPathsHandlers() {
|
||||
tokens: totalTokens,
|
||||
});
|
||||
}
|
||||
|
||||
for (const excludePath of excludePaths || []) {
|
||||
const { formattedOutput, files } = await extractCodebase({
|
||||
appPath,
|
||||
chatContext: {
|
||||
contextPaths: [excludePath],
|
||||
smartContextAutoIncludes: [],
|
||||
},
|
||||
});
|
||||
const totalTokens = estimateTokens(formattedOutput);
|
||||
|
||||
results.excludePaths.push({
|
||||
...excludePath,
|
||||
files: files.length,
|
||||
tokens: totalTokens,
|
||||
});
|
||||
}
|
||||
return results;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ export function validateChatContext(chatContext: unknown): AppChatContext {
|
||||
return {
|
||||
contextPaths: [],
|
||||
smartContextAutoIncludes: [],
|
||||
excludePaths: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,6 +21,7 @@ export function validateChatContext(chatContext: unknown): AppChatContext {
|
||||
return {
|
||||
contextPaths: [],
|
||||
smartContextAutoIncludes: [],
|
||||
excludePaths: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +120,7 @@ export type GlobPath = z.infer<typeof GlobPathSchema>;
|
||||
export const AppChatContextSchema = z.object({
|
||||
contextPaths: z.array(GlobPathSchema),
|
||||
smartContextAutoIncludes: z.array(GlobPathSchema),
|
||||
excludePaths: z.array(GlobPathSchema).optional(),
|
||||
});
|
||||
export type AppChatContext = z.infer<typeof AppChatContextSchema>;
|
||||
|
||||
@@ -131,6 +132,7 @@ export type ContextPathResult = GlobPath & {
|
||||
export type ContextPathResults = {
|
||||
contextPaths: ContextPathResult[];
|
||||
smartContextAutoIncludes: ContextPathResult[];
|
||||
excludePaths: ContextPathResult[];
|
||||
};
|
||||
|
||||
export const ReleaseChannelSchema = z.enum(["stable", "beta"]);
|
||||
|
||||
@@ -472,9 +472,10 @@ export async function extractCodebase({
|
||||
}
|
||||
|
||||
// Collect files from contextPaths and smartContextAutoIncludes
|
||||
const { contextPaths, smartContextAutoIncludes } = chatContext;
|
||||
const { contextPaths, smartContextAutoIncludes, excludePaths } = chatContext;
|
||||
const includedFiles = new Set<string>();
|
||||
const autoIncludedFiles = new Set<string>();
|
||||
const excludedFiles = new Set<string>();
|
||||
|
||||
// Add files from contextPaths
|
||||
if (contextPaths && contextPaths.length > 0) {
|
||||
@@ -509,6 +510,7 @@ export async function extractCodebase({
|
||||
const matches = await glob(pattern, {
|
||||
nodir: true,
|
||||
absolute: true,
|
||||
ignore: "**/node_modules/**",
|
||||
});
|
||||
matches.forEach((file) => {
|
||||
const normalizedFile = path.normalize(file);
|
||||
@@ -518,12 +520,36 @@ export async function extractCodebase({
|
||||
}
|
||||
}
|
||||
|
||||
// Add files from excludePaths
|
||||
if (excludePaths && excludePaths.length > 0) {
|
||||
for (const p of excludePaths) {
|
||||
const pattern = createFullGlobPath({
|
||||
appPath,
|
||||
globPath: p.globPath,
|
||||
});
|
||||
const matches = await glob(pattern, {
|
||||
nodir: true,
|
||||
absolute: true,
|
||||
ignore: "**/node_modules/**",
|
||||
});
|
||||
matches.forEach((file) => {
|
||||
const normalizedFile = path.normalize(file);
|
||||
excludedFiles.add(normalizedFile);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Only filter files if contextPaths are provided
|
||||
// If only smartContextAutoIncludes are provided, keep all files and just mark auto-includes as forced
|
||||
if (contextPaths && contextPaths.length > 0) {
|
||||
files = files.filter((file) => includedFiles.has(path.normalize(file)));
|
||||
}
|
||||
|
||||
// Filter out excluded files (this takes precedence over include paths)
|
||||
if (excludedFiles.size > 0) {
|
||||
files = files.filter((file) => !excludedFiles.has(path.normalize(file)));
|
||||
}
|
||||
|
||||
// Sort files by modification time (oldest first)
|
||||
// This is important for cache-ability.
|
||||
const sortedFiles = await sortFilesByModificationTime([...new Set(files)]);
|
||||
@@ -543,7 +569,9 @@ export async function extractCodebase({
|
||||
virtualFileSystem,
|
||||
});
|
||||
|
||||
const isForced = autoIncludedFiles.has(path.normalize(file));
|
||||
const isForced =
|
||||
autoIncludedFiles.has(path.normalize(file)) &&
|
||||
!excludedFiles.has(path.normalize(file));
|
||||
|
||||
// Determine file content based on whether we should read it
|
||||
let fileContent: string;
|
||||
|
||||
Reference in New Issue
Block a user