Skip to content
Closed
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
1 change: 1 addition & 0 deletions apps/desktop/src/settings/DesktopClientSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const clientSettings: ClientSettings = {
confirmThreadUnpin: false,
contextWindowMeterEnabled: false,
composerCollapseOnScroll: true,
compactStaleThreadsByDefault: true,
dismissedProviderUpdateNotificationKeys: [],
diffFilesCollapsed: true,
diffIgnoreWhitespace: true,
Expand Down
29 changes: 16 additions & 13 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7810,18 +7810,21 @@ export default function ChatView(props: ChatViewProps) {
})
? activeContextWindow.usedTokens
: null;
// Threads whose Compact chip is turned off. A send that starts its turn
// clears its thread's entry; a failed send keeps it for the retry.
const [fullHistoryThreadKeys, setFullHistoryThreadKeys] = useState<ReadonlySet<string>>(
() => new Set(),
);
const keepFullHistory = fullHistoryThreadKeys.has(routeThreadKey);
const setKeepFullHistory = useCallback((threadKey: string, keep: boolean) => {
setFullHistoryThreadKeys((current) => {
if (current.has(threadKey) === keep) return current;
const next = new Set(current);
if (keep) next.add(threadKey);
else next.delete(threadKey);
// Whether each thread's Compact chip is turned off. Threads without an entry
// follow the compactStaleThreadsByDefault setting. A send that starts its
// turn clears its thread's entry; a failed send keeps it for the retry.
const [fullHistoryByThreadKey, setFullHistoryByThreadKey] = useState<
ReadonlyMap<string, boolean>
>(() => new Map());
const keepFullHistory =
fullHistoryByThreadKey.get(routeThreadKey) ?? !settings.compactStaleThreadsByDefault;
// `null` drops the thread's entry so its chip follows the setting again.
const setKeepFullHistory = useCallback((threadKey: string, keep: boolean | null) => {
setFullHistoryByThreadKey((current) => {
if (current.get(threadKey) === (keep ?? undefined)) return current;
const next = new Map(current);
if (keep === null) next.delete(threadKey);
else next.set(threadKey, keep);
return next;
});
}, []);
Expand Down Expand Up @@ -10097,7 +10100,7 @@ export default function ChatView(props: ChatViewProps) {
failure = startResult;
} else {
turnStartSucceeded = true;
setKeepFullHistory(routeThreadKey, false);
setKeepFullHistory(routeThreadKey, null);
// The turn is under way and will spend quota, so that thread's limits
// snapshot is stale. Uploads may have outlasted a navigation, so only
// the sending thread's panel clears.
Expand Down
34 changes: 34 additions & 0 deletions apps/web/src/components/settings/SettingsPanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,10 @@ export function useSettingsRestore(onRestored?: () => void) {
...(settings.composerCollapseOnScroll !== DEFAULT_UNIFIED_SETTINGS.composerCollapseOnScroll
? ["Collapse composer on scroll"]
: []),
...(settings.compactStaleThreadsByDefault !==
DEFAULT_UNIFIED_SETTINGS.compactStaleThreadsByDefault
? ["Compact stale threads by default"]
: []),
...(settings.composerRichTextEnabled !== DEFAULT_UNIFIED_SETTINGS.composerRichTextEnabled
? ["Rich text composer"]
: []),
Expand Down Expand Up @@ -685,6 +689,7 @@ export function useSettingsRestore(onRestored?: () => void) {
settings.confirmThreadDelete,
settings.confirmThreadUnpin,
settings.composerCollapseOnScroll,
settings.compactStaleThreadsByDefault,
settings.composerRichTextEnabled,
settings.sendShortcut,
settings.followUpBehavior,
Expand Down Expand Up @@ -808,6 +813,7 @@ export function useSettingsRestore(onRestored?: () => void) {
proactivePanelsEnabled: DEFAULT_UNIFIED_SETTINGS.proactivePanelsEnabled,
showSkillsInSlashMenu: DEFAULT_UNIFIED_SETTINGS.showSkillsInSlashMenu,
composerCollapseOnScroll: DEFAULT_UNIFIED_SETTINGS.composerCollapseOnScroll,
compactStaleThreadsByDefault: DEFAULT_UNIFIED_SETTINGS.compactStaleThreadsByDefault,
composerRichTextEnabled: DEFAULT_UNIFIED_SETTINGS.composerRichTextEnabled,
sendShortcut: DEFAULT_UNIFIED_SETTINGS.sendShortcut,
followUpBehavior: DEFAULT_UNIFIED_SETTINGS.followUpBehavior,
Expand Down Expand Up @@ -2824,6 +2830,34 @@ export function GeneralSettingsPanel() {
}
/>

<SettingsRow
{...searchableSetting("compact-stale-threads")}
description="When you return to a large Claude thread after its prompt cache expires, start the Compact chip on so your next message summarizes the history first. Turn this off to keep the full history unless you switch the chip on."
resetAction={
settings.compactStaleThreadsByDefault !==
DEFAULT_UNIFIED_SETTINGS.compactStaleThreadsByDefault ? (
<SettingResetButton
label="compact stale threads by default"
onClick={() =>
updateSettings({
compactStaleThreadsByDefault:
DEFAULT_UNIFIED_SETTINGS.compactStaleThreadsByDefault,
})
}
/>
) : null
}
control={
<Switch
checked={settings.compactStaleThreadsByDefault}
onCheckedChange={(checked) =>
updateSettings({ compactStaleThreadsByDefault: Boolean(checked) })
}
aria-label="Compact stale threads by default"
/>
}
/>

<SettingsRow
{...searchableSetting("send-shortcut")}
description="Choose when Enter sends a prompt or inserts a new line"
Expand Down
6 changes: 6 additions & 0 deletions apps/web/src/components/settings/settingsSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,12 @@ export const SETTINGS_SEARCH_ITEMS = [
to: "/settings/general",
searchTerms: ["composer rest resting scroll wheel conversation timeline shrink minimize"],
},
{
id: "compact-stale-threads",
title: "Compact stale threads by default",
to: "/settings/general",
searchTerms: ["compaction summarize full history prompt cache expired resume claude chip"],
},
{
id: "send-shortcut",
title: "Send shortcut",
Expand Down
3 changes: 2 additions & 1 deletion docs/user/providers-claude.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ You can also send `/compact` in an existing conversation. Web and desktop offer
after Claude's prompt cache expires, a **Compact** chip with the thread's token count shows
next to the send button. While it is on, Enter summarizes the history first, then
sends your message. Click the chip to switch it to **Full** and keep the full
history for that message. The cache lasts about an hour on a Claude
history for that message. To start the chip at **Full** instead, turn off
**Settings → General → Compact stale threads by default**. The cache lasts about an hour on a Claude
subscription and 5 minutes with an API key, Bedrock, or Vertex. See [commands and skills](./composer.md#commands-and-skills) for using
composer commands.

Expand Down
8 changes: 8 additions & 0 deletions packages/contracts/src/settings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,14 @@ describe("ClientSettings composer collapse", () => {
).toBe(false);
});

it("compacts stale threads by default and accepts opting out", () => {
expect(decodeClientSettings({}).compactStaleThreadsByDefault).toBe(true);
expect(
decodeClientSettingsPatch({ compactStaleThreadsByDefault: false })
.compactStaleThreadsByDefault,
).toBe(false);
});

it("drops the retired blur trigger key", () => {
const decoded = decodeClientSettings({ composerCollapseOnBlur: false });
expect(decoded.composerCollapseOnScroll).toBe(true);
Expand Down
6 changes: 6 additions & 0 deletions packages/contracts/src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,11 @@ export const ClientSettingsSchema = Schema.Struct({
// Desktop resting composer: scrolling an existing thread's conversation
// settles the composer into its single-line layout. Losing focus never does.
composerCollapseOnScroll: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
// Starting state of the composer's Compact chip on a stale Claude thread.
// Off starts the chip at Full; either way the chip still flips per message.
compactStaleThreadsByDefault: Schema.Boolean.pipe(
Schema.withDecodingDefault(Effect.succeed(true)),
),
// Rich text is the default; users can opt out for literal Markdown editing.
composerRichTextEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
sendShortcut: Schema.Literals(["enter", "mod-enter-multiline", "mod-enter"]).pipe(
Expand Down Expand Up @@ -1734,6 +1739,7 @@ export const ClientSettingsPatch = Schema.Struct({
planModeEnabled: Schema.optionalKey(Schema.Boolean),
contextWindowMeterEnabled: Schema.optionalKey(Schema.Boolean),
composerCollapseOnScroll: Schema.optionalKey(Schema.Boolean),
compactStaleThreadsByDefault: Schema.optionalKey(Schema.Boolean),
composerRichTextEnabled: Schema.optionalKey(Schema.Boolean),
sendShortcut: Schema.optionalKey(Schema.Literals(["enter", "mod-enter-multiline", "mod-enter"])),
followUpBehavior: Schema.optionalKey(Schema.Literals(["queue", "steer"])),
Expand Down