Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 9 additions & 17 deletions app/components/AllFilesDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import React, { useState, useEffect, useMemo } from "react";
import React, { useState, useEffect } from "react";
import { X, Download, Circle, CircleCheck, File } from "lucide-react";
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
Expand Down Expand Up @@ -137,14 +137,15 @@ const AllFilesDialog = ({
const [fileUrls, setFileUrls] = useState<Map<number, string>>(new Map());
const [isLoadingUrls, setIsLoadingUrls] = useState(false);

// Reset URLs when dialog closes
useEffect(() => {
if (!open) {
const handleOpenChange = (newOpen: boolean) => {
if (!newOpen) {
setFileUrls(new Map());
setIsLoadingUrls(false);
setSelectionMode(false);
setSelectedFiles(new Set());
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open]);
onOpenChange(newOpen);
};

// Batch fetch all URLs when dialog opens
useEffect(() => {
Expand Down Expand Up @@ -217,15 +218,6 @@ const AllFilesDialog = ({
};
}, [open, files, getFileUrlAction, convex, fileUrlCache]);

// Reset selection when dialog closes
useEffect(() => {
if (!open) {
// eslint-disable-next-line react-hooks/set-state-in-effect
setSelectionMode(false);

setSelectedFiles(new Set());
}
}, [open]);

const handleEnterSelectionMode = () => {
setSelectionMode(true);
Expand Down Expand Up @@ -340,7 +332,7 @@ const AllFilesDialog = ({
};

return (
<Dialog open={open} onOpenChange={onOpenChange}>
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent
className="bg-background rounded-[20px] border border-border fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 max-w-[95%] max-h-[95%] overflow-auto h-[680px] flex flex-col p-0"
style={{ width: "600px" }}
Expand Down Expand Up @@ -388,7 +380,7 @@ const AllFilesDialog = ({
<Download className="size-5 text-muted-foreground" />
</Button>
<Button
onClick={() => onOpenChange(false)}
onClick={() => handleOpenChange(false)}
variant="ghost"
size="icon"
className="h-7 w-7"
Expand Down
3 changes: 2 additions & 1 deletion app/components/ComputerSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ export const ComputerSidebarBase: React.FC<ComputerSidebarProps> = ({
if (sidebarOpen && toolExecutions.length > 0) {
previousToolCountRef.current = toolExecutions.length;
}
}, [sidebarOpen]); // Only run when sidebar opens/closes
// eslint-disable-next-line react-hooks/exhaustive-deps -- Intentionally only sync on sidebar open/close, not on every tool execution
}, [sidebarOpen]);

// Auto-follow new tools when at live position during streaming
useEffect(() => {
Expand Down
1 change: 1 addition & 0 deletions app/components/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,7 @@ export const Chat = ({
// Reset processing flag after brief delay
setTimeout(() => setIsProcessingQueue(false), 100);
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- todosRef and sandboxPreferenceRef are stable refs, .current is read at runtime
}, [
status,
messageQueue.length,
Expand Down
5 changes: 3 additions & 2 deletions convex/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1037,11 +1037,12 @@ export const regenerateWithNewContent = mutation({
await ctx.db.delete(msg._id);
}

// Check if deleted messages invalidate the chat summary
// Check if deleted OR edited messages invalidate the chat summary
// Include the edited message ID since modifying the cutoff message also invalidates
await checkAndInvalidateSummary(
ctx,
message.chat_id,
messages.map((m) => m.id),
[message.id, ...messages.map((m) => m.id)],
);

return null;
Expand Down
5 changes: 3 additions & 2 deletions lib/api/chat-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,9 +287,9 @@ export const createChatHandler = () => {
// Refresh system prompt when memory updates occur, cache and reuse until next update
prepareStep: async ({ steps, messages }) => {
try {
// Run summarization check on every step (agent mode, non-temporary)
// Run summarization check on every step (non-temporary chats only)
// but only summarize once
if (mode === "agent" && !temporary && !hasSummarized) {
if (!temporary && !hasSummarized) {
const {
needsSummarization,
summarizedMessages,
Expand All @@ -300,6 +300,7 @@ export const createChatHandler = () => {
finalMessages,
subscription,
trackedProvider.languageModel("summarization-model"),
mode,
);

if (needsSummarization && cutoffMessageId && summaryText) {
Expand Down
57 changes: 39 additions & 18 deletions lib/utils/message-summarization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
import { v4 as uuidv4 } from "uuid";
import { getMaxTokensForSubscription } from "@/lib/token-utils";
import { countTokens } from "gpt-tokenizer";
import { SubscriptionTier } from "@/types";
import { SubscriptionTier, ChatMode } from "@/types";

// Keep last N messages unsummarized for context
const MESSAGES_TO_KEEP_UNSUMMARIZED = 2;
Expand All @@ -19,6 +19,42 @@ const MESSAGES_TO_KEEP_UNSUMMARIZED = 2;
// This provides ~3.2k tokens (for 32k Pro plan) for assistant's response and summary
const SUMMARIZATION_THRESHOLD_PERCENTAGE = 0.9;

const AGENT_SUMMARIZATION_PROMPT =
"You are an agent performing context condensation for a security agent. Your job is to compress scan data while preserving ALL operationally critical information for continuing the security assessment.\n\n" +
"CRITICAL ELEMENTS TO PRESERVE:\n" +
"- Discovered vulnerabilities and potential attack vectors\n" +
"- Scan results and tool outputs (compressed but maintaining key findings)\n" +
"- Access credentials, tokens, or authentication details found\n" +
"- System architecture insights and potential weak points\n" +
"- Progress made in the assessment\n" +
"- Failed attempts and dead ends (to avoid duplication)\n" +
"- Any decisions made about the testing approach\n\n" +
"COMPRESSION GUIDELINES:\n" +
"- Preserve exact technical details (URLs, paths, parameters, payloads)\n" +
"- Summarize verbose tool outputs while keeping critical findings\n" +
"- Maintain version numbers, specific technologies identified\n" +
"- Keep exact error messages that might indicate vulnerabilities\n" +
"- Compress repetitive or similar findings into consolidated form\n\n" +
"Remember: Another security agent will use this summary to continue the assessment. They must be able to pick up exactly where you left off without losing any operational advantage or context needed to find vulnerabilities.";

const ASK_SUMMARIZATION_PROMPT =
"You are performing context condensation for a conversational assistant. Your job is to compress the conversation while preserving key information for continuity.\n\n" +
"CRITICAL ELEMENTS TO PRESERVE:\n" +
"- User's questions and the assistant's answers\n" +
"- Key facts, decisions, and conclusions reached\n" +
"- Any URLs, code snippets, or technical details shared\n" +
"- User preferences or context mentioned\n" +
"- Unresolved questions or ongoing threads\n\n" +
"COMPRESSION GUIDELINES:\n" +
"- Preserve exact technical details when relevant\n" +
"- Summarize repetitive exchanges into consolidated form\n" +
"- Maintain the conversational flow and context\n" +
"- Keep user-stated goals and requirements\n\n" +
"Remember: The assistant will use this summary to continue helping the user seamlessly.";

const getSummarizationPrompt = (mode: ChatMode): string =>
mode === "agent" ? AGENT_SUMMARIZATION_PROMPT : ASK_SUMMARIZATION_PROMPT;

/**
* Count tokens for ModelMessage array
* Uses countPartTokens-like logic for each message content part
Expand Down Expand Up @@ -65,6 +101,7 @@ export const checkAndSummarizeIfNeeded = async (
uiMessages: UIMessage[],
subscription: SubscriptionTier,
languageModel: LanguageModel,
mode: ChatMode,
): Promise<{
needsSummarization: boolean;
summarizedMessages: UIMessage[];
Expand Down Expand Up @@ -121,23 +158,7 @@ export const checkAndSummarizeIfNeeded = async (
try {
const result = await generateText({
model: languageModel,
system:
"You are an agent performing context condensation for a security agent. Your job is to compress scan data while preserving ALL operationally critical information for continuing the security assessment.\n\n" +
"CRITICAL ELEMENTS TO PRESERVE:\n" +
"- Discovered vulnerabilities and potential attack vectors\n" +
"- Scan results and tool outputs (compressed but maintaining key findings)\n" +
"- Access credentials, tokens, or authentication details found\n" +
"- System architecture insights and potential weak points\n" +
"- Progress made in the assessment\n" +
"- Failed attempts and dead ends (to avoid duplication)\n" +
"- Any decisions made about the testing approach\n\n" +
"COMPRESSION GUIDELINES:\n" +
"- Preserve exact technical details (URLs, paths, parameters, payloads)\n" +
"- Summarize verbose tool outputs while keeping critical findings\n" +
"- Maintain version numbers, specific technologies identified\n" +
"- Keep exact error messages that might indicate vulnerabilities\n" +
"- Compress repetitive or similar findings into consolidated form\n\n" +
"Remember: Another security agent will use this summary to continue the assessment. They must be able to pick up exactly where you left off without losing any operational advantage or context needed to find vulnerabilities.",
system: getSummarizationPrompt(mode),
messages: [
...convertToModelMessages(messagesToSummarize),
{
Expand Down