Skip to content

Commit 42d4d0a

Browse files
feat(Chat): add blank-input prompt history navigation
Summary: Add shell-like prompt history navigation to the chat input: when the input is blank, `Up` recalls older submitted prompts and `Down` moves forward toward newer prompts, ending at an empty draft. History is scoped to the current chat session, derived from that session's persisted transcript on load, then maintained in memory for the rest of the mounted session. Implementation Changes: - Extend `Chat/Input` to own prompt-history state: - Initialize history from the active session's existing user prompts. - Append newly submitted non-command user prompts after trimming and successful submit. - Track a navigation index plus an empty-draft state so `Down` can return to blank input after recall. - Reset navigation state whenever the input `history` prop changes for a different session. - Update `Chat` to derive session prompt history once from `initialMessages` for the active session and pass it into `Input` as `history`. - Include only `role === user` messages. - Exclude slash commands from history. - Do not persist or read any separate history file. - Intercept `Up`/`Down` in `Chat/Input` only when: - input is blank, or - the user is already navigating history. - Preserve existing behavior when not navigating history: - `Up`/`Down` continue to work for file suggestions and command menus. - Normal typing exits navigation mode and resumes ordinary editing from the recalled value. Public Interface Changes: - `Input` props gain `history: string[]`. Tests: - Add `Chat` coverage for deriving `history` from `initialMessages`, including excluding slash commands. - Add `Input` tests for: - recalling the most recent prompt with blank-input `Up` - stepping backward through multiple prompts with repeated `Up` - stepping forward with `Down` - returning to empty input at the end of forward navigation - not recording empty submissions - not recording slash commands - resetting navigation state when `history` changes for the new session - preserving existing file-suggestion `Up`/`Down` behavior when suggestions are visible Assumptions: - Source of truth for persisted recall is `~/.code-ollama/sessions/<sessionId>/messages.jsonl`, already loaded into `initialMessages`. - History derivation happens once per active session load; no repeated transcript scans during input editing. - Prompt history is prompt-only, not command history. - `/clear` creates a new session and therefore starts with empty history.
1 parent 22fde11 commit 42d4d0a

4 files changed

Lines changed: 289 additions & 45 deletions

File tree

src/components/Chat/Chat.test.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ import { prewarmCodeBlocks } from '../CodeBlock';
88

99
const mockState = vi.hoisted(() => ({
1010
handler: undefined as ((value: string) => void) | undefined,
11+
history: [] as string[],
1112
testInput: '',
1213
shouldReset: false,
1314
clear() {
1415
this.handler = undefined;
16+
this.history = [];
1517
this.testInput = '';
1618
this.shouldReset = true;
1719
},
@@ -94,6 +96,7 @@ vi.mock('../../utils', async () => ({
9496

9597
vi.mock('./Input', () => ({
9698
Input: (props: {
99+
history?: string[];
97100
onSubmit?: (value: string) => void;
98101
onInterrupt?: () => void;
99102
isDisabled?: boolean;
@@ -102,6 +105,8 @@ vi.mock('./Input', () => ({
102105
mockState.handler = props.onSubmit;
103106
}
104107

108+
mockState.history = props.history ?? [];
109+
105110
if (props.onInterrupt) {
106111
interruptState.handler = props.onInterrupt;
107112
}
@@ -216,6 +221,28 @@ describe('Chat', () => {
216221
expect(frame).toContain('hello');
217222
}, 10_000);
218223

224+
it('derives prompt history from user messages and excludes slash commands', async () => {
225+
render(
226+
<Chat
227+
initialMessages={[
228+
{ role: 'user', content: 'first prompt' },
229+
{ role: 'assistant', content: 'response' },
230+
{ role: 'user', content: '/model' },
231+
{ role: 'user', content: 'second prompt' },
232+
]}
233+
model="gemma4"
234+
onCommand={vi.fn()}
235+
mode={MODE.SAFE}
236+
onModeChange={onModeChange}
237+
sessionId="0"
238+
/>,
239+
);
240+
241+
await time.tick();
242+
243+
expect(mockState.history).toEqual(['first prompt', 'second prompt']);
244+
});
245+
219246
it('does not add blank messages', async () => {
220247
const chat = (
221248
<Chat

src/components/Chat/Chat.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Box, Text } from 'ink';
2-
import { useCallback, useEffect, useRef, useState } from 'react';
2+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
33

44
import { DECISION, MODE, PROMPT, ROLE } from '../../constants';
55
import type { Decision, Mode, ToolName, ToolResult } from '../../types';
@@ -38,6 +38,13 @@ export function Chat({
3838
sessionId,
3939
}: Props) {
4040
const sessionMessages = initialMessages ?? [];
41+
const history = useMemo(
42+
() =>
43+
sessionMessages.flatMap(({ role, content }) =>
44+
role === ROLE.USER && !content.startsWith('/') ? [content] : [],
45+
),
46+
[sessionMessages],
47+
);
4148
const [messages, setMessages] = useState<ollama.Message[]>(sessionMessages);
4249
const [streamingMessage, setStreamingMessage] =
4350
useState<ollama.Message | null>(null);
@@ -586,6 +593,7 @@ export function Chat({
586593
{!pendingPlan && !pendingToolCall && (
587594
<Box marginTop={1}>
588595
<Input
596+
history={history}
589597
isDisabled={isLoading}
590598
onInterrupt={handleInterrupt}
591599
// eslint-disable-next-line @typescript-eslint/no-misused-promises

0 commit comments

Comments
 (0)