mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -07:00
fix(cli): preserve input history after /clear command (#5890)
Co-authored-by: Allen Hutchison <adh@google.com>
This commit is contained in:
committed by
GitHub
parent
5e5f2dffc0
commit
6f91cfa9a3
@@ -233,6 +233,14 @@ vi.mock('./hooks/useLogger', () => ({
|
|||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('./hooks/useInputHistoryStore.js', () => ({
|
||||||
|
useInputHistoryStore: vi.fn(() => ({
|
||||||
|
inputHistory: [],
|
||||||
|
addInput: vi.fn(),
|
||||||
|
initializeFromLogger: vi.fn(),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock('./hooks/useConsoleMessages.js', () => ({
|
vi.mock('./hooks/useConsoleMessages.js', () => ({
|
||||||
useConsoleMessages: vi.fn(() => ({
|
useConsoleMessages: vi.fn(() => ({
|
||||||
consoleMessages: [],
|
consoleMessages: [],
|
||||||
|
|||||||
+16
-39
@@ -56,6 +56,7 @@ import { DetailedMessagesDisplay } from './components/DetailedMessagesDisplay.js
|
|||||||
import { HistoryItemDisplay } from './components/HistoryItemDisplay.js';
|
import { HistoryItemDisplay } from './components/HistoryItemDisplay.js';
|
||||||
import { ContextSummaryDisplay } from './components/ContextSummaryDisplay.js';
|
import { ContextSummaryDisplay } from './components/ContextSummaryDisplay.js';
|
||||||
import { useHistory } from './hooks/useHistoryManager.js';
|
import { useHistory } from './hooks/useHistoryManager.js';
|
||||||
|
import { useInputHistoryStore } from './hooks/useInputHistoryStore.js';
|
||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
import type { EditorType, Config, IdeContext } from '@google/gemini-cli-core';
|
import type { EditorType, Config, IdeContext } from '@google/gemini-cli-core';
|
||||||
import {
|
import {
|
||||||
@@ -624,7 +625,8 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
|||||||
shellModeActive,
|
shellModeActive,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [userMessages, setUserMessages] = useState<string[]>([]);
|
// Independent input history management (unaffected by /clear)
|
||||||
|
const inputHistoryStore = useInputHistoryStore();
|
||||||
|
|
||||||
// Stable reference for cancel handler to avoid circular dependency
|
// Stable reference for cancel handler to avoid circular dependency
|
||||||
const cancelHandlerRef = useRef<() => void>(() => {});
|
const cancelHandlerRef = useRef<() => void>(() => {});
|
||||||
@@ -673,7 +675,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastUserMessage = userMessages.at(-1);
|
const lastUserMessage = inputHistoryStore.inputHistory.at(-1);
|
||||||
let textToSet = lastUserMessage || '';
|
let textToSet = lastUserMessage || '';
|
||||||
|
|
||||||
// Append queued messages if any exist
|
// Append queued messages if any exist
|
||||||
@@ -688,7 +690,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
|||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
buffer,
|
buffer,
|
||||||
userMessages,
|
inputHistoryStore.inputHistory,
|
||||||
getQueuedMessagesText,
|
getQueuedMessagesText,
|
||||||
clearQueue,
|
clearQueue,
|
||||||
pendingHistoryItems,
|
pendingHistoryItems,
|
||||||
@@ -697,9 +699,15 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
|||||||
// Input handling - queue messages for processing
|
// Input handling - queue messages for processing
|
||||||
const handleFinalSubmit = useCallback(
|
const handleFinalSubmit = useCallback(
|
||||||
(submittedValue: string) => {
|
(submittedValue: string) => {
|
||||||
|
const trimmedValue = submittedValue.trim();
|
||||||
|
if (trimmedValue.length > 0) {
|
||||||
|
// Add to independent input history
|
||||||
|
inputHistoryStore.addInput(trimmedValue);
|
||||||
|
}
|
||||||
|
// Always add to message queue
|
||||||
addMessage(submittedValue);
|
addMessage(submittedValue);
|
||||||
},
|
},
|
||||||
[addMessage],
|
[addMessage, inputHistoryStore],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleIdePromptComplete = useCallback(
|
const handleIdePromptComplete = useCallback(
|
||||||
@@ -842,41 +850,10 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
|||||||
|
|
||||||
const logger = useLogger(config.storage);
|
const logger = useLogger(config.storage);
|
||||||
|
|
||||||
|
// Initialize independent input history from logger
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchUserMessages = async () => {
|
inputHistoryStore.initializeFromLogger(logger);
|
||||||
const pastMessagesRaw = (await logger?.getPreviousUserMessages()) || []; // Newest first
|
}, [logger, inputHistoryStore]);
|
||||||
|
|
||||||
const currentSessionUserMessages = history
|
|
||||||
.filter(
|
|
||||||
(item): item is HistoryItem & { type: 'user'; text: string } =>
|
|
||||||
item.type === 'user' &&
|
|
||||||
typeof item.text === 'string' &&
|
|
||||||
item.text.trim() !== '',
|
|
||||||
)
|
|
||||||
.map((item) => item.text)
|
|
||||||
.reverse(); // Newest first, to match pastMessagesRaw sorting
|
|
||||||
|
|
||||||
// Combine, with current session messages being more recent
|
|
||||||
const combinedMessages = [
|
|
||||||
...currentSessionUserMessages,
|
|
||||||
...pastMessagesRaw,
|
|
||||||
];
|
|
||||||
|
|
||||||
// Deduplicate consecutive identical messages from the combined list (still newest first)
|
|
||||||
const deduplicatedMessages: string[] = [];
|
|
||||||
if (combinedMessages.length > 0) {
|
|
||||||
deduplicatedMessages.push(combinedMessages[0]); // Add the newest one unconditionally
|
|
||||||
for (let i = 1; i < combinedMessages.length; i++) {
|
|
||||||
if (combinedMessages[i] !== combinedMessages[i - 1]) {
|
|
||||||
deduplicatedMessages.push(combinedMessages[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Reverse to oldest first for useInputHistory
|
|
||||||
setUserMessages(deduplicatedMessages.reverse());
|
|
||||||
};
|
|
||||||
fetchUserMessages();
|
|
||||||
}, [history, logger]);
|
|
||||||
|
|
||||||
const isInputActive =
|
const isInputActive =
|
||||||
(streamingState === StreamingState.Idle ||
|
(streamingState === StreamingState.Idle ||
|
||||||
@@ -1336,7 +1313,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
|||||||
inputWidth={inputWidth}
|
inputWidth={inputWidth}
|
||||||
suggestionsWidth={suggestionsWidth}
|
suggestionsWidth={suggestionsWidth}
|
||||||
onSubmit={handleFinalSubmit}
|
onSubmit={handleFinalSubmit}
|
||||||
userMessages={userMessages}
|
userMessages={inputHistoryStore.inputHistory}
|
||||||
onClearScreen={handleClearScreen}
|
onClearScreen={handleClearScreen}
|
||||||
config={config}
|
config={config}
|
||||||
slashCommands={slashCommands}
|
slashCommands={slashCommands}
|
||||||
|
|||||||
@@ -0,0 +1,301 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { act, renderHook } from '@testing-library/react';
|
||||||
|
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
import { useInputHistoryStore } from './useInputHistoryStore.js';
|
||||||
|
|
||||||
|
describe('useInputHistoryStore', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialize with empty input history', () => {
|
||||||
|
const { result } = renderHook(() => useInputHistoryStore());
|
||||||
|
|
||||||
|
expect(result.current.inputHistory).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add input to history', () => {
|
||||||
|
const { result } = renderHook(() => useInputHistoryStore());
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.addInput('test message 1');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.inputHistory).toEqual(['test message 1']);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.addInput('test message 2');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.inputHistory).toEqual([
|
||||||
|
'test message 1',
|
||||||
|
'test message 2',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not add empty or whitespace-only inputs', () => {
|
||||||
|
const { result } = renderHook(() => useInputHistoryStore());
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.addInput('');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.inputHistory).toEqual([]);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.addInput(' ');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.inputHistory).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should deduplicate consecutive identical messages', () => {
|
||||||
|
const { result } = renderHook(() => useInputHistoryStore());
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.addInput('test message');
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.addInput('test message'); // Same as previous
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.inputHistory).toEqual(['test message']);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.addInput('different message');
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.addInput('test message'); // Same as first, but not consecutive
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.inputHistory).toEqual([
|
||||||
|
'test message',
|
||||||
|
'different message',
|
||||||
|
'test message',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialize from logger successfully', async () => {
|
||||||
|
const mockLogger = {
|
||||||
|
getPreviousUserMessages: vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(['newest', 'middle', 'oldest']),
|
||||||
|
};
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useInputHistoryStore());
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.initializeFromLogger(mockLogger);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should reverse the order to oldest first
|
||||||
|
expect(result.current.inputHistory).toEqual(['oldest', 'middle', 'newest']);
|
||||||
|
expect(mockLogger.getPreviousUserMessages).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle logger initialization failure gracefully', async () => {
|
||||||
|
const mockLogger = {
|
||||||
|
getPreviousUserMessages: vi
|
||||||
|
.fn()
|
||||||
|
.mockRejectedValue(new Error('Logger error')),
|
||||||
|
};
|
||||||
|
|
||||||
|
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useInputHistoryStore());
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.initializeFromLogger(mockLogger);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.inputHistory).toEqual([]);
|
||||||
|
expect(consoleSpy).toHaveBeenCalledWith(
|
||||||
|
'Failed to initialize input history from logger:',
|
||||||
|
expect.any(Error),
|
||||||
|
);
|
||||||
|
|
||||||
|
consoleSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialize only once', async () => {
|
||||||
|
const mockLogger = {
|
||||||
|
getPreviousUserMessages: vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(['message1', 'message2']),
|
||||||
|
};
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useInputHistoryStore());
|
||||||
|
|
||||||
|
// Call initializeFromLogger twice
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.initializeFromLogger(mockLogger);
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.initializeFromLogger(mockLogger);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should be called only once
|
||||||
|
expect(mockLogger.getPreviousUserMessages).toHaveBeenCalledTimes(1);
|
||||||
|
expect(result.current.inputHistory).toEqual(['message2', 'message1']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle null logger gracefully', async () => {
|
||||||
|
const { result } = renderHook(() => useInputHistoryStore());
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.initializeFromLogger(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.inputHistory).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should trim input before adding to history', () => {
|
||||||
|
const { result } = renderHook(() => useInputHistoryStore());
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.addInput(' test message ');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.inputHistory).toEqual(['test message']);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deduplication logic from previous implementation', () => {
|
||||||
|
it('should deduplicate consecutive messages from past sessions during initialization', async () => {
|
||||||
|
const mockLogger = {
|
||||||
|
getPreviousUserMessages: vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue([
|
||||||
|
'message1',
|
||||||
|
'message1',
|
||||||
|
'message2',
|
||||||
|
'message2',
|
||||||
|
'message3',
|
||||||
|
]), // newest first with duplicates
|
||||||
|
};
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useInputHistoryStore());
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.initializeFromLogger(mockLogger);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should deduplicate consecutive messages and reverse to oldest first
|
||||||
|
expect(result.current.inputHistory).toEqual([
|
||||||
|
'message3',
|
||||||
|
'message2',
|
||||||
|
'message1',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should deduplicate across session boundaries', async () => {
|
||||||
|
const mockLogger = {
|
||||||
|
getPreviousUserMessages: vi.fn().mockResolvedValue(['old2', 'old1']), // newest first
|
||||||
|
};
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useInputHistoryStore());
|
||||||
|
|
||||||
|
// Initialize with past session
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.initializeFromLogger(mockLogger);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add current session inputs
|
||||||
|
act(() => {
|
||||||
|
result.current.addInput('old2'); // Same as last past session message
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should deduplicate across session boundary
|
||||||
|
expect(result.current.inputHistory).toEqual(['old1', 'old2']);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.addInput('new1');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.inputHistory).toEqual(['old1', 'old2', 'new1']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve non-consecutive duplicates', async () => {
|
||||||
|
const mockLogger = {
|
||||||
|
getPreviousUserMessages: vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(['message2', 'message1', 'message2']), // newest first with non-consecutive duplicate
|
||||||
|
};
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useInputHistoryStore());
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.initializeFromLogger(mockLogger);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Non-consecutive duplicates should be preserved
|
||||||
|
expect(result.current.inputHistory).toEqual([
|
||||||
|
'message2',
|
||||||
|
'message1',
|
||||||
|
'message2',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle complex deduplication with current session', () => {
|
||||||
|
const { result } = renderHook(() => useInputHistoryStore());
|
||||||
|
|
||||||
|
// Add multiple messages with duplicates
|
||||||
|
act(() => {
|
||||||
|
result.current.addInput('hello');
|
||||||
|
});
|
||||||
|
act(() => {
|
||||||
|
result.current.addInput('hello'); // consecutive duplicate
|
||||||
|
});
|
||||||
|
act(() => {
|
||||||
|
result.current.addInput('world');
|
||||||
|
});
|
||||||
|
act(() => {
|
||||||
|
result.current.addInput('world'); // consecutive duplicate
|
||||||
|
});
|
||||||
|
act(() => {
|
||||||
|
result.current.addInput('hello'); // non-consecutive duplicate
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should have deduplicated consecutive ones
|
||||||
|
expect(result.current.inputHistory).toEqual(['hello', 'world', 'hello']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should maintain oldest-first order in final output', async () => {
|
||||||
|
const mockLogger = {
|
||||||
|
getPreviousUserMessages: vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(['newest', 'middle', 'oldest']), // newest first
|
||||||
|
};
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useInputHistoryStore());
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.initializeFromLogger(mockLogger);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add current session messages
|
||||||
|
act(() => {
|
||||||
|
result.current.addInput('current1');
|
||||||
|
});
|
||||||
|
act(() => {
|
||||||
|
result.current.addInput('current2');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should maintain oldest-first order
|
||||||
|
expect(result.current.inputHistory).toEqual([
|
||||||
|
'oldest',
|
||||||
|
'middle',
|
||||||
|
'newest',
|
||||||
|
'current1',
|
||||||
|
'current2',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
|
||||||
|
interface Logger {
|
||||||
|
getPreviousUserMessages(): Promise<string[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseInputHistoryStoreReturn {
|
||||||
|
inputHistory: string[];
|
||||||
|
addInput: (input: string) => void;
|
||||||
|
initializeFromLogger: (logger: Logger | null) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for independently managing input history.
|
||||||
|
* Completely separated from chat history and unaffected by /clear commands.
|
||||||
|
*/
|
||||||
|
export function useInputHistoryStore(): UseInputHistoryStoreReturn {
|
||||||
|
const [inputHistory, setInputHistory] = useState<string[]>([]);
|
||||||
|
const [_pastSessionMessages, setPastSessionMessages] = useState<string[]>([]);
|
||||||
|
const [_currentSessionMessages, setCurrentSessionMessages] = useState<
|
||||||
|
string[]
|
||||||
|
>([]);
|
||||||
|
const [isInitialized, setIsInitialized] = useState(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recalculate the complete input history from past and current sessions.
|
||||||
|
* Applies the same deduplication logic as the previous implementation.
|
||||||
|
*/
|
||||||
|
const recalculateHistory = useCallback(
|
||||||
|
(currentSession: string[], pastSession: string[]) => {
|
||||||
|
// Combine current session (newest first) + past session (newest first)
|
||||||
|
const combinedMessages = [...currentSession, ...pastSession];
|
||||||
|
|
||||||
|
// Deduplicate consecutive identical messages (same algorithm as before)
|
||||||
|
const deduplicatedMessages: string[] = [];
|
||||||
|
if (combinedMessages.length > 0) {
|
||||||
|
deduplicatedMessages.push(combinedMessages[0]); // Add the newest one unconditionally
|
||||||
|
for (let i = 1; i < combinedMessages.length; i++) {
|
||||||
|
if (combinedMessages[i] !== combinedMessages[i - 1]) {
|
||||||
|
deduplicatedMessages.push(combinedMessages[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reverse to oldest first for useInputHistory
|
||||||
|
setInputHistory(deduplicatedMessages.reverse());
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize input history from logger with past session data.
|
||||||
|
* Executed only once at app startup.
|
||||||
|
*/
|
||||||
|
const initializeFromLogger = useCallback(
|
||||||
|
async (logger: Logger | null) => {
|
||||||
|
if (isInitialized || !logger) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pastMessages = (await logger.getPreviousUserMessages()) || [];
|
||||||
|
setPastSessionMessages(pastMessages); // Store as newest first
|
||||||
|
recalculateHistory([], pastMessages);
|
||||||
|
setIsInitialized(true);
|
||||||
|
} catch (error) {
|
||||||
|
// Start with empty history even if logger initialization fails
|
||||||
|
console.warn('Failed to initialize input history from logger:', error);
|
||||||
|
setPastSessionMessages([]);
|
||||||
|
recalculateHistory([], []);
|
||||||
|
setIsInitialized(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[isInitialized, recalculateHistory],
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add new input to history.
|
||||||
|
* Recalculates the entire history with deduplication.
|
||||||
|
*/
|
||||||
|
const addInput = useCallback(
|
||||||
|
(input: string) => {
|
||||||
|
const trimmedInput = input.trim();
|
||||||
|
if (!trimmedInput) return; // Filter empty/whitespace-only inputs
|
||||||
|
|
||||||
|
setCurrentSessionMessages((prevCurrent) => {
|
||||||
|
const newCurrentSession = [...prevCurrent, trimmedInput];
|
||||||
|
|
||||||
|
setPastSessionMessages((prevPast) => {
|
||||||
|
recalculateHistory(
|
||||||
|
newCurrentSession.slice().reverse(), // Convert to newest first
|
||||||
|
prevPast,
|
||||||
|
);
|
||||||
|
return prevPast; // No change to past messages
|
||||||
|
});
|
||||||
|
|
||||||
|
return newCurrentSession;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[recalculateHistory],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
inputHistory,
|
||||||
|
addInput,
|
||||||
|
initializeFromLogger,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user