Skip to content
11 changes: 11 additions & 0 deletions src-tauri/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,11 @@ pub(crate) struct AppSettings {
rename = "showMessageFilePath"
)]
pub(crate) show_message_file_path: bool,
#[serde(
default = "default_chat_history_scrollback_items",
rename = "chatHistoryScrollbackItems"
)]
pub(crate) chat_history_scrollback_items: Option<u32>,
#[serde(default, rename = "threadTitleAutogenerationEnabled")]
pub(crate) thread_title_autogeneration_enabled: bool,
#[serde(default = "default_ui_font_family", rename = "uiFontFamily")]
Expand Down Expand Up @@ -760,6 +765,10 @@ fn default_show_message_file_path() -> bool {
true
}

fn default_chat_history_scrollback_items() -> Option<u32> {
Some(200)
}

fn default_ui_font_family() -> String {
"system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif".to_string()
}
Expand Down Expand Up @@ -1187,6 +1196,7 @@ impl Default for AppSettings {
theme: default_theme(),
usage_show_remaining: default_usage_show_remaining(),
show_message_file_path: default_show_message_file_path(),
chat_history_scrollback_items: default_chat_history_scrollback_items(),
thread_title_autogeneration_enabled: false,
ui_font_family: default_ui_font_family(),
code_font_family: default_code_font_family(),
Expand Down Expand Up @@ -1350,6 +1360,7 @@ mod tests {
assert_eq!(settings.theme, "system");
assert!(!settings.usage_show_remaining);
assert!(settings.show_message_file_path);
assert_eq!(settings.chat_history_scrollback_items, Some(200));
assert!(!settings.thread_title_autogeneration_enabled);
assert!(settings.ui_font_family.contains("system-ui"));
assert!(settings.code_font_family.contains("ui-monospace"));
Expand Down
3 changes: 3 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,9 @@ function MainApp() {
reviewDeliveryMode: appSettings.reviewDeliveryMode,
steerEnabled: appSettings.steerEnabled,
threadTitleAutogenerationEnabled: appSettings.threadTitleAutogenerationEnabled,
chatHistoryScrollbackItems: appSettingsLoading
? null
: appSettings.chatHistoryScrollbackItems,
customPrompts: prompts,
onMessageActivity: handleThreadMessageActivity,
threadSortKey: threadListSortKey,
Expand Down
1 change: 1 addition & 0 deletions src/features/settings/components/SettingsView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ const baseSettings: AppSettings = {
theme: "system",
usageShowRemaining: false,
showMessageFilePath: true,
chatHistoryScrollbackItems: 200,
threadTitleAutogenerationEnabled: false,
uiFontFamily:
'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
// @vitest-environment jsdom
import { fireEvent, render, screen, within } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { cleanup, fireEvent, render, screen, within } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { AppSettings } from "@/types";
import { SettingsDisplaySection } from "./SettingsDisplaySection";

describe("SettingsDisplaySection", () => {
afterEach(() => {
cleanup();
});
it("toggles auto-generated thread titles", () => {
const onUpdateAppSettings = vi.fn(async () => {});

Expand Down Expand Up @@ -58,4 +61,286 @@ describe("SettingsDisplaySection", () => {
expect.objectContaining({ threadTitleAutogenerationEnabled: true }),
);
});
it("toggles unlimited chat history", () => {
const onUpdateAppSettings = vi.fn(async () => {});

render(
<SettingsDisplaySection
appSettings={
({
theme: "system",
usageShowRemaining: false,
showMessageFilePath: true,
chatHistoryScrollbackItems: 200,
threadTitleAutogenerationEnabled: false,
uiFontFamily: "",
codeFontFamily: "",
codeFontSize: 11,
notificationSoundsEnabled: true,
systemNotificationsEnabled: true,
} as unknown) as AppSettings
}
reduceTransparency={false}
scaleShortcutTitle=""
scaleShortcutText=""
scaleDraft="100%"
uiFontDraft=""
codeFontDraft=""
codeFontSizeDraft={11}
onUpdateAppSettings={onUpdateAppSettings}
onToggleTransparency={vi.fn()}
onSetScaleDraft={vi.fn() as any}
onCommitScale={vi.fn(async () => {})}
onResetScale={vi.fn(async () => {})}
onSetUiFontDraft={vi.fn() as any}
onCommitUiFont={vi.fn(async () => {})}
onSetCodeFontDraft={vi.fn() as any}
onCommitCodeFont={vi.fn(async () => {})}
onSetCodeFontSizeDraft={vi.fn() as any}
onCommitCodeFontSize={vi.fn(async () => {})}
onTestNotificationSound={vi.fn()}
onTestSystemNotification={vi.fn()}
/>,
);

const row = screen.getByText("Unlimited chat history").closest(".settings-toggle-row");
expect(row).toBeTruthy();
const button = within(row as HTMLElement).getByRole("button");

fireEvent.click(button);

expect(onUpdateAppSettings).toHaveBeenCalledWith(
expect.objectContaining({ chatHistoryScrollbackItems: null }),
);
});

it("disables scrollback controls when unlimited chat history is enabled", () => {
const onUpdateAppSettings = vi.fn(async () => {});

render(
<SettingsDisplaySection
appSettings={
({
theme: "system",
usageShowRemaining: false,
showMessageFilePath: true,
chatHistoryScrollbackItems: null,
threadTitleAutogenerationEnabled: false,
uiFontFamily: "",
codeFontFamily: "",
codeFontSize: 11,
notificationSoundsEnabled: true,
systemNotificationsEnabled: true,
} as unknown) as AppSettings
}
reduceTransparency={false}
scaleShortcutTitle=""
scaleShortcutText=""
scaleDraft="100%"
uiFontDraft=""
codeFontDraft=""
codeFontSizeDraft={11}
onUpdateAppSettings={onUpdateAppSettings}
onToggleTransparency={vi.fn()}
onSetScaleDraft={vi.fn() as any}
onCommitScale={vi.fn(async () => {})}
onResetScale={vi.fn(async () => {})}
onSetUiFontDraft={vi.fn() as any}
onCommitUiFont={vi.fn(async () => {})}
onSetCodeFontDraft={vi.fn() as any}
onCommitCodeFont={vi.fn(async () => {})}
onSetCodeFontSizeDraft={vi.fn() as any}
onCommitCodeFontSize={vi.fn(async () => {})}
onTestNotificationSound={vi.fn()}
onTestSystemNotification={vi.fn()}
/>,
);

const presetSelect = screen.getByLabelText("Scrollback preset");
expect((presetSelect as HTMLSelectElement).disabled).toBe(true);

const maxItemsInput = screen.getByLabelText("Max items per thread");
expect((maxItemsInput as HTMLInputElement).disabled).toBe(true);

const maxItemsRow = maxItemsInput.closest(".settings-field-row");
expect(maxItemsRow).toBeTruthy();
const resetButton = within(maxItemsRow as HTMLElement).getByRole("button", {
name: "Reset",
});
expect((resetButton as HTMLButtonElement).disabled).toBe(true);

fireEvent.change(presetSelect, { target: { value: "1000" } });
expect(onUpdateAppSettings).not.toHaveBeenCalled();
});

it("applies scrollback presets", () => {
const onUpdateAppSettings = vi.fn(async () => {});

render(
<SettingsDisplaySection
appSettings={
({
theme: "system",
usageShowRemaining: false,
showMessageFilePath: true,
chatHistoryScrollbackItems: 200,
threadTitleAutogenerationEnabled: false,
uiFontFamily: "",
codeFontFamily: "",
codeFontSize: 11,
notificationSoundsEnabled: true,
systemNotificationsEnabled: true,
} as unknown) as AppSettings
}
reduceTransparency={false}
scaleShortcutTitle=""
scaleShortcutText=""
scaleDraft="100%"
uiFontDraft=""
codeFontDraft=""
codeFontSizeDraft={11}
onUpdateAppSettings={onUpdateAppSettings}
onToggleTransparency={vi.fn()}
onSetScaleDraft={vi.fn() as any}
onCommitScale={vi.fn(async () => {})}
onResetScale={vi.fn(async () => {})}
onSetUiFontDraft={vi.fn() as any}
onCommitUiFont={vi.fn(async () => {})}
onSetCodeFontDraft={vi.fn() as any}
onCommitCodeFont={vi.fn(async () => {})}
onSetCodeFontSizeDraft={vi.fn() as any}
onCommitCodeFontSize={vi.fn(async () => {})}
onTestNotificationSound={vi.fn()}
onTestSystemNotification={vi.fn()}
/>,
);

const select = screen.getByLabelText("Scrollback preset");
fireEvent.change(select, { target: { value: "1000" } });

expect(onUpdateAppSettings).toHaveBeenCalledWith(
expect.objectContaining({ chatHistoryScrollbackItems: 1000 }),
);
});

it("does not persist scrollback draft on blur when toggling unlimited", () => {
const onUpdateAppSettings = vi.fn(async () => {});

render(
<SettingsDisplaySection
appSettings={
({
theme: "system",
usageShowRemaining: false,
showMessageFilePath: true,
chatHistoryScrollbackItems: 200,
threadTitleAutogenerationEnabled: false,
uiFontFamily: "",
codeFontFamily: "",
codeFontSize: 11,
notificationSoundsEnabled: true,
systemNotificationsEnabled: true,
} as unknown) as AppSettings
}
reduceTransparency={false}
scaleShortcutTitle=""
scaleShortcutText=""
scaleDraft="100%"
uiFontDraft=""
codeFontDraft=""
codeFontSizeDraft={11}
onUpdateAppSettings={onUpdateAppSettings}
onToggleTransparency={vi.fn()}
onSetScaleDraft={vi.fn() as any}
onCommitScale={vi.fn(async () => {})}
onResetScale={vi.fn(async () => {})}
onSetUiFontDraft={vi.fn() as any}
onCommitUiFont={vi.fn(async () => {})}
onSetCodeFontDraft={vi.fn() as any}
onCommitCodeFont={vi.fn(async () => {})}
onSetCodeFontSizeDraft={vi.fn() as any}
onCommitCodeFontSize={vi.fn(async () => {})}
onTestNotificationSound={vi.fn()}
onTestSystemNotification={vi.fn()}
/>,
);

const maxItemsInput = screen.getByLabelText("Max items per thread");
fireEvent.change(maxItemsInput, { target: { value: "50" } });

const unlimitedRow = screen
.getByText("Unlimited chat history")
.closest(".settings-toggle-row");
expect(unlimitedRow).toBeTruthy();
const unlimitedButton = within(unlimitedRow as HTMLElement).getByRole("button");

fireEvent.blur(maxItemsInput, { relatedTarget: unlimitedButton });
fireEvent.click(unlimitedButton);

expect(onUpdateAppSettings).toHaveBeenCalledTimes(1);
expect(onUpdateAppSettings).toHaveBeenCalledWith(
expect.objectContaining({ chatHistoryScrollbackItems: null }),
);
});

it("does not persist scrollback draft on blur when clicking Reset", () => {
const onUpdateAppSettings = vi.fn(async () => {});

render(
<SettingsDisplaySection
appSettings={
({
theme: "system",
usageShowRemaining: false,
showMessageFilePath: true,
chatHistoryScrollbackItems: 200,
threadTitleAutogenerationEnabled: false,
uiFontFamily: "",
codeFontFamily: "",
codeFontSize: 11,
notificationSoundsEnabled: true,
systemNotificationsEnabled: true,
} as unknown) as AppSettings
}
reduceTransparency={false}
scaleShortcutTitle=""
scaleShortcutText=""
scaleDraft="100%"
uiFontDraft=""
codeFontDraft=""
codeFontSizeDraft={11}
onUpdateAppSettings={onUpdateAppSettings}
onToggleTransparency={vi.fn()}
onSetScaleDraft={vi.fn() as any}
onCommitScale={vi.fn(async () => {})}
onResetScale={vi.fn(async () => {})}
onSetUiFontDraft={vi.fn() as any}
onCommitUiFont={vi.fn(async () => {})}
onSetCodeFontDraft={vi.fn() as any}
onCommitCodeFont={vi.fn(async () => {})}
onSetCodeFontSizeDraft={vi.fn() as any}
onCommitCodeFontSize={vi.fn(async () => {})}
onTestNotificationSound={vi.fn()}
onTestSystemNotification={vi.fn()}
/>,
);

const maxItemsInput = screen.getByLabelText("Max items per thread");
fireEvent.change(maxItemsInput, { target: { value: "50" } });

const maxItemsRow = maxItemsInput.closest(".settings-field-row");
expect(maxItemsRow).toBeTruthy();
const resetButton = within(maxItemsRow as HTMLElement).getByRole("button", {
name: "Reset",
});

fireEvent.blur(maxItemsInput, { relatedTarget: resetButton });
fireEvent.click(resetButton);

expect(onUpdateAppSettings).toHaveBeenCalledTimes(1);
expect(onUpdateAppSettings).toHaveBeenCalledWith(
expect.objectContaining({ chatHistoryScrollbackItems: 200 }),
);
});

});
Loading
Loading