mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 09:01:14 +00:00
feat(tui): add prompt history navigation with Up/Down arrows
Browse previously submitted prompts using Up/Down arrow keys, similar to shell history and Claude Code's prompt history feature. - Up arrow when editor is empty: browse to older prompts - Down arrow when browsing: return to newer prompts or clear editor - Cursor movement within multi-line history entries supported - History is session-scoped, stores up to 100 entries - Consecutive duplicates are not added to history Includes 15 new tests for history navigation behavior.
This commit is contained in:
parent
029a04c43b
commit
c550ed2bca
5 changed files with 350 additions and 18 deletions
|
|
@ -4,6 +4,247 @@ import { Editor } from "../src/components/editor.js";
|
|||
import { defaultEditorTheme } from "./test-themes.js";
|
||||
|
||||
describe("Editor component", () => {
|
||||
describe("Prompt history navigation", () => {
|
||||
it("does nothing on Up arrow when history is empty", () => {
|
||||
const editor = new Editor(defaultEditorTheme);
|
||||
|
||||
editor.handleInput("\x1b[A"); // Up arrow
|
||||
|
||||
assert.strictEqual(editor.getText(), "");
|
||||
});
|
||||
|
||||
it("shows most recent history entry on Up arrow when editor is empty", () => {
|
||||
const editor = new Editor(defaultEditorTheme);
|
||||
|
||||
editor.addToHistory("first prompt");
|
||||
editor.addToHistory("second prompt");
|
||||
|
||||
editor.handleInput("\x1b[A"); // Up arrow
|
||||
|
||||
assert.strictEqual(editor.getText(), "second prompt");
|
||||
});
|
||||
|
||||
it("cycles through history entries on repeated Up arrow", () => {
|
||||
const editor = new Editor(defaultEditorTheme);
|
||||
|
||||
editor.addToHistory("first");
|
||||
editor.addToHistory("second");
|
||||
editor.addToHistory("third");
|
||||
|
||||
editor.handleInput("\x1b[A"); // Up - shows "third"
|
||||
assert.strictEqual(editor.getText(), "third");
|
||||
|
||||
editor.handleInput("\x1b[A"); // Up - shows "second"
|
||||
assert.strictEqual(editor.getText(), "second");
|
||||
|
||||
editor.handleInput("\x1b[A"); // Up - shows "first"
|
||||
assert.strictEqual(editor.getText(), "first");
|
||||
|
||||
editor.handleInput("\x1b[A"); // Up - stays at "first" (oldest)
|
||||
assert.strictEqual(editor.getText(), "first");
|
||||
});
|
||||
|
||||
it("returns to empty editor on Down arrow after browsing history", () => {
|
||||
const editor = new Editor(defaultEditorTheme);
|
||||
|
||||
editor.addToHistory("prompt");
|
||||
|
||||
editor.handleInput("\x1b[A"); // Up - shows "prompt"
|
||||
assert.strictEqual(editor.getText(), "prompt");
|
||||
|
||||
editor.handleInput("\x1b[B"); // Down - clears editor
|
||||
assert.strictEqual(editor.getText(), "");
|
||||
});
|
||||
|
||||
it("navigates forward through history with Down arrow", () => {
|
||||
const editor = new Editor(defaultEditorTheme);
|
||||
|
||||
editor.addToHistory("first");
|
||||
editor.addToHistory("second");
|
||||
editor.addToHistory("third");
|
||||
|
||||
// Go to oldest
|
||||
editor.handleInput("\x1b[A"); // third
|
||||
editor.handleInput("\x1b[A"); // second
|
||||
editor.handleInput("\x1b[A"); // first
|
||||
|
||||
// Navigate back
|
||||
editor.handleInput("\x1b[B"); // second
|
||||
assert.strictEqual(editor.getText(), "second");
|
||||
|
||||
editor.handleInput("\x1b[B"); // third
|
||||
assert.strictEqual(editor.getText(), "third");
|
||||
|
||||
editor.handleInput("\x1b[B"); // empty
|
||||
assert.strictEqual(editor.getText(), "");
|
||||
});
|
||||
|
||||
it("exits history mode when typing a character", () => {
|
||||
const editor = new Editor(defaultEditorTheme);
|
||||
|
||||
editor.addToHistory("old prompt");
|
||||
|
||||
editor.handleInput("\x1b[A"); // Up - shows "old prompt"
|
||||
editor.handleInput("x"); // Type a character - exits history mode
|
||||
|
||||
assert.strictEqual(editor.getText(), "old promptx");
|
||||
});
|
||||
|
||||
it("exits history mode on setText", () => {
|
||||
const editor = new Editor(defaultEditorTheme);
|
||||
|
||||
editor.addToHistory("first");
|
||||
editor.addToHistory("second");
|
||||
|
||||
editor.handleInput("\x1b[A"); // Up - shows "second"
|
||||
editor.setText(""); // External clear
|
||||
|
||||
// Up should start fresh from most recent
|
||||
editor.handleInput("\x1b[A");
|
||||
assert.strictEqual(editor.getText(), "second");
|
||||
});
|
||||
|
||||
it("does not add empty strings to history", () => {
|
||||
const editor = new Editor(defaultEditorTheme);
|
||||
|
||||
editor.addToHistory("");
|
||||
editor.addToHistory(" ");
|
||||
editor.addToHistory("valid");
|
||||
|
||||
editor.handleInput("\x1b[A");
|
||||
assert.strictEqual(editor.getText(), "valid");
|
||||
|
||||
// Should not have more entries
|
||||
editor.handleInput("\x1b[A");
|
||||
assert.strictEqual(editor.getText(), "valid");
|
||||
});
|
||||
|
||||
it("does not add consecutive duplicates to history", () => {
|
||||
const editor = new Editor(defaultEditorTheme);
|
||||
|
||||
editor.addToHistory("same");
|
||||
editor.addToHistory("same");
|
||||
editor.addToHistory("same");
|
||||
|
||||
editor.handleInput("\x1b[A"); // "same"
|
||||
assert.strictEqual(editor.getText(), "same");
|
||||
|
||||
editor.handleInput("\x1b[A"); // stays at "same" (only one entry)
|
||||
assert.strictEqual(editor.getText(), "same");
|
||||
});
|
||||
|
||||
it("allows non-consecutive duplicates in history", () => {
|
||||
const editor = new Editor(defaultEditorTheme);
|
||||
|
||||
editor.addToHistory("first");
|
||||
editor.addToHistory("second");
|
||||
editor.addToHistory("first"); // Not consecutive, should be added
|
||||
|
||||
editor.handleInput("\x1b[A"); // "first"
|
||||
assert.strictEqual(editor.getText(), "first");
|
||||
|
||||
editor.handleInput("\x1b[A"); // "second"
|
||||
assert.strictEqual(editor.getText(), "second");
|
||||
|
||||
editor.handleInput("\x1b[A"); // "first" (older one)
|
||||
assert.strictEqual(editor.getText(), "first");
|
||||
});
|
||||
|
||||
it("uses cursor movement instead of history when editor has content", () => {
|
||||
const editor = new Editor(defaultEditorTheme);
|
||||
|
||||
editor.addToHistory("history item");
|
||||
editor.setText("line1\nline2");
|
||||
|
||||
// Cursor is at end of line2, Up should move to line1
|
||||
editor.handleInput("\x1b[A"); // Up - cursor movement
|
||||
|
||||
// Insert character to verify cursor position
|
||||
editor.handleInput("X");
|
||||
|
||||
// X should be inserted in line1, not replace with history
|
||||
assert.strictEqual(editor.getText(), "line1X\nline2");
|
||||
});
|
||||
|
||||
it("limits history to 100 entries", () => {
|
||||
const editor = new Editor(defaultEditorTheme);
|
||||
|
||||
// Add 105 entries
|
||||
for (let i = 0; i < 105; i++) {
|
||||
editor.addToHistory(`prompt ${i}`);
|
||||
}
|
||||
|
||||
// Navigate to oldest
|
||||
for (let i = 0; i < 100; i++) {
|
||||
editor.handleInput("\x1b[A");
|
||||
}
|
||||
|
||||
// Should be at entry 5 (oldest kept), not entry 0
|
||||
assert.strictEqual(editor.getText(), "prompt 5");
|
||||
|
||||
// One more Up should not change anything
|
||||
editor.handleInput("\x1b[A");
|
||||
assert.strictEqual(editor.getText(), "prompt 5");
|
||||
});
|
||||
|
||||
it("allows cursor movement within multi-line history entry with Down", () => {
|
||||
const editor = new Editor(defaultEditorTheme);
|
||||
|
||||
editor.addToHistory("line1\nline2\nline3");
|
||||
|
||||
// Browse to the multi-line entry
|
||||
editor.handleInput("\x1b[A"); // Up - shows entry, cursor at end of line3
|
||||
assert.strictEqual(editor.getText(), "line1\nline2\nline3");
|
||||
|
||||
// Down should exit history since cursor is on last line
|
||||
editor.handleInput("\x1b[B"); // Down
|
||||
assert.strictEqual(editor.getText(), ""); // Exited to empty
|
||||
});
|
||||
|
||||
it("allows cursor movement within multi-line history entry with Up", () => {
|
||||
const editor = new Editor(defaultEditorTheme);
|
||||
|
||||
editor.addToHistory("older entry");
|
||||
editor.addToHistory("line1\nline2\nline3");
|
||||
|
||||
// Browse to the multi-line entry
|
||||
editor.handleInput("\x1b[A"); // Up - shows multi-line, cursor at end of line3
|
||||
|
||||
// Up should move cursor within the entry (not on first line yet)
|
||||
editor.handleInput("\x1b[A"); // Up - cursor moves to line2
|
||||
assert.strictEqual(editor.getText(), "line1\nline2\nline3"); // Still same entry
|
||||
|
||||
editor.handleInput("\x1b[A"); // Up - cursor moves to line1 (now on first visual line)
|
||||
assert.strictEqual(editor.getText(), "line1\nline2\nline3"); // Still same entry
|
||||
|
||||
// Now Up should navigate to older history entry
|
||||
editor.handleInput("\x1b[A"); // Up - navigate to older
|
||||
assert.strictEqual(editor.getText(), "older entry");
|
||||
});
|
||||
|
||||
it("navigates from multi-line entry back to newer via Down after cursor movement", () => {
|
||||
const editor = new Editor(defaultEditorTheme);
|
||||
|
||||
editor.addToHistory("line1\nline2\nline3");
|
||||
|
||||
// Browse to entry and move cursor up
|
||||
editor.handleInput("\x1b[A"); // Up - shows entry, cursor at end
|
||||
editor.handleInput("\x1b[A"); // Up - cursor to line2
|
||||
editor.handleInput("\x1b[A"); // Up - cursor to line1
|
||||
|
||||
// Now Down should move cursor down within the entry
|
||||
editor.handleInput("\x1b[B"); // Down - cursor to line2
|
||||
assert.strictEqual(editor.getText(), "line1\nline2\nline3");
|
||||
|
||||
editor.handleInput("\x1b[B"); // Down - cursor to line3
|
||||
assert.strictEqual(editor.getText(), "line1\nline2\nline3");
|
||||
|
||||
// Now on last line, Down should exit history
|
||||
editor.handleInput("\x1b[B"); // Down - exit to empty
|
||||
assert.strictEqual(editor.getText(), "");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Unicode text editing behavior", () => {
|
||||
it("inserts mixed ASCII, umlauts, and emojis as literal text", () => {
|
||||
const editor = new Editor(defaultEditorTheme);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue