-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinputHistory.ts
More file actions
111 lines (100 loc) · 3.24 KB
/
Copy pathinputHistory.ts
File metadata and controls
111 lines (100 loc) · 3.24 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
/**
* Bash-style input history for the AI Assistant chat input.
*
* Mirrors :mod:`DataLab/datalab/aiassistant/inputhistory.py`. Provides
* Ctrl+Up/Down navigation through previously submitted prompts and
* persists to ``localStorage`` so history survives across sessions
* (DataLab Qt uses an on-disk text file).
*/
const STORAGE_KEY = "datalab-web.aiassistant.inputHistory";
export class InputHistory {
private items: string[] = [];
/** Index of the entry currently displayed when navigating; ``null``
* means the user is editing a fresh draft. */
private index: number | null = null;
/** Draft preserved when the user starts navigating, restored when
* navigating past the most recent entry. */
private draft = "";
private readonly maxSize: number;
private readonly storageKey: string;
constructor(maxSize = 500, storageKey = STORAGE_KEY) {
this.maxSize = Math.max(1, Math.floor(maxSize));
this.storageKey = storageKey;
this.load();
}
private load(): void {
if (typeof localStorage === "undefined") return;
try {
const raw = localStorage.getItem(this.storageKey);
if (!raw) return;
const parsed: unknown = JSON.parse(raw);
if (Array.isArray(parsed)) {
this.items = parsed.filter((x): x is string => typeof x === "string");
}
} catch {
this.items = [];
}
}
private save(): void {
if (typeof localStorage === "undefined") return;
try {
const trimmed = this.items.slice(-this.maxSize);
localStorage.setItem(this.storageKey, JSON.stringify(trimmed));
} catch {
/* quota or disabled storage — silently ignore */
}
}
/** Snapshot (oldest first). */
getItems(): string[] {
return [...this.items];
}
clear(): void {
this.items = [];
this.resetNavigation();
this.save();
}
/** Append a submitted prompt; deduplicates and trims to ``maxSize``. */
add(text: string): void {
const trimmed = text.replace(/\n+$/, "");
if (!trimmed.trim()) return;
this.items = this.items.filter((it) => it !== trimmed);
this.items.push(trimmed);
if (this.items.length > this.maxSize) {
this.items = this.items.slice(-this.maxSize);
}
this.resetNavigation();
this.save();
}
/** Forget the current navigation position and any preserved draft. */
resetNavigation(): void {
this.index = null;
this.draft = "";
}
/** Return the previous (older) entry, or ``null`` if none. Captures the
* current draft on the first call so it can be restored later. */
previous(currentText: string): string | null {
if (this.items.length === 0) return null;
if (this.index === null) {
this.draft = currentText;
this.index = this.items.length - 1;
} else if (this.index > 0) {
this.index -= 1;
} else {
return this.items[this.index];
}
return this.items[this.index];
}
/** Return the next (newer) entry; restores the draft past the end.
* ``null`` when no navigation is in progress. */
next(): string | null {
if (this.index === null) return null;
if (this.index < this.items.length - 1) {
this.index += 1;
return this.items[this.index];
}
this.index = null;
const draft = this.draft;
this.draft = "";
return draft;
}
}