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
87 changes: 79 additions & 8 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5804,15 +5804,13 @@ export default function ChatView(props: ChatViewProps) {
pendingApprovals.length > 0 ||
pendingUserInputs.length > 0 ||
showPlanFollowUpPrompt;
const compactDisabled = compactThreadUnavailable || composerHasUnsentContent;
const compactDisabled = compactThreadUnavailable;
const compactDisabledReason = compactDisabled
? composerHasUnsentContent
? "Send or clear your draft before compacting"
: !activeProject
? "Choose a project before compacting"
: !manualCompactionProviderAvailable
? "Compaction is unavailable for this provider"
: "Compacting is unavailable right now"
? !activeProject
? "Choose a project before compacting"
: !manualCompactionProviderAvailable
? "Compaction is unavailable for this provider"
: "Compacting is unavailable right now"
: null;
const resumeCompactionBannerItem = useMemo<ComposerBannerStackItem | null>(() => {
if (
Expand Down Expand Up @@ -6402,6 +6400,78 @@ export default function ChatView(props: ChatViewProps) {
],
);

const onCompactContext = async () => {
if (compactDisabled || !activeThread || !clientSettingsHydrated || sendInFlightRef.current) {
return;
}
const context = composerRef.current?.getSendContext();
if (!context?.providerAvailable) return;

// Compaction is a standalone command; the draft and its attachments stay local.
const threadId = activeThread.id;
const messageId = newMessageId();
const createdAt = new Date().toISOString();
sendInFlightRef.current = true;
beginLocalDispatch();
setThreadError(threadId, null);
setOptimisticUserMessages((messages) => [
...messages,
{
id: messageId,
role: "user",
text: "/compact",
turnId: null,
createdAt,
updatedAt: createdAt,
streaming: false,
},
]);
scrollToEnd();
try {
const settingsResult = await persistThreadSettingsForNextTurn({
threadId,
createdAt,
modelSelection: context.selectedModelSelection,
...(localCheckoutBranchMismatch
? { branch: localCheckoutBranchMismatch.currentBranch }
: {}),
runtimeMode,
interactionMode: context.interactionMode,
});
const result =
settingsResult._tag === "Failure"
? settingsResult
: await startThreadTurn({
environmentId,
input: {
threadId,
message: { messageId, role: "user", text: "/compact", attachments: [] },
modelSelection: context.selectedModelSelection,
runtimeMode,
interactionMode: context.interactionMode,
createdAt,
},
});
if (result._tag === "Failure") {
setOptimisticUserMessages((messages) =>
messages.filter((message) => message.id !== messageId),
);
resetLocalDispatch();
if (!isAtomCommandInterrupted(result)) {
const error = squashAtomCommandFailure(result);
setThreadError(
threadId,
error instanceof Error ? error.message : "Failed to compact context.",
);
}
} else {
clearUsageLimitsFor(routeThreadKey);
}
} finally {
sendInFlightRef.current = false;
}
};

const onSend = async (
e?: { preventDefault: () => void },
submissionIntent: ComposerSubmissionIntent = "foreground",
Expand Down Expand Up @@ -8461,6 +8531,7 @@ export default function ChatView(props: ChatViewProps) {
onPageScrollKeyDown={onComposerPageScrollKeyDown}
onPageScrollKeyUp={onComposerPageScrollKeyUp}
onPageScrollRelease={onComposerPageScrollRelease}
onCompactContext={onCompactContext}
onSend={onSend}
onInterrupt={onInterrupt}
onImplementPlanInNewThread={onImplementPlanInNewThread}
Expand Down
33 changes: 5 additions & 28 deletions apps/web/src/components/chat/ChatComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1354,6 +1354,7 @@ export interface ChatComposerProps {
onPageScrollRelease: () => void;

// Callbacks
onCompactContext: () => void;
onSend: (e?: { preventDefault: () => void }, intent?: ComposerSubmissionIntent) => void;
onInterrupt: () => void;
onImplementPlanInNewThread: () => void;
Expand Down Expand Up @@ -1460,6 +1461,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
onPageScrollKeyDown,
onPageScrollKeyUp,
onPageScrollRelease,
onCompactContext,
onSend,
onInterrupt,
onImplementPlanInNewThread,
Expand Down Expand Up @@ -1995,7 +1997,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
/**
* Count of pasted images still being compressed, per thread. Reserved
* against the attachment limit so concurrent pastes can't overshoot it,
* and checked before sending or compacting so an image cannot move into
* and checked before sending so an image cannot move into
* the next draft.
*/
const pendingImageCompressionsRef = useRef<Map<string, number>>(new Map());
Expand Down Expand Up @@ -3097,42 +3099,17 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
) {
return;
}
// The compact buttons cannot see the compression counter (it lives in
// a ref), so they render enabled during a paste; toast instead of
// silently ignoring the click.
if ((pendingImageCompressionsRef.current.get(attachmentTargetKey) ?? 0) > 0) {
toastManager.add({
type: "info",
title: "Still compressing a pasted image.",
description: "Compact again once its thumbnail appears.",
});
return;
}

promptRef.current = "/compact";
setComposerDraftPrompt(composerDraftTarget, "/compact");
submitComposer();
// A blocked dispatch (busy send ref, provider preflight rejection)
// would leave the injected "/compact" behind as if the user typed it.
// Clearing here is safe even when the send did dispatch: the send
// snapshots its prompt synchronously and clears the draft itself.
if (promptRef.current === "/compact") {
promptRef.current = "";
setComposerDraftPrompt(composerDraftTarget, "");
}
onCompactContext();
}, [
activePendingApproval,
activeThreadId,
compactDisabled,
composerDraftTarget,
isConnecting,
isSendBusy,
noProviderAvailable,
onCompactContext,
pendingUserInputs.length,
phase,
promptRef,
setComposerDraftPrompt,
submitComposer,
]);
const expandMobileComposer = useCallback(() => {
if (composerBlurFrameRef.current !== null) {
Expand Down
Loading