feat(chat): persist more composer input history with global or session scope; recallable with up/down arrow keys (#3035)

* feat(chat): persist input history

* feat(settings): configure input history scope

* fix(web): keep input history validation packaged

* fix(chat): preserve input history across tabs

* fix(settings): restore prompt history limit

* fix(settings): keep history deletion warning visible

* fix(i18n): restore Turkish Git empty state translations

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
Matt Visnovsky
2026-09-05 19:19:03 +03:00
committed by GitHub
co-authored by Bohdan Triapitsyn
parent 29480383cc
commit 1d6b15bc04
49 changed files with 3353 additions and 458 deletions
+3
View File
@@ -1,5 +1,6 @@
import type { WorktreeMetadata } from '@/types/worktree';
import type { DraftStarterRef } from '@/lib/draftStarters';
import type { InputHistoryScope } from '@/lib/inputHistoryScope';
type RuntimePlatform = 'web' | 'desktop' | 'vscode';
@@ -722,6 +723,8 @@ export interface SettingsPayload {
sessionRetentionAction?: 'archive' | 'delete';
followUpBehavior?: 'steer' | 'queue';
queueModeEnabled?: boolean;
inputHistoryScope?: InputHistoryScope;
inputHistoryLimit?: number;
gitmojiEnabled?: boolean;
inputSpellcheckEnabled?: boolean;
enterToSend?: boolean;
+3
View File
@@ -2,6 +2,7 @@ import { z } from 'zod';
import type { ProjectEntry, RuntimeAPIs, TerminalShell } from '@/lib/api/types';
import { getInjectedBootOutcome } from '@/lib/desktopBoot';
import type { DraftStarterRef } from '@/lib/draftStarters';
import type { InputHistoryScope } from '@/lib/inputHistoryScope';
import type { MobileKeyboardMode } from '@/lib/mobileKeyboardMode';
import { getRuntimeApiBaseUrl, getRuntimeKey } from '@/lib/runtime-switch';
import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
@@ -177,6 +178,8 @@ export type DesktopSettings = {
weekStartPreference?: 'auto' | 'sunday' | 'monday';
chatRenderMode?: 'sorted' | 'live';
messageStreamTransport?: 'auto' | 'ws' | 'sse';
inputHistoryScope?: InputHistoryScope;
inputHistoryLimit?: number;
activityRenderMode?: 'collapsed' | 'summary';
mermaidRenderingMode?: 'svg' | 'ascii';
userMessageRenderingMode?: 'markdown' | 'plain';
@@ -2138,6 +2138,15 @@ export const settingsDict = {
'settings.openchamber.visual.field.followUpBehaviorDescription': 'Wählen Sie aus, was passiert, wenn Sie Enter auf einer Follow-up-Nachricht drücken, während der Agent noch antwortet.',
'settings.openchamber.visual.option.followUpBehavior.steer.label': 'Steuerung',
'settings.openchamber.visual.option.followUpBehavior.queue.label': 'Warteschlange',
'settings.openchamber.visual.field.inputHistoryScope': 'Bereich des Eingabeverlaufs',
'settings.openchamber.visual.field.inputHistoryScopeDescription': 'Wählen Sie aus, ob gesendete Prompts projektübergreifend in allen mit dieser Laufzeit verbundenen Projekten oder nur in der aktuellen Sitzung wieder abgerufen werden können.',
'settings.openchamber.visual.section.inputHistoryScopeAria': 'Bereich des Eingabeverlaufs',
'settings.openchamber.visual.option.inputHistoryScope.global.label': 'Alle Projekte',
'settings.openchamber.visual.option.inputHistoryScope.session.label': 'Aktuelle Sitzung',
'settings.openchamber.visual.field.inputHistoryLimit': 'Zu merkende Prompts',
'settings.openchamber.visual.field.inputHistoryLimitDescription': 'Wenn Sie diese Zahl verringern, werden ältere Prompts sofort aus Ihrem Verlauf entfernt.',
'settings.openchamber.visual.field.inputHistoryLimitAria': 'Zu merkende Prompts',
'settings.openchamber.visual.field.inputHistoryLimitUnit': 'Prompts',
'settings.providers.page.quotaCredentials.accessToken': 'Zugriffstoken',
'settings.providers.page.quotaCredentials.usageToken': 'Nutzungs-API-Token',
@@ -2227,6 +2227,15 @@ export const settingsDict = {
'settings.openchamber.visual.field.followUpBehaviorDescription': 'Choose what happens when you press Enter on a follow-up message while the agent is still responding.',
'settings.openchamber.visual.option.followUpBehavior.steer.label': 'Steer',
'settings.openchamber.visual.option.followUpBehavior.queue.label': 'Queue',
'settings.openchamber.visual.field.inputHistoryScope': 'Input history scope',
'settings.openchamber.visual.field.inputHistoryScopeDescription': 'Choose whether submitted prompts are recalled across all projects connected to this runtime or only in the current session.',
'settings.openchamber.visual.section.inputHistoryScopeAria': 'Input history scope',
'settings.openchamber.visual.option.inputHistoryScope.global.label': 'All projects',
'settings.openchamber.visual.option.inputHistoryScope.session.label': 'Current session',
'settings.openchamber.visual.field.inputHistoryLimit': 'Prompts to remember',
'settings.openchamber.visual.field.inputHistoryLimitDescription': 'Lowering this number removes older prompts from your history.',
'settings.openchamber.visual.field.inputHistoryLimitAria': 'Prompts to remember',
'settings.openchamber.visual.field.inputHistoryLimitUnit': 'prompts',
...linearIntegrationI18n.en,
'settings.page.integrations.title': 'Integrations',
'settings.page.integrations.description': 'Connect GitHub and Linear so OpenChamber can work with your issues and pull requests.',
@@ -2237,6 +2237,15 @@ export const settingsDict = {
"settings.openchamber.visual.field.followUpBehaviorDescription": "Choose what happens when you press Enter on a follow-up message while the agent is still responding.",
"settings.openchamber.visual.option.followUpBehavior.steer.label": "Steer",
"settings.openchamber.visual.option.followUpBehavior.queue.label": "Queue",
"settings.openchamber.visual.field.inputHistoryScope": "Alcance del historial de entrada",
"settings.openchamber.visual.field.inputHistoryScopeDescription": "Elige si los prompts enviados se recuperan en todos los proyectos conectados a este runtime o solo en la sesión actual.",
"settings.openchamber.visual.section.inputHistoryScopeAria": "Alcance del historial de entrada",
"settings.openchamber.visual.option.inputHistoryScope.global.label": "Todos los proyectos",
"settings.openchamber.visual.option.inputHistoryScope.session.label": "Sesión actual",
"settings.openchamber.visual.field.inputHistoryLimit": "Prompts que recordar",
"settings.openchamber.visual.field.inputHistoryLimitDescription": "Bajar este número elimina de inmediato los prompts más antiguos de tu historial.",
"settings.openchamber.visual.field.inputHistoryLimitAria": "Prompts que recordar",
"settings.openchamber.visual.field.inputHistoryLimitUnit": "prompts",
...linearIntegrationI18n.es,
'settings.page.integrations.title': 'Integraciones',
'settings.page.integrations.description': 'Conecta GitHub y Linear para que OpenChamber pueda trabajar con tus issues y pull requests.',
@@ -2237,6 +2237,15 @@ export const settingsDict = {
'settings.openchamber.visual.field.followUpBehaviorDescription': 'Choose what happens when you press Enter on a follow-up message while the agent is still responding.',
'settings.openchamber.visual.option.followUpBehavior.steer.label': 'Steer',
'settings.openchamber.visual.option.followUpBehavior.queue.label': 'Queue',
'settings.openchamber.visual.field.inputHistoryScope': 'Portée de l\'historique de saisie',
'settings.openchamber.visual.field.inputHistoryScopeDescription': 'Choisissez si les prompts envoyés sont rappelés dans tous les projets reliés à ce runtime ou seulement dans la session en cours.',
'settings.openchamber.visual.section.inputHistoryScopeAria': 'Portée de l\'historique de saisie',
'settings.openchamber.visual.option.inputHistoryScope.global.label': 'Tous les projets',
'settings.openchamber.visual.option.inputHistoryScope.session.label': 'Session actuelle',
'settings.openchamber.visual.field.inputHistoryLimit': 'Prompts à mémoriser',
'settings.openchamber.visual.field.inputHistoryLimitDescription': 'Réduire ce nombre supprime aussitôt les prompts les plus anciens de votre historique.',
'settings.openchamber.visual.field.inputHistoryLimitAria': 'Prompts à mémoriser',
'settings.openchamber.visual.field.inputHistoryLimitUnit': 'prompts',
...linearIntegrationI18n.fr,
'settings.page.integrations.title': 'Intégrations',
'settings.page.integrations.description': 'Connectez GitHub et Linear pour quOpenChamber puisse travailler avec vos issues et pull requests.',
@@ -2237,6 +2237,15 @@ export const settingsDict = {
'settings.openchamber.visual.field.followUpBehaviorDescription': 'エージェントが応答している間にフォローアップメッセージで Enter を押したときの動作を選択します。',
'settings.openchamber.visual.option.followUpBehavior.steer.label': 'ステア',
'settings.openchamber.visual.option.followUpBehavior.queue.label': 'キュー',
'settings.openchamber.visual.field.inputHistoryScope': '入力履歴の範囲',
'settings.openchamber.visual.field.inputHistoryScopeDescription': '送信したプロンプトを、このランタイムに接続されたすべてのプロジェクトで呼び出せるようにするか、現在のセッションだけにするかを選びます。',
'settings.openchamber.visual.section.inputHistoryScopeAria': '入力履歴の範囲',
'settings.openchamber.visual.option.inputHistoryScope.global.label': 'すべてのプロジェクト',
'settings.openchamber.visual.option.inputHistoryScope.session.label': '現在のセッション',
'settings.openchamber.visual.field.inputHistoryLimit': '記憶するプロンプト数',
'settings.openchamber.visual.field.inputHistoryLimitDescription': 'この数を減らすと、履歴内の古いプロンプトはすぐに削除されます。',
'settings.openchamber.visual.field.inputHistoryLimitAria': '記憶するプロンプト数',
'settings.openchamber.visual.field.inputHistoryLimitUnit': '件',
...linearIntegrationI18n.ja,
'settings.page.integrations.title': '連携',
'settings.page.integrations.description': 'GitHub と Linear を接続すると、OpenChamber が Issue やプルリクエストを扱えるようになります。',
@@ -2237,6 +2237,15 @@ export const settingsDict = {
'settings.openchamber.visual.field.followUpBehaviorDescription': 'Choose what happens when you press Enter on a follow-up message while the agent is still responding.',
'settings.openchamber.visual.option.followUpBehavior.steer.label': 'Steer',
'settings.openchamber.visual.option.followUpBehavior.queue.label': 'Queue',
'settings.openchamber.visual.field.inputHistoryScope': '입력 기록 범위',
'settings.openchamber.visual.field.inputHistoryScopeDescription': '보낸 프롬프트를 이 런타임에 연결된 모든 프로젝트에서 다시 불러올지, 현재 세션에서만 다시 불러올지 선택합니다.',
'settings.openchamber.visual.section.inputHistoryScopeAria': '입력 기록 범위',
'settings.openchamber.visual.option.inputHistoryScope.global.label': '모든 프로젝트',
'settings.openchamber.visual.option.inputHistoryScope.session.label': '현재 세션',
'settings.openchamber.visual.field.inputHistoryLimit': '기억할 프롬프트 수',
'settings.openchamber.visual.field.inputHistoryLimitDescription': '이 숫자를 낮추면 기록에서 오래된 프롬프트가 바로 삭제됩니다.',
'settings.openchamber.visual.field.inputHistoryLimitAria': '기억할 프롬프트 수',
'settings.openchamber.visual.field.inputHistoryLimitUnit': '개',
...linearIntegrationI18n.ko,
'settings.page.integrations.title': '통합',
'settings.page.integrations.description': 'GitHub와 Linear를 연결하면 OpenChamber가 이슈와 풀 리퀘스트를 다룰 수 있습니다.',
@@ -2230,6 +2230,15 @@ export const settingsDict = {
'settings.openchamber.visual.field.followUpBehaviorDescription': 'Choose what happens when you press Enter on a follow-up message while the agent is still responding.',
'settings.openchamber.visual.option.followUpBehavior.steer.label': 'Steer',
'settings.openchamber.visual.option.followUpBehavior.queue.label': 'Queue',
'settings.openchamber.visual.field.inputHistoryScope': 'Zakres historii wpisów',
'settings.openchamber.visual.field.inputHistoryScopeDescription': 'Wybierz, czy wysłane prompty mają być przywoływane we wszystkich projektach podłączonych do tego środowiska uruchomieniowego, czy tylko w bieżącej sesji.',
'settings.openchamber.visual.section.inputHistoryScopeAria': 'Zakres historii wpisów',
'settings.openchamber.visual.option.inputHistoryScope.global.label': 'Wszystkie projekty',
'settings.openchamber.visual.option.inputHistoryScope.session.label': 'Bieżąca sesja',
'settings.openchamber.visual.field.inputHistoryLimit': 'Liczba zapamiętywanych promptów',
'settings.openchamber.visual.field.inputHistoryLimitDescription': 'Zmniejszenie tej liczby od razu usuwa starsze prompty z historii.',
'settings.openchamber.visual.field.inputHistoryLimitAria': 'Liczba zapamiętywanych promptów',
'settings.openchamber.visual.field.inputHistoryLimitUnit': 'promptów',
...linearIntegrationI18n.pl,
'settings.page.integrations.title': 'Integracje',
'settings.page.integrations.description': 'Połącz GitHub i Linear, aby OpenChamber mógł pracować z Twoimi issue i pull requestami.',
@@ -2237,6 +2237,15 @@ export const settingsDict = {
"settings.openchamber.visual.field.followUpBehaviorDescription": "Choose what happens when you press Enter on a follow-up message while the agent is still responding.",
"settings.openchamber.visual.option.followUpBehavior.steer.label": "Steer",
"settings.openchamber.visual.option.followUpBehavior.queue.label": "Queue",
"settings.openchamber.visual.field.inputHistoryScope": "Escopo do histórico de entrada",
"settings.openchamber.visual.field.inputHistoryScopeDescription": "Escolha se os prompts enviados podem ser recuperados em todos os projetos conectados a este runtime ou apenas na sessão atual.",
"settings.openchamber.visual.section.inputHistoryScopeAria": "Escopo do histórico de entrada",
"settings.openchamber.visual.option.inputHistoryScope.global.label": "Todos os projetos",
"settings.openchamber.visual.option.inputHistoryScope.session.label": "Sessão atual",
"settings.openchamber.visual.field.inputHistoryLimit": "Prompts para lembrar",
"settings.openchamber.visual.field.inputHistoryLimitDescription": "Reduzir esse número remove na hora os prompts mais antigos do seu histórico.",
"settings.openchamber.visual.field.inputHistoryLimitAria": "Prompts para lembrar",
"settings.openchamber.visual.field.inputHistoryLimitUnit": "prompts",
...linearIntegrationI18n['pt-BR'],
'settings.page.integrations.title': 'Integrações',
'settings.page.integrations.description': 'Conecte o GitHub e o Linear para que o OpenChamber possa trabalhar com suas issues e pull requests.',
@@ -1950,6 +1950,15 @@ export const settingsDict = {
'settings.openchamber.visual.actions.resetSpacingAria': 'Aralığı sıfırla',
'settings.openchamber.visual.field.inputBarOffset': 'Giriş Çubuğu Ofseti',
'settings.openchamber.visual.field.inputBarOffsetTooltip': 'Ana ekran çubuğu gibi işletim sistemi düzeyindeki ekran engellerinden kaçınmak için giriş çubuğunu yukarı kaldırır.',
'settings.openchamber.visual.field.inputHistoryScope': 'Girdi geçmişi kapsamı',
'settings.openchamber.visual.field.inputHistoryScopeDescription': 'Gönderilen istemlerin bu çalışma zamanına bağlı tüm projelerde mi yoksa yalnızca geçerli oturumda mı geri çağrılacağını seçin.',
'settings.openchamber.visual.section.inputHistoryScopeAria': 'Girdi geçmişi kapsamı',
'settings.openchamber.visual.option.inputHistoryScope.global.label': 'Tüm projeler',
'settings.openchamber.visual.option.inputHistoryScope.session.label': 'Geçerli oturum',
'settings.openchamber.visual.field.inputHistoryLimit': 'Hatırlanacak prompt sayısı',
'settings.openchamber.visual.field.inputHistoryLimitDescription': 'Bu sayıyı azaltmak, eski prompt\'ları geçmişinizden hemen siler.',
'settings.openchamber.visual.field.inputHistoryLimitAria': 'Hatırlanacak prompt sayısı',
'settings.openchamber.visual.field.inputHistoryLimitUnit': 'prompt',
'settings.openchamber.visual.actions.resetInputBarOffsetAria': 'Giriş çubuğu ofsetini sıfırla',
'settings.openchamber.visual.field.terminalQuickKeysAria': 'Terminal hızlı tuşları',
'settings.openchamber.visual.field.terminalQuickKeys': 'Terminal Hızlı Tuşları',
@@ -2237,6 +2237,15 @@ export const settingsDict = {
"settings.openchamber.visual.field.followUpBehaviorDescription": "Choose what happens when you press Enter on a follow-up message while the agent is still responding.",
"settings.openchamber.visual.option.followUpBehavior.steer.label": "Steer",
"settings.openchamber.visual.option.followUpBehavior.queue.label": "Queue",
"settings.openchamber.visual.field.inputHistoryScope": "Обсяг історії введення",
"settings.openchamber.visual.field.inputHistoryScopeDescription": "Виберіть, чи згадувати надіслані промпти в усіх проєктах, підключених до цього рантайму, чи лише в поточній сесії.",
"settings.openchamber.visual.section.inputHistoryScopeAria": "Обсяг історії введення",
"settings.openchamber.visual.option.inputHistoryScope.global.label": "Усі проєкти",
"settings.openchamber.visual.option.inputHistoryScope.session.label": "Поточна сесія",
"settings.openchamber.visual.field.inputHistoryLimit": "Скільки промптів пам’ятати",
"settings.openchamber.visual.field.inputHistoryLimitDescription": "Якщо зменшити це число, старіші промпти одразу буде видалено з історії.",
"settings.openchamber.visual.field.inputHistoryLimitAria": "Скільки промптів пам’ятати",
"settings.openchamber.visual.field.inputHistoryLimitUnit": "промптів",
...linearIntegrationI18n.uk,
'settings.page.integrations.title': 'Інтеграції',
'settings.page.integrations.description': 'Підключіть GitHub і Linear, щоб OpenChamber міг працювати з вашими задачами та pull request-ами.',
@@ -2237,6 +2237,15 @@ export const settingsDict = {
'settings.openchamber.visual.field.followUpBehaviorDescription': 'Choose what happens when you press Enter on a follow-up message while the agent is still responding.',
'settings.openchamber.visual.option.followUpBehavior.steer.label': 'Steer',
'settings.openchamber.visual.option.followUpBehavior.queue.label': 'Queue',
'settings.openchamber.visual.field.inputHistoryScope': '输入历史范围',
'settings.openchamber.visual.field.inputHistoryScopeDescription': '选择已提交的提示词是要在此运行时连接的所有项目中都可回忆,还是仅限当前会话。',
'settings.openchamber.visual.section.inputHistoryScopeAria': '输入历史范围',
'settings.openchamber.visual.option.inputHistoryScope.global.label': '所有项目',
'settings.openchamber.visual.option.inputHistoryScope.session.label': '当前会话',
'settings.openchamber.visual.field.inputHistoryLimit': '要记住的提示词数量',
'settings.openchamber.visual.field.inputHistoryLimitDescription': '调低这个数字会立即从历史记录中删除较早的提示词。',
'settings.openchamber.visual.field.inputHistoryLimitAria': '要记住的提示词数量',
'settings.openchamber.visual.field.inputHistoryLimitUnit': '条',
...linearIntegrationI18n['zh-CN'],
'settings.page.integrations.title': '集成',
'settings.page.integrations.description': '连接 GitHub 和 Linear,让 OpenChamber 可以处理你的 issue 和拉取请求。',
@@ -2237,6 +2237,15 @@ export const settingsDict = {
'settings.openchamber.visual.field.followUpBehaviorDescription': 'Choose what happens when you press Enter on a follow-up message while the agent is still responding.',
'settings.openchamber.visual.option.followUpBehavior.steer.label': 'Steer',
'settings.openchamber.visual.option.followUpBehavior.queue.label': 'Queue',
'settings.openchamber.visual.field.inputHistoryScope': '輸入歷史範圍',
'settings.openchamber.visual.field.inputHistoryScopeDescription': '選擇已送出的提示詞要在連接到這個執行環境的所有專案中都能回叫,還是只限目前工作階段。',
'settings.openchamber.visual.section.inputHistoryScopeAria': '輸入歷史範圍',
'settings.openchamber.visual.option.inputHistoryScope.global.label': '所有專案',
'settings.openchamber.visual.option.inputHistoryScope.session.label': '目前工作階段',
'settings.openchamber.visual.field.inputHistoryLimit': '要記住的提示詞數量',
'settings.openchamber.visual.field.inputHistoryLimitDescription': '調低這個數字會立刻從歷史記錄移除較早的提示詞。',
'settings.openchamber.visual.field.inputHistoryLimitAria': '要記住的提示詞數量',
'settings.openchamber.visual.field.inputHistoryLimitUnit': '則',
...linearIntegrationI18n['zh-TW'],
'settings.page.integrations.title': '整合',
'settings.page.integrations.description': '連接 GitHub 和 Linear,讓 OpenChamber 可以處理你的 issue 和 pull request。',
+18
View File
@@ -0,0 +1,18 @@
export type InputHistoryScope = 'global' | 'session';
export const DEFAULT_INPUT_HISTORY_SCOPE: InputHistoryScope = 'global';
export const DEFAULT_INPUT_HISTORY_LIMIT = 40;
export const MIN_INPUT_HISTORY_LIMIT = 1;
export const MAX_INPUT_HISTORY_LIMIT = 100;
export const isInputHistoryScope = (value: string | null | undefined): value is InputHistoryScope => (
value === 'global' || value === 'session'
);
export const isInputHistoryLimit = (value: number | null | undefined): value is number => (
value !== null
&& value !== undefined
&& Number.isInteger(value)
&& value >= MIN_INPUT_HISTORY_LIMIT
&& value <= MAX_INPUT_HISTORY_LIMIT
);
+177
View File
@@ -4,6 +4,11 @@ import type { RuntimeAPIs, SettingsPayload } from '@/lib/api/types';
import { registerRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
import { startModelPrefsAutoSave } from '@/lib/modelPrefsAutoSave';
import { startAppearanceAutoSave } from '@/lib/appearanceAutoSave';
import {
DEFAULT_INPUT_HISTORY_LIMIT,
DEFAULT_INPUT_HISTORY_SCOPE,
} from '@/lib/inputHistoryScope';
import { useInputHistoryStore } from '@/stores/useInputHistoryStore';
import { useUIStore } from '@/stores/useUIStore';
import { useMessageQueueStore } from '@/stores/messageQueueStore';
import { useSessionDisplayStore } from '@/stores/useSessionDisplayStore';
@@ -30,6 +35,8 @@ type TestWindow = {
let createdWindow = false;
let createdLocalStorage = false;
const originalInputHistoryApplyScope = useInputHistoryStore.getState().applyScope;
const originalInputHistoryApplyEntryLimit = useInputHistoryStore.getState().applyEntryLimit;
const ensureLocalStorage = (): void => {
if (typeof localStorage !== 'undefined') {
@@ -156,6 +163,14 @@ describe('updateDesktopSettings', () => {
registerRuntimeAPIs(null);
invalidateSettingsCache();
resetModelPrefsState();
useInputHistoryStore.setState({
entryLimit: DEFAULT_INPUT_HISTORY_LIMIT,
scope: DEFAULT_INPUT_HISTORY_SCOPE,
globalBuckets: {},
sessionBuckets: {},
applyEntryLimit: originalInputHistoryApplyEntryLimit,
applyScope: originalInputHistoryApplyScope,
});
});
test('waits for the debounced settings save to finish before resolving', async () => {
@@ -574,6 +589,24 @@ describe('updateDesktopSettings', () => {
});
});
test('applies validated input history scope from shared settings save responses', async () => {
getWindow();
registerSettingsSave(async () => ({ inputHistoryScope: 'session' }));
await updateDesktopSettings({ inputHistoryScope: 'session' });
expect(useInputHistoryStore.getState().scope).toBe('session');
});
test('applies validated input history limit from shared settings save responses', async () => {
getWindow();
registerSettingsSave(async () => ({ inputHistoryLimit: 100 }));
await updateDesktopSettings({ inputHistoryLimit: 100 });
expect(useInputHistoryStore.getState().entryLimit).toBe(100);
});
test('does not broadcast a stale project selection over a newer pending update', async () => {
const firstSave = deferred<SettingsPayload>();
const savedChanges: Array<Partial<SettingsPayload>> = [];
@@ -816,6 +849,150 @@ describe('updateDesktopSettings', () => {
expect(useUIStore.getState().autoSaveEnabled).toBe(false);
});
test('applies persisted input history scope from server settings', async () => {
getWindow();
invalidateSettingsCache();
registerSettingsApi(async () => ({}), async () => ({
settings: {
inputHistoryScope: 'session',
autoSaveEnabled: true,
draftStartersCraftGoalAdded: true,
draftStartersScheduleTaskAdded: true,
},
source: 'web',
}));
await syncDesktopSettings();
expect(useInputHistoryStore.getState().scope).toBe('session');
});
test('applies persisted input history limit from server settings', async () => {
getWindow();
invalidateSettingsCache();
registerSettingsApi(async () => ({}), async () => ({
settings: {
inputHistoryLimit: 100,
autoSaveEnabled: true,
draftStartersCraftGoalAdded: true,
draftStartersScheduleTaskAdded: true,
},
source: 'web',
}));
await syncDesktopSettings();
expect(useInputHistoryStore.getState().entryLimit).toBe(100);
});
test('defaults omitted input history scope to global without writing a migration', async () => {
getWindow();
invalidateSettingsCache();
useInputHistoryStore.getState().applyScope('session');
const saveCalls: Array<Partial<SettingsPayload>> = [];
registerSettingsApi(async (changes) => {
saveCalls.push(changes);
return changes as SettingsPayload;
}, async () => ({
settings: {
autoSaveEnabled: true,
draftStartersCraftGoalAdded: true,
draftStartersScheduleTaskAdded: true,
},
source: 'web',
}));
await syncDesktopSettings();
expect(useInputHistoryStore.getState().scope).toBe(DEFAULT_INPUT_HISTORY_SCOPE);
expect(saveCalls.some((changes) => changes.inputHistoryScope !== undefined)).toBe(false);
});
test('defaults omitted input history limit to forty without writing a migration', async () => {
getWindow();
invalidateSettingsCache();
useInputHistoryStore.getState().applyEntryLimit(100);
const saveCalls: Array<Partial<SettingsPayload>> = [];
registerSettingsApi(async (changes) => {
saveCalls.push(changes);
return changes as SettingsPayload;
}, async () => ({
settings: {
autoSaveEnabled: true,
draftStartersCraftGoalAdded: true,
draftStartersScheduleTaskAdded: true,
},
source: 'web',
}));
await syncDesktopSettings();
expect(useInputHistoryStore.getState().entryLimit).toBe(DEFAULT_INPUT_HISTORY_LIMIT);
expect(saveCalls.some((changes) => changes.inputHistoryLimit !== undefined)).toBe(false);
});
test('does not reapply the hydrated input history scope when it already matches', async () => {
getWindow();
invalidateSettingsCache();
useInputHistoryStore.getState().applyScope('session');
let applyScopeCalls = 0;
useInputHistoryStore.setState({
applyScope: (scope) => {
applyScopeCalls += 1;
originalInputHistoryApplyScope(scope);
},
});
registerSettingsApi(async () => ({}), async () => ({
settings: {
inputHistoryScope: 'session',
autoSaveEnabled: true,
draftStartersCraftGoalAdded: true,
draftStartersScheduleTaskAdded: true,
},
source: 'web',
}));
try {
await syncDesktopSettings();
} finally {
useInputHistoryStore.setState({ applyScope: originalInputHistoryApplyScope });
}
expect(applyScopeCalls).toBe(0);
expect(useInputHistoryStore.getState().scope).toBe('session');
});
test('does not reapply the hydrated input history limit when it already matches', async () => {
getWindow();
invalidateSettingsCache();
useInputHistoryStore.getState().applyEntryLimit(100);
let applyEntryLimitCalls = 0;
useInputHistoryStore.setState({
applyEntryLimit: (limit) => {
applyEntryLimitCalls += 1;
originalInputHistoryApplyEntryLimit(limit);
},
});
registerSettingsApi(async () => ({}), async () => ({
settings: {
inputHistoryLimit: 100,
autoSaveEnabled: true,
draftStartersCraftGoalAdded: true,
draftStartersScheduleTaskAdded: true,
},
source: 'web',
}));
try {
await syncDesktopSettings();
} finally {
useInputHistoryStore.setState({ applyEntryLimit: originalInputHistoryApplyEntryLimit });
}
expect(applyEntryLimitCalls).toBe(0);
expect(useInputHistoryStore.getState().entryLimit).toBe(100);
});
test('autosaves autoSaveEnabled changes to shared settings', async () => {
getWindow();
useUIStore.getState().setAutoSaveEnabled(true);
+30
View File
@@ -15,6 +15,15 @@ import { setFilesViewShowGitignored } from '@/lib/filesViewShowGitignored';
import { loadAppearancePreferences, applyAppearancePreferences } from '@/lib/appearancePersistence';
import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
import { sanitizeStarterRefs } from '@/lib/draftStarters';
import {
DEFAULT_INPUT_HISTORY_LIMIT,
DEFAULT_INPUT_HISTORY_SCOPE,
isInputHistoryLimit,
isInputHistoryScope,
} from '@/lib/inputHistoryScope';
import {
useInputHistoryStore,
} from '@/stores/useInputHistoryStore';
import { normalizeMobileKeyboardMode, setStoredMobileKeyboardMode } from '@/lib/mobileKeyboardMode';
import { runtimeFetch } from '@/lib/runtime-fetch';
import { isCapacitorApp } from '@/lib/platform';
@@ -76,6 +85,8 @@ const persistRuntimeSettingsMirror = (settings: DesktopSettings, runtimeKey: str
pwaAppName: settings.pwaAppName,
mobileKeyboardMode: settings.mobileKeyboardMode,
openCodeUpdateToastDismissedVersion: settings.openCodeUpdateToastDismissedVersion,
inputHistoryScope: settings.inputHistoryScope,
inputHistoryLimit: settings.inputHistoryLimit,
dictationEnabled: settings.dictationEnabled,
sttProvider: settings.sttProvider,
sttServerUrl: settings.sttServerUrl,
@@ -595,6 +606,8 @@ const materializeAuthoritativeUiSettings = (settings: DesktopSettings): DesktopS
userMessageRenderingMode: defaults.userMessageRenderingMode,
collapsibleUserMessages: defaults.collapsibleUserMessages,
messageStreamTransport: 'auto',
inputHistoryScope: DEFAULT_INPUT_HISTORY_SCOPE,
inputHistoryLimit: DEFAULT_INPUT_HISTORY_LIMIT,
stickyUserHeader: defaults.stickyUserHeader,
promptNavigatorEnabled: defaults.promptNavigatorEnabled,
wideChatLayoutEnabled: defaults.wideChatLayoutEnabled,
@@ -643,6 +656,7 @@ const applyDesktopUiPreferences = (settings: DesktopSettings) => {
? window.__zustand_config_store__ ?? null
: null;
const queueStore = useMessageQueueStore.getState();
const inputHistoryStore = useInputHistoryStore.getState();
if (typeof settings.workStatusPanelEnabled === 'boolean'
&& settings.workStatusPanelEnabled !== store.workStatusPanelEnabled) {
@@ -868,6 +882,16 @@ const applyDesktopUiPreferences = (settings: DesktopSettings) => {
configStore.setSettingsMessageStreamTransport(settings.messageStreamTransport);
}
}
if (
typeof settings.inputHistoryScope === 'string'
&& isInputHistoryScope(settings.inputHistoryScope)
&& settings.inputHistoryScope !== inputHistoryStore.scope
) {
inputHistoryStore.applyScope(settings.inputHistoryScope);
}
if (isInputHistoryLimit(settings.inputHistoryLimit) && settings.inputHistoryLimit !== inputHistoryStore.entryLimit) {
inputHistoryStore.applyEntryLimit(settings.inputHistoryLimit);
}
if (typeof settings.stickyUserHeader === 'boolean' && settings.stickyUserHeader !== store.stickyUserHeader) {
store.setStickyUserHeader(settings.stickyUserHeader);
}
@@ -1203,6 +1227,12 @@ const sanitizeWebSettings = (payload: unknown): DesktopSettings | null => {
if (typeof candidate.streamingAutoFollowEnabled === 'boolean') {
result.streamingAutoFollowEnabled = candidate.streamingAutoFollowEnabled;
}
if (typeof candidate.inputHistoryScope === 'string' && isInputHistoryScope(candidate.inputHistoryScope)) {
result.inputHistoryScope = candidate.inputHistoryScope;
}
if (typeof candidate.inputHistoryLimit === 'number' && isInputHistoryLimit(candidate.inputHistoryLimit)) {
result.inputHistoryLimit = candidate.inputHistoryLimit;
}
if (typeof candidate.sessionRecapEnabled === 'boolean') {
result.sessionRecapEnabled = candidate.sessionRecapEnabled;
}
@@ -30,6 +30,28 @@ describe('settings search', () => {
expect(results.some((result) => result.id === 'integrations.linear.mapping')).toBe(true);
});
test('finds the chat input history scope setting', () => {
const results = buildSettingsSearchResults({
query: 'input history scope',
runtimeCtx,
t,
getPageTitle: (page) => page,
});
expect(results.some((result) => result.id === 'chat.input-history-scope')).toBe(true);
});
test('finds the chat input history limit setting by recall keywords', () => {
const results = buildSettingsSearchResults({
query: 'remember prompts',
runtimeCtx,
t,
getPageTitle: (page) => page,
});
expect(results.some((result) => result.id === 'chat.input-history-limit')).toBe(true);
});
test('hides Linear connect in VS Code', () => {
const results = buildSettingsSearchResults({
query: 'linear',
+14
View File
@@ -341,6 +341,20 @@ const SETTINGS_SEARCH_ITEMS: readonly SettingsSearchItem[] = [
descriptionKey: 'settings.openchamber.visual.field.followUpBehaviorDescription',
keywords: ['follow up', 'queue', 'steer', 'send immediately'],
},
{
id: 'chat.input-history-scope',
page: 'chat',
titleKey: 'settings.openchamber.visual.field.inputHistoryScope',
descriptionKey: 'settings.openchamber.visual.field.inputHistoryScopeDescription',
keywords: ['input history', 'composer history', 'global', 'session', 'reuse'],
},
{
id: 'chat.input-history-limit',
page: 'chat',
titleKey: 'settings.openchamber.visual.field.inputHistoryLimit',
descriptionKey: 'settings.openchamber.visual.field.inputHistoryLimitDescription',
keywords: ['history limit', 'prompt recall', 'remember prompts', 'composer history', 'submitted prompts', 'trim history'],
},
{
id: 'chat.persist-drafts',
page: 'chat',