| author | |
| committer | |
| log | 0647c481017f25ddff2ec1dff68ef3944489b6db |
| tree | fff5558d75d0f6bd83a9d9e121ba7d4eca2a36f9 |
| parent | d6f47ff9725f0902b526f854519cb7e92dcee52f |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>4 files changed, 104 insertions(+), 97 deletions(-)
apps/web/src/components/ChatView.tsx+24-15| ... | @@ -7672,9 +7672,9 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -7672,9 +7672,9 @@ export default function ChatView(props: ChatViewProps) { |
| 7672 | : "Compacting is unavailable right now" | 7672 | : "Compacting is unavailable right now" |
| 7673 | : null; | 7673 | : null; |
| 7674 | // Tokens a stale Claude session would re-read on its next turn. While set, | 7674 | // Tokens a stale Claude session would re-read on its next turn. While set, |
| 7675 | // Enter compacts first and the composer's send button says so; "Send with | 7675 | // the composer shows a Compact chip and Enter compacts first; turning the |
| 7676 | // full history" in its menu skips that once. Held queues and multi-model | 7676 | // chip off sends the next message with full history. Held queues and |
| 7677 | // sends never compact first, so the offer hides for them. | 7677 | // multi-model sends never compact first, so the offer hides for them. |
| 7678 | const resumeCompactionTokens = | 7678 | const resumeCompactionTokens = |
| 7679 | activeContextWindow && | 7679 | activeContextWindow && |
| 7680 | !resumeCompactionPermanentlyDismissed && | 7680 | !resumeCompactionPermanentlyDismissed && |
| ... | @@ -7690,17 +7690,25 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -7690,17 +7690,25 @@ export default function ChatView(props: ChatViewProps) { |
| 7690 | }) | 7690 | }) |
| 7691 | ? activeContextWindow.usedTokens | 7691 | ? activeContextWindow.usedTokens |
| 7692 | : null; | 7692 | : null; |
| 7693 | // Set only for the synchronous span of a "Send with full history" submit; | 7693 | // Threads whose Compact chip is turned off. A send that starts its turn |
| 7694 | // onSend reads it before its first await. | 7694 | // clears its thread's entry; a failed send keeps it for the retry. |
| 7695 | const keepFullHistoryOnceRef = useRef(false); | 7695 | const [fullHistoryThreadKeys, setFullHistoryThreadKeys] = useState<ReadonlySet<string>>( |
| 7696 | const sendWithFullHistory = useCallback((send: () => void) => { | 7696 | () => new Set(), |
| 7697 | keepFullHistoryOnceRef.current = true; | 7697 | ); |
| 7698 | try { | 7698 | const keepFullHistory = fullHistoryThreadKeys.has(routeThreadKey); |
| 7699 | send(); | 7699 | const setKeepFullHistory = useCallback((threadKey: string, keep: boolean) => { |
| 7700 | } finally { | 7700 | setFullHistoryThreadKeys((current) => { |
| 7701 | keepFullHistoryOnceRef.current = false; | 7701 | if (current.has(threadKey) === keep) return current; |
| 7702 | } | 7702 | const next = new Set(current); |
| 7703 | if (keep) next.add(threadKey); | ||
| 7704 | else next.delete(threadKey); | ||
| 7705 | return next; | ||
| 7706 | }); | ||
| 7703 | }, []); | 7707 | }, []); |
| 7708 | const toggleKeepFullHistory = useCallback( | ||
| 7709 | () => setKeepFullHistory(routeThreadKey, !keepFullHistory), | ||
| 7710 | [keepFullHistory, routeThreadKey, setKeepFullHistory], | ||
| 7711 | ); | ||
| 7704 | const handleRestoreThreadBranch = useCallback(() => { | 7712 | const handleRestoreThreadBranch = useCallback(() => { |
| 7705 | if (!canWriteSourceControl) return; | 7713 | if (!canWriteSourceControl) return; |
| 7706 | if (gitStatusQuery.data?.hasWorkingTreeChanges) { | 7714 | if (gitStatusQuery.data?.hasWorkingTreeChanges) { |
| ... | @@ -8652,7 +8660,6 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -8652,7 +8660,6 @@ export default function ChatView(props: ChatViewProps) { |
| 8652 | }, | 8660 | }, |
| 8653 | ) => { | 8661 | ) => { |
| 8654 | e?.preventDefault(); | 8662 | e?.preventDefault(); |
| 8655 | const keepFullHistory = keepFullHistoryOnceRef.current; | ||
| 8656 | // Typed out in full rather than picked from the menu. Attachments or contexts | 8663 | // Typed out in full rather than picked from the menu. Attachments or contexts |
| 8657 | // mean the user is sending a prompt, so those go through as usual. | 8664 | // mean the user is sending a prompt, so those go through as usual. |
| 8658 | if ( | 8665 | if ( |
| ... | @@ -9896,6 +9903,7 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -9896,6 +9903,7 @@ export default function ChatView(props: ChatViewProps) { |
| 9896 | failure = startResult; | 9903 | failure = startResult; |
| 9897 | } else { | 9904 | } else { |
| 9898 | turnStartSucceeded = true; | 9905 | turnStartSucceeded = true; |
| 9906 | setKeepFullHistory(routeThreadKey, false); | ||
| 9899 | // The turn is under way and will spend quota, so that thread's limits | 9907 | // The turn is under way and will spend quota, so that thread's limits |
| 9900 | // snapshot is stale. Uploads may have outlasted a navigation, so only | 9908 | // snapshot is stale. Uploads may have outlasted a navigation, so only |
| 9901 | // the sending thread's panel clears. | 9909 | // the sending thread's panel clears. |
| ... | @@ -11559,7 +11567,8 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -11559,7 +11567,8 @@ export default function ChatView(props: ChatViewProps) { |
| 11559 | } | 11567 | } |
| 11560 | bannerItems={composerBannerItems} | 11568 | bannerItems={composerBannerItems} |
| 11561 | resumeCompactionTokens={resumeCompactionTokens} | 11569 | resumeCompactionTokens={resumeCompactionTokens} |
| 11562 | onSendWithFullHistory={sendWithFullHistory} | 11570 | keepFullHistory={keepFullHistory} |
| 11571 | onToggleKeepFullHistory={toggleKeepFullHistory} | ||
| 11563 | // With attachments or contexts aboard the pick just inserts the | 11572 | // With attachments or contexts aboard the pick just inserts the |
| 11564 | // text, so it sends as a prompt like the typed path would. | 11573 | // text, so it sends as a prompt like the typed path would. |
| 11565 | onUsageLimitsCommand={ | 11574 | onUsageLimitsCommand={ |
apps/web/src/components/chat/ChatComposer.tsx+15-14| ... | @@ -1391,7 +1391,8 @@ const ComposerFooterPrimaryActions = memo(function ComposerFooterPrimaryActions( | ... | @@ -1391,7 +1391,8 @@ const ComposerFooterPrimaryActions = memo(function ComposerFooterPrimaryActions( |
| 1391 | compactDisabled: boolean; | 1391 | compactDisabled: boolean; |
| 1392 | compactDisabledReason: string | null; | 1392 | compactDisabledReason: string | null; |
| 1393 | compactBeforeSendTokens: number | null; | 1393 | compactBeforeSendTokens: number | null; |
| 1394 | onSendWithFullHistory: () => void; | 1394 | keepFullHistory: boolean; |
| 1395 | onToggleKeepFullHistory: () => void; | ||
| 1395 | }) { | 1396 | }) { |
| 1396 | return ( | 1397 | return ( |
| 1397 | <> | 1398 | <> |
| ... | @@ -1431,7 +1432,8 @@ const ComposerFooterPrimaryActions = memo(function ComposerFooterPrimaryActions( | ... | @@ -1431,7 +1432,8 @@ const ComposerFooterPrimaryActions = memo(function ComposerFooterPrimaryActions( |
| 1431 | onInterrupt={props.onInterrupt} | 1432 | onInterrupt={props.onInterrupt} |
| 1432 | onImplementPlanInNewThread={props.onImplementPlanInNewThread} | 1433 | onImplementPlanInNewThread={props.onImplementPlanInNewThread} |
| 1433 | compactBeforeSendTokens={props.compactBeforeSendTokens} | 1434 | compactBeforeSendTokens={props.compactBeforeSendTokens} |
| 1434 | onSendWithFullHistory={props.onSendWithFullHistory} | 1435 | keepFullHistory={props.keepFullHistory} |
| 1436 | onToggleKeepFullHistory={props.onToggleKeepFullHistory} | ||
| 1435 | /> | 1437 | /> |
| 1436 | </> | 1438 | </> |
| 1437 | ); | 1439 | ); |
| ... | @@ -1551,10 +1553,12 @@ export interface ChatComposerProps { | ... | @@ -1551,10 +1553,12 @@ export interface ChatComposerProps { |
| 1551 | sendDisabledReason: string | null; | 1553 | sendDisabledReason: string | null; |
| 1552 | isPreparingWorktree: boolean; | 1554 | isPreparingWorktree: boolean; |
| 1553 | bannerItems: readonly ComposerBannerStackItem[]; | 1555 | bannerItems: readonly ComposerBannerStackItem[]; |
| 1554 | /** Tokens Enter compacts before sending; null when the next send keeps full history. */ | 1556 | /** Tokens a stale session would re-read; null when the thread is not offered compaction. */ |
| 1555 | resumeCompactionTokens: number | null; | 1557 | resumeCompactionTokens: number | null; |
| 1556 | /** Runs `send` as a one-off send that keeps full history instead of compacting first. */ | 1558 | /** The Compact chip is off, so the next send keeps full history. */ |
| 1557 | onSendWithFullHistory: (send: () => void) => void; | 1559 | keepFullHistory: boolean; |
| 1560 | /** Flips the Compact chip for the active thread. */ | ||
| 1561 | onToggleKeepFullHistory: () => void; | ||
| 1558 | /** Picking /usage-limits from the menu is the action itself; the draft keeps nothing of it. */ | 1562 | /** Picking /usage-limits from the menu is the action itself; the draft keeps nothing of it. */ |
| 1559 | onUsageLimitsCommand?: (() => void) | undefined; | 1563 | onUsageLimitsCommand?: (() => void) | undefined; |
| 1560 | environmentUnavailable: { | 1564 | environmentUnavailable: { |
| ... | @@ -3016,7 +3020,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) | ... | @@ -3016,7 +3020,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 3016 | (!composerSendState.hasSendableContent && !showResumeAction); | 3020 | (!composerSendState.hasSendableContent && !showResumeAction); |
| 3017 | const collapsedComposerPrimaryActionLabel = showResumeAction | 3021 | const collapsedComposerPrimaryActionLabel = showResumeAction |
| 3018 | ? "Resume thread" | 3022 | ? "Resume thread" |
| 3019 | : props.resumeCompactionTokens !== null | 3023 | : props.resumeCompactionTokens !== null && !props.keepFullHistory |
| 3020 | ? "Open composer to compact and send" | 3024 | ? "Open composer to compact and send" |
| 3021 | : "Send message"; | 3025 | : "Send message"; |
| 3022 | const showMobilePendingAnswerActions = | 3026 | const showMobilePendingAnswerActions = |
| ... | @@ -4271,11 +4275,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) | ... | @@ -4271,11 +4275,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 4271 | }, | 4275 | }, |
| 4272 | [phase, settings.followUpBehavior, submitComposer], | 4276 | [phase, settings.followUpBehavior, submitComposer], |
| 4273 | ); | 4277 | ); |
| 4274 | const { onSendWithFullHistory } = props; | ||
| 4275 | const sendWithFullHistory = useCallback( | ||
| 4276 | () => onSendWithFullHistory(() => submitComposer()), | ||
| 4277 | [onSendWithFullHistory, submitComposer], | ||
| 4278 | ); | ||
| 4279 | const submitCitationAndSend = useCallback(() => { | 4278 | const submitCitationAndSend = useCallback(() => { |
| 4280 | submitComposer( | 4279 | submitComposer( |
| 4281 | undefined, | 4280 | undefined, |
| ... | @@ -6896,8 +6895,9 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) | ... | @@ -6896,8 +6895,9 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 6896 | onClick={(event) => { | 6895 | onClick={(event) => { |
| 6897 | event.stopPropagation(); | 6896 | event.stopPropagation(); |
| 6898 | if (showResumeAction) onResume(); | 6897 | if (showResumeAction) onResume(); |
| 6899 | // Compacting first is only sent from the labeled button, so expand to show it. | 6898 | // Compacting first only sends from the expanded composer, where the chip shows it. |
| 6900 | else if (props.resumeCompactionTokens !== null) expandMobileComposer(); | 6899 | else if (props.resumeCompactionTokens !== null && !props.keepFullHistory) |
| 6900 | expandMobileComposer(); | ||
| 6901 | else submitComposer(); | 6901 | else submitComposer(); |
| 6902 | }} | 6902 | }} |
| 6903 | > | 6903 | > |
| ... | @@ -7615,7 +7615,8 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) | ... | @@ -7615,7 +7615,8 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 7615 | onInterrupt={handleInterruptPrimaryAction} | 7615 | onInterrupt={handleInterruptPrimaryAction} |
| 7616 | onImplementPlanInNewThread={handleImplementPlanInNewThreadPrimaryAction} | 7616 | onImplementPlanInNewThread={handleImplementPlanInNewThreadPrimaryAction} |
| 7617 | compactBeforeSendTokens={props.resumeCompactionTokens} | 7617 | compactBeforeSendTokens={props.resumeCompactionTokens} |
| 7618 | onSendWithFullHistory={sendWithFullHistory} | 7618 | keepFullHistory={props.keepFullHistory} |
| 7619 | onToggleKeepFullHistory={props.onToggleKeepFullHistory} | ||
| 7619 | compactDisabled={ | 7620 | compactDisabled={ |
| 7620 | compactDisabled || noProviderAvailable || isSendBusy || isConnecting | 7621 | compactDisabled || noProviderAvailable || isSendBusy || isConnecting |
| 7621 | } | 7622 | } |
apps/web/src/components/chat/ComposerPrimaryActions.tsx+61-64| ... | @@ -1,5 +1,5 @@ | ... | @@ -1,5 +1,5 @@ |
| 1 | import { memo, type MouseEventHandler, type PointerEventHandler } from "react"; | 1 | import { memo, type MouseEventHandler, type PointerEventHandler } from "react"; |
| 2 | import { CheckIcon, ChevronDownIcon, ChevronLeftIcon, PlayIcon } from "lucide-react"; | 2 | import { CheckIcon, ChevronDownIcon, ChevronLeftIcon, Minimize2Icon, PlayIcon } from "lucide-react"; |
| 3 | import { CornerUpRight, ListPlus } from "lucide"; | 3 | import { CornerUpRight, ListPlus } from "lucide"; |
| 4 | import { MorphIcon } from "~/components/MorphIcon"; | 4 | import { MorphIcon } from "~/components/MorphIcon"; |
| 5 | import { useEnvironmentIdentificationMode } from "~/hooks/useSettings"; | 5 | import { useEnvironmentIdentificationMode } from "~/hooks/useSettings"; |
| ... | @@ -51,9 +51,11 @@ interface ComposerPrimaryActionsProps { | ... | @@ -51,9 +51,11 @@ interface ComposerPrimaryActionsProps { |
| 51 | onPreviousPendingQuestion: () => void; | 51 | onPreviousPendingQuestion: () => void; |
| 52 | onInterrupt: () => void; | 52 | onInterrupt: () => void; |
| 53 | onImplementPlanInNewThread: () => void; | 53 | onImplementPlanInNewThread: () => void; |
| 54 | /** Tokens a stale session would re-read. When set, Enter compacts first and the button says so. */ | 54 | /** Tokens a stale session would re-read. When set, a Compact chip shows the count and Enter compacts first. */ |
| 55 | compactBeforeSendTokens?: number | null; | 55 | compactBeforeSendTokens?: number | null; |
| 56 | onSendWithFullHistory?: () => void; | 56 | /** The Compact chip is turned off, so the next send keeps full history. */ |
| 57 | keepFullHistory?: boolean; | ||
| 58 | onToggleKeepFullHistory?: () => void; | ||
| 57 | } | 59 | } |
| 58 | 60 | ||
| 59 | const formatPendingPrimaryActionLabel = (input: { | 61 | const formatPendingPrimaryActionLabel = (input: { |
| ... | @@ -108,7 +110,8 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({ | ... | @@ -108,7 +110,8 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({ |
| 108 | onInterrupt, | 110 | onInterrupt, |
| 109 | onImplementPlanInNewThread, | 111 | onImplementPlanInNewThread, |
| 110 | compactBeforeSendTokens = null, | 112 | compactBeforeSendTokens = null, |
| 111 | onSendWithFullHistory, | 113 | keepFullHistory = false, |
| 114 | onToggleKeepFullHistory, | ||
| 112 | }: ComposerPrimaryActionsProps) { | 115 | }: ComposerPrimaryActionsProps) { |
| 113 | const pointerFocusProps = preserveComposerFocusOnPointerDown | 116 | const pointerFocusProps = preserveComposerFocusOnPointerDown |
| 114 | ? { onPointerDown: preventPointerFocus } | 117 | ? { onPointerDown: preventPointerFocus } |
| ... | @@ -266,68 +269,24 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({ | ... | @@ -266,68 +269,24 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({ |
| 266 | } | 269 | } |
| 267 | 270 | ||
| 268 | const showResume = canResume && !hasSendableContent && !isEditingQueuedMessage; | 271 | const showResume = canResume && !hasSendableContent && !isEditingQueuedMessage; |
| 269 | const sendBlocked = isSendBusy || isSendDisabled || isConnecting || isEnvironmentUnavailable; | ||
| 270 | 272 | ||
| 271 | if (compactBeforeSendTokens !== null && !showResume && !isEditingQueuedMessage) { | 273 | const compactTokens = |
| 272 | const tokens = formatContextWindowTokens(compactBeforeSendTokens); | 274 | compactBeforeSendTokens !== null && !showResume && !isEditingQueuedMessage |
| 273 | return ( | 275 | ? formatContextWindowTokens(compactBeforeSendTokens) |
| 274 | <div data-chat-composer-compact-send="true" className="flex items-center justify-end"> | 276 | : null; |
| 275 | <Tooltip> | 277 | const compactsBeforeSend = compactTokens !== null && !keepFullHistory; |
| 276 | <TooltipTrigger | ||
| 277 | render={ | ||
| 278 | <button | ||
| 279 | type="submit" | ||
| 280 | className={cn( | ||
| 281 | messageActionPillClassName, | ||
| 282 | "h-9 rounded-r-none sm:h-8", | ||
| 283 | compact ? "px-3" : "px-4", | ||
| 284 | )} | ||
| 285 | {...pointerFocusProps} | ||
| 286 | onClick={onSubmitMessage} | ||
| 287 | disabled={sendBlocked || !hasSendableContent} | ||
| 288 | /> | ||
| 289 | } | ||
| 290 | > | ||
| 291 | {isConnecting || isSendBusy ? "Sending..." : "Compact and send"} | ||
| 292 | </TooltipTrigger> | ||
| 293 | <TooltipPopup>Summarize {tokens} tokens of history, then send</TooltipPopup> | ||
| 294 | </Tooltip> | ||
| 295 | <Menu> | ||
| 296 | <MenuTrigger | ||
| 297 | render={ | ||
| 298 | <button | ||
| 299 | type="button" | ||
| 300 | className={cn( | ||
| 301 | messageActionPillClassName, | ||
| 302 | "h-9 rounded-l-none border-l border-message-action-foreground/20 px-2 sm:h-8", | ||
| 303 | )} | ||
| 304 | aria-label="Send options" | ||
| 305 | {...pointerFocusProps} | ||
| 306 | disabled={sendBlocked || !hasSendableContent} | ||
| 307 | /> | ||
| 308 | } | ||
| 309 | > | ||
| 310 | <ChevronDownIcon className="size-3.5" /> | ||
| 311 | </MenuTrigger> | ||
| 312 | <MenuPopup align="end" side="top" {...composerFloatingLayerProps}> | ||
| 313 | <MenuItem disabled={sendBlocked} onClick={onSendWithFullHistory}> | ||
| 314 | Send with full history ({tokens} tokens) | ||
| 315 | </MenuItem> | ||
| 316 | </MenuPopup> | ||
| 317 | </Menu> | ||
| 318 | </div> | ||
| 319 | ); | ||
| 320 | } | ||
| 321 | 278 | ||
| 322 | const submitLabel = showResume | 279 | const submitLabel = showResume |
| 323 | ? "Resume thread" | 280 | ? "Resume thread" |
| 324 | : isEditingQueuedMessage | 281 | : isEditingQueuedMessage |
| 325 | ? "Update queued message" | 282 | ? "Update queued message" |
| 326 | : isQueuing | 283 | : compactsBeforeSend |
| 327 | ? "Queue message" | 284 | ? "Compact and send" |
| 328 | : isRunning | 285 | : isQueuing |
| 329 | ? "Steer message" | 286 | ? "Queue message" |
| 330 | : "Submit message"; | 287 | : isRunning |
| 288 | ? "Steer message" | ||
| 289 | : "Submit message"; | ||
| 331 | const submitStatus = isEnvironmentUnavailable | 290 | const submitStatus = isEnvironmentUnavailable |
| 332 | ? "Environment disconnected" | 291 | ? "Environment disconnected" |
| 333 | : (sendDisabledReason ?? | 292 | : (sendDisabledReason ?? |
| ... | @@ -342,9 +301,11 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({ | ... | @@ -342,9 +301,11 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({ |
| 342 | : null)); | 301 | : null)); |
| 343 | const submitTooltip = | 302 | const submitTooltip = |
| 344 | submitStatus ?? | 303 | submitStatus ?? |
| 345 | (isRunning && !isEditingQueuedMessage | 304 | (compactsBeforeSend |
| 346 | ? `Click to ${followUpBehavior}, Ctrl/⌘-click${alternateShortcutLabel ? ` or ${alternateShortcutLabel}` : ""} to ${alternateAction}` | 305 | ? `Summarize ${compactTokens} tokens of history, then send` |
| 347 | : submitLabel); | 306 | : isRunning && !isEditingQueuedMessage |
| 307 | ? `Click to ${followUpBehavior}, Ctrl/⌘-click${alternateShortcutLabel ? ` or ${alternateShortcutLabel}` : ""} to ${alternateAction}` | ||
| 308 | : submitLabel); | ||
| 348 | 309 | ||
| 349 | const sendButton = ( | 310 | const sendButton = ( |
| 350 | <button | 311 | <button |
| ... | @@ -393,10 +354,46 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({ | ... | @@ -393,10 +354,46 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({ |
| 393 | </button> | 354 | </button> |
| 394 | ); | 355 | ); |
| 395 | 356 | ||
| 396 | return ( | 357 | const submit = ( |
| 397 | <Tooltip key="submit"> | 358 | <Tooltip key="submit"> |
| 398 | <TooltipTrigger render={<span className="inline-flex" />}>{sendButton}</TooltipTrigger> | 359 | <TooltipTrigger render={<span className="inline-flex" />}>{sendButton}</TooltipTrigger> |
| 399 | <TooltipPopup>{submitTooltip}</TooltipPopup> | 360 | <TooltipPopup>{submitTooltip}</TooltipPopup> |
| 400 | </Tooltip> | 361 | </Tooltip> |
| 401 | ); | 362 | ); |
| 363 | if (compactTokens === null) return submit; | ||
| 364 | |||
| 365 | return ( | ||
| 366 | <div data-chat-composer-compact-send="true" className="flex items-center gap-2"> | ||
| 367 | <Tooltip> | ||
| 368 | <TooltipTrigger | ||
| 369 | render={ | ||
| 370 | <button | ||
| 371 | type="button" | ||
| 372 | className={cn( | ||
| 373 | "flex h-7 shrink-0 cursor-pointer items-center gap-1.5 rounded-full border px-2.5 text-xs tabular-nums outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-64 [&_svg]:pointer-events-none [&_svg]:size-3.5", | ||
| 374 | keepFullHistory | ||
| 375 | ? "border-border text-muted-foreground hover:text-foreground" | ||
| 376 | : "border-warning/40 text-warning hover:bg-warning/8", | ||
| 377 | )} | ||
| 378 | {...pointerFocusProps} | ||
| 379 | aria-pressed={!keepFullHistory} | ||
| 380 | aria-label={`Compact ${compactTokens} tokens of history before sending`} | ||
| 381 | disabled={!canOperateThread} | ||
| 382 | onClick={onToggleKeepFullHistory} | ||
| 383 | /> | ||
| 384 | } | ||
| 385 | > | ||
| 386 | <Minimize2Icon aria-hidden="true" /> | ||
| 387 | {keepFullHistory ? "Full" : "Compact"} | ||
| 388 | <span>{compactTokens}</span> | ||
| 389 | </TooltipTrigger> | ||
| 390 | <TooltipPopup> | ||
| 391 | {keepFullHistory | ||
| 392 | ? `Next send keeps all ${compactTokens} tokens. Click to compact first` | ||
| 393 | : `Next send compacts ${compactTokens} tokens first. Click to keep full history`} | ||
| 394 | </TooltipPopup> | ||
| 395 | </Tooltip> | ||
| 396 | {submit} | ||
| 397 | </div> | ||
| 398 | ); | ||
| 402 | }); | 399 | }); |
docs/user/providers-claude.md+4-4| ... | @@ -50,10 +50,10 @@ window. Leave it empty for Claude Code's default. | ... | @@ -50,10 +50,10 @@ window. Leave it empty for Claude Code's default. |
| 50 | 50 | ||
| 51 | You can also send `/compact` in an existing conversation. Web and desktop offer | 51 | You can also send `/compact` in an existing conversation. Web and desktop offer |
| 52 | **Compact context** from the context meter. When you return to a large thread | 52 | **Compact context** from the context meter. When you return to a large thread |
| 53 | after more than an hour, the send button changes to **Compact and send**: Enter | 53 | after more than an hour, a **Compact** chip with the thread's token count shows |
| 54 | summarizes the history first, then sends your message. To keep the full history | 54 | next to the send button. While it is on, Enter summarizes the history first, then |
| 55 | for that message, open the menu next to the button and choose **Send with full | 55 | sends your message. Click the chip to switch it to **Full** and keep the full |
| 56 | history**. See [commands and skills](./composer.md#commands-and-skills) for using | 56 | history for that message. See [commands and skills](./composer.md#commands-and-skills) for using |
| 57 | composer commands. | 57 | composer commands. |
| 58 | 58 | ||
| 59 | ## Usage limits | 59 | ## Usage limits |