@oh-my-pi/pi-coding-agent 4.0.1 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/package.json +5 -5
- package/src/core/auth-storage.ts +9 -1
- package/src/core/history-storage.ts +166 -0
- package/src/core/index.ts +1 -0
- package/src/core/keybindings.ts +3 -0
- package/src/modes/interactive/components/custom-editor.ts +7 -0
- package/src/modes/interactive/components/history-search.ts +158 -0
- package/src/modes/interactive/interactive-mode.ts +34 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [4.1.0] - 2026-01-10
|
|
6
|
+
### Added
|
|
7
|
+
|
|
8
|
+
- Added persistent prompt history with SQLite-backed storage and Ctrl+R search
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Fixed credential blocking logic to correctly check for remaining available credentials instead of always returning true
|
|
13
|
+
|
|
5
14
|
## [4.0.1] - 2026-01-10
|
|
6
15
|
### Added
|
|
7
16
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "4.0
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"ompConfig": {
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
"prepublishOnly": "bun run generate-template && bun run clean && bun run build"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@oh-my-pi/pi-ai": "4.0
|
|
43
|
-
"@oh-my-pi/pi-agent-core": "4.0
|
|
44
|
-
"@oh-my-pi/pi-git-tool": "4.0
|
|
45
|
-
"@oh-my-pi/pi-tui": "4.0
|
|
42
|
+
"@oh-my-pi/pi-ai": "4.1.0",
|
|
43
|
+
"@oh-my-pi/pi-agent-core": "4.1.0",
|
|
44
|
+
"@oh-my-pi/pi-git-tool": "4.1.0",
|
|
45
|
+
"@oh-my-pi/pi-tui": "4.1.0",
|
|
46
46
|
"@openai/agents": "^0.3.7",
|
|
47
47
|
"@sinclair/typebox": "^0.34.46",
|
|
48
48
|
"ajv": "^8.17.1",
|
package/src/core/auth-storage.ts
CHANGED
|
@@ -797,7 +797,15 @@ export class AuthStorage {
|
|
|
797
797
|
}
|
|
798
798
|
|
|
799
799
|
this.markCredentialBlocked(providerKey, sessionCredential.index, blockedUntil);
|
|
800
|
-
|
|
800
|
+
|
|
801
|
+
const remainingCredentials = this.getCredentialsForProvider(provider)
|
|
802
|
+
.map((credential, index) => ({ credential, index }))
|
|
803
|
+
.filter(
|
|
804
|
+
(entry): entry is { credential: AuthCredential; index: number } =>
|
|
805
|
+
entry.credential.type === sessionCredential.type && entry.index !== sessionCredential.index,
|
|
806
|
+
);
|
|
807
|
+
|
|
808
|
+
return remainingCredentials.some((candidate) => !this.isCredentialBlocked(providerKey, candidate.index));
|
|
801
809
|
}
|
|
802
810
|
|
|
803
811
|
/**
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { getAgentDir } from "../config";
|
|
4
|
+
import { logger } from "./logger";
|
|
5
|
+
|
|
6
|
+
export interface HistoryEntry {
|
|
7
|
+
id: number;
|
|
8
|
+
prompt: string;
|
|
9
|
+
created_at: number;
|
|
10
|
+
cwd?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type Statement = ReturnType<Database["prepare"]>;
|
|
14
|
+
|
|
15
|
+
type HistoryRow = {
|
|
16
|
+
id: number;
|
|
17
|
+
prompt: string;
|
|
18
|
+
created_at: number;
|
|
19
|
+
cwd: string | null;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export class HistoryStorage {
|
|
23
|
+
private db: Database;
|
|
24
|
+
private static instance?: HistoryStorage;
|
|
25
|
+
|
|
26
|
+
// Prepared statements
|
|
27
|
+
private insertStmt: Statement;
|
|
28
|
+
private recentStmt: Statement;
|
|
29
|
+
private searchStmt: Statement;
|
|
30
|
+
private lastPromptStmt: Statement;
|
|
31
|
+
|
|
32
|
+
private constructor(dbPath: string) {
|
|
33
|
+
this.ensureDir(dbPath);
|
|
34
|
+
|
|
35
|
+
this.db = new Database(dbPath);
|
|
36
|
+
|
|
37
|
+
const hasFts = this.db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='history_fts'").get();
|
|
38
|
+
|
|
39
|
+
this.db.exec(`
|
|
40
|
+
PRAGMA journal_mode=WAL;
|
|
41
|
+
PRAGMA synchronous=NORMAL;
|
|
42
|
+
|
|
43
|
+
CREATE TABLE IF NOT EXISTS history (
|
|
44
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
45
|
+
prompt TEXT NOT NULL,
|
|
46
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
47
|
+
cwd TEXT
|
|
48
|
+
);
|
|
49
|
+
CREATE INDEX IF NOT EXISTS idx_history_created_at ON history(created_at DESC);
|
|
50
|
+
|
|
51
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS history_fts USING fts5(prompt, content='history', content_rowid='id');
|
|
52
|
+
|
|
53
|
+
CREATE TRIGGER IF NOT EXISTS history_ai AFTER INSERT ON history BEGIN
|
|
54
|
+
INSERT INTO history_fts(rowid, prompt) VALUES (new.id, new.prompt);
|
|
55
|
+
END;
|
|
56
|
+
`);
|
|
57
|
+
|
|
58
|
+
if (!hasFts) {
|
|
59
|
+
try {
|
|
60
|
+
this.db.run("INSERT INTO history_fts(history_fts) VALUES('rebuild')");
|
|
61
|
+
} catch (error) {
|
|
62
|
+
logger.warn("HistoryStorage FTS rebuild failed", { error: String(error) });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this.insertStmt = this.db.prepare("INSERT INTO history (prompt, cwd) VALUES (?, ?)");
|
|
67
|
+
this.recentStmt = this.db.prepare(
|
|
68
|
+
"SELECT id, prompt, created_at, cwd FROM history ORDER BY created_at DESC, id DESC LIMIT ?",
|
|
69
|
+
);
|
|
70
|
+
this.searchStmt = this.db.prepare(
|
|
71
|
+
"SELECT h.id, h.prompt, h.created_at, h.cwd FROM history_fts f JOIN history h ON h.id = f.rowid WHERE history_fts MATCH ? ORDER BY h.created_at DESC, h.id DESC LIMIT ?",
|
|
72
|
+
);
|
|
73
|
+
this.lastPromptStmt = this.db.prepare("SELECT prompt FROM history ORDER BY id DESC LIMIT 1");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
static open(dbPath: string = join(getAgentDir(), "history.db")): HistoryStorage {
|
|
77
|
+
if (!HistoryStorage.instance) {
|
|
78
|
+
HistoryStorage.instance = new HistoryStorage(dbPath);
|
|
79
|
+
}
|
|
80
|
+
return HistoryStorage.instance;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
add(prompt: string, cwd?: string): void {
|
|
84
|
+
const trimmed = prompt.trim();
|
|
85
|
+
if (!trimmed) return;
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const last = this.lastPromptStmt.get() as { prompt?: string } | undefined;
|
|
89
|
+
if (last?.prompt === trimmed) return;
|
|
90
|
+
|
|
91
|
+
this.insertStmt.run(trimmed, cwd ?? null);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
logger.error("HistoryStorage add failed", { error: String(error) });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
getRecent(limit: number): HistoryEntry[] {
|
|
98
|
+
const safeLimit = this.normalizeLimit(limit);
|
|
99
|
+
if (safeLimit === 0) return [];
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const rows = this.recentStmt.all(safeLimit) as HistoryRow[];
|
|
103
|
+
return rows.map((row) => this.toEntry(row));
|
|
104
|
+
} catch (error) {
|
|
105
|
+
logger.error("HistoryStorage getRecent failed", { error: String(error) });
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
search(query: string, limit: number): HistoryEntry[] {
|
|
111
|
+
const safeLimit = this.normalizeLimit(limit);
|
|
112
|
+
if (safeLimit === 0) return [];
|
|
113
|
+
|
|
114
|
+
const ftsQuery = this.buildFtsQuery(query);
|
|
115
|
+
if (!ftsQuery) return [];
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const rows = this.searchStmt.all(ftsQuery, safeLimit) as HistoryRow[];
|
|
119
|
+
return rows.map((row) => this.toEntry(row));
|
|
120
|
+
} catch (error) {
|
|
121
|
+
logger.error("HistoryStorage search failed", { error: String(error) });
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private ensureDir(dbPath: string): void {
|
|
127
|
+
const dir = dirname(dbPath);
|
|
128
|
+
const result = Bun.spawnSync(["mkdir", "-p", dir]);
|
|
129
|
+
if (result.exitCode !== 0) {
|
|
130
|
+
const stderr = result.stderr ? new TextDecoder().decode(result.stderr) : "";
|
|
131
|
+
throw new Error(`Failed to create history directory: ${dir} ${stderr}`.trim());
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private normalizeLimit(limit: number): number {
|
|
136
|
+
if (!Number.isFinite(limit)) return 0;
|
|
137
|
+
const clamped = Math.max(0, Math.floor(limit));
|
|
138
|
+
return Math.min(clamped, 1000);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private buildFtsQuery(query: string): string | null {
|
|
142
|
+
const tokens = query
|
|
143
|
+
.trim()
|
|
144
|
+
.split(/\s+/)
|
|
145
|
+
.map((token) => token.trim())
|
|
146
|
+
.filter(Boolean);
|
|
147
|
+
|
|
148
|
+
if (tokens.length === 0) return null;
|
|
149
|
+
|
|
150
|
+
return tokens
|
|
151
|
+
.map((token) => {
|
|
152
|
+
const escaped = token.replace(/"/g, '""');
|
|
153
|
+
return `"${escaped}"*`;
|
|
154
|
+
})
|
|
155
|
+
.join(" ");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private toEntry(row: HistoryRow): HistoryEntry {
|
|
159
|
+
return {
|
|
160
|
+
id: row.id,
|
|
161
|
+
prompt: row.prompt,
|
|
162
|
+
created_at: row.created_at,
|
|
163
|
+
cwd: row.cwd ?? undefined,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
package/src/core/index.ts
CHANGED
package/src/core/keybindings.ts
CHANGED
|
@@ -26,6 +26,7 @@ export type AppAction =
|
|
|
26
26
|
| "expandTools"
|
|
27
27
|
| "toggleThinking"
|
|
28
28
|
| "externalEditor"
|
|
29
|
+
| "historySearch"
|
|
29
30
|
| "followUp"
|
|
30
31
|
| "dequeue";
|
|
31
32
|
|
|
@@ -53,6 +54,7 @@ export const DEFAULT_APP_KEYBINDINGS: Record<AppAction, KeyId | KeyId[]> = {
|
|
|
53
54
|
cycleModelForward: "ctrl+p",
|
|
54
55
|
cycleModelBackward: "shift+ctrl+p",
|
|
55
56
|
selectModel: "ctrl+l",
|
|
57
|
+
historySearch: "ctrl+r",
|
|
56
58
|
expandTools: "ctrl+o",
|
|
57
59
|
toggleThinking: "ctrl+t",
|
|
58
60
|
externalEditor: "ctrl+g",
|
|
@@ -78,6 +80,7 @@ const APP_ACTIONS: AppAction[] = [
|
|
|
78
80
|
"cycleModelForward",
|
|
79
81
|
"cycleModelBackward",
|
|
80
82
|
"selectModel",
|
|
83
|
+
"historySearch",
|
|
81
84
|
"expandTools",
|
|
82
85
|
"toggleThinking",
|
|
83
86
|
"externalEditor",
|
|
@@ -29,6 +29,7 @@ export class CustomEditor extends Editor {
|
|
|
29
29
|
public onCtrlP?: () => void;
|
|
30
30
|
public onShiftCtrlP?: () => void;
|
|
31
31
|
public onCtrlL?: () => void;
|
|
32
|
+
public onCtrlR?: () => void;
|
|
32
33
|
public onCtrlO?: () => void;
|
|
33
34
|
public onCtrlT?: () => void;
|
|
34
35
|
public onCtrlG?: () => void;
|
|
@@ -113,6 +114,12 @@ export class CustomEditor extends Editor {
|
|
|
113
114
|
return;
|
|
114
115
|
}
|
|
115
116
|
|
|
117
|
+
// Intercept Ctrl+R for history search
|
|
118
|
+
if (matchesKey(data, "ctrl+r") && this.onCtrlR) {
|
|
119
|
+
this.onCtrlR();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
116
123
|
// Intercept Ctrl+O for tool output expansion
|
|
117
124
|
if (isCtrlO(data) && this.onCtrlO) {
|
|
118
125
|
this.onCtrlO();
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Component,
|
|
3
|
+
Container,
|
|
4
|
+
Input,
|
|
5
|
+
isArrowDown,
|
|
6
|
+
isArrowUp,
|
|
7
|
+
isEnter,
|
|
8
|
+
isEscape,
|
|
9
|
+
Spacer,
|
|
10
|
+
Text,
|
|
11
|
+
truncateToWidth,
|
|
12
|
+
visibleWidth,
|
|
13
|
+
} from "@oh-my-pi/pi-tui";
|
|
14
|
+
import type { HistoryEntry, HistoryStorage } from "../../../core/history-storage";
|
|
15
|
+
import { theme } from "../theme/theme";
|
|
16
|
+
import { DynamicBorder } from "./dynamic-border";
|
|
17
|
+
|
|
18
|
+
class HistoryResultsList implements Component {
|
|
19
|
+
private results: HistoryEntry[] = [];
|
|
20
|
+
private selectedIndex = 0;
|
|
21
|
+
private maxVisible = 10;
|
|
22
|
+
|
|
23
|
+
setResults(results: HistoryEntry[], selectedIndex: number): void {
|
|
24
|
+
this.results = results;
|
|
25
|
+
this.selectedIndex = selectedIndex;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
setSelectedIndex(selectedIndex: number): void {
|
|
29
|
+
this.selectedIndex = selectedIndex;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
invalidate(): void {
|
|
33
|
+
// No cached state to invalidate currently
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
render(width: number): string[] {
|
|
37
|
+
const lines: string[] = [];
|
|
38
|
+
|
|
39
|
+
if (this.results.length === 0) {
|
|
40
|
+
lines.push(theme.fg("muted", " No matching history"));
|
|
41
|
+
return lines;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const startIndex = Math.max(
|
|
45
|
+
0,
|
|
46
|
+
Math.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.results.length - this.maxVisible),
|
|
47
|
+
);
|
|
48
|
+
const endIndex = Math.min(startIndex + this.maxVisible, this.results.length);
|
|
49
|
+
|
|
50
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
51
|
+
const entry = this.results[i];
|
|
52
|
+
const isSelected = i === this.selectedIndex;
|
|
53
|
+
|
|
54
|
+
const cursorSymbol = `${theme.nav.cursor} `;
|
|
55
|
+
const cursorWidth = visibleWidth(cursorSymbol);
|
|
56
|
+
const cursor = isSelected ? theme.fg("accent", cursorSymbol) : " ".repeat(cursorWidth);
|
|
57
|
+
const maxWidth = width - cursorWidth;
|
|
58
|
+
|
|
59
|
+
const normalized = entry.prompt.replace(/\s+/g, " ").trim();
|
|
60
|
+
const truncated = truncateToWidth(normalized, maxWidth, theme.format.ellipsis);
|
|
61
|
+
lines.push(cursor + (isSelected ? theme.bold(truncated) : truncated));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (startIndex > 0 || endIndex < this.results.length) {
|
|
65
|
+
const scrollText = ` (${this.selectedIndex + 1}/${this.results.length})`;
|
|
66
|
+
lines.push(theme.fg("muted", truncateToWidth(scrollText, width, "")));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return lines;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export class HistorySearchComponent extends Container {
|
|
74
|
+
private historyStorage: HistoryStorage;
|
|
75
|
+
private searchInput: Input;
|
|
76
|
+
private results: HistoryEntry[] = [];
|
|
77
|
+
private selectedIndex = 0;
|
|
78
|
+
private resultsList: HistoryResultsList;
|
|
79
|
+
private onSelect: (prompt: string) => void;
|
|
80
|
+
private onCancel: () => void;
|
|
81
|
+
private resultLimit = 100;
|
|
82
|
+
|
|
83
|
+
constructor(historyStorage: HistoryStorage, onSelect: (prompt: string) => void, onCancel: () => void) {
|
|
84
|
+
super();
|
|
85
|
+
this.historyStorage = historyStorage;
|
|
86
|
+
this.onSelect = onSelect;
|
|
87
|
+
this.onCancel = onCancel;
|
|
88
|
+
|
|
89
|
+
this.searchInput = new Input();
|
|
90
|
+
this.searchInput.onSubmit = () => {
|
|
91
|
+
const selected = this.results[this.selectedIndex];
|
|
92
|
+
if (selected) {
|
|
93
|
+
this.onSelect(selected.prompt);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
this.searchInput.onEscape = () => {
|
|
97
|
+
this.onCancel();
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
this.resultsList = new HistoryResultsList();
|
|
101
|
+
|
|
102
|
+
this.addChild(new Spacer(1));
|
|
103
|
+
this.addChild(new Text(theme.bold("Search History (Ctrl+R)"), 1, 0));
|
|
104
|
+
this.addChild(new Spacer(1));
|
|
105
|
+
this.addChild(new DynamicBorder());
|
|
106
|
+
this.addChild(new Spacer(1));
|
|
107
|
+
this.addChild(this.searchInput);
|
|
108
|
+
this.addChild(new Spacer(1));
|
|
109
|
+
this.addChild(this.resultsList);
|
|
110
|
+
this.addChild(new Spacer(1));
|
|
111
|
+
this.addChild(new Text(theme.fg("muted", "up/down navigate enter select esc cancel"), 1, 0));
|
|
112
|
+
this.addChild(new Spacer(1));
|
|
113
|
+
this.addChild(new DynamicBorder());
|
|
114
|
+
|
|
115
|
+
this.updateResults();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
handleInput(keyData: string): void {
|
|
119
|
+
if (isArrowUp(keyData)) {
|
|
120
|
+
if (this.results.length === 0) return;
|
|
121
|
+
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
122
|
+
this.resultsList.setSelectedIndex(this.selectedIndex);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (isArrowDown(keyData)) {
|
|
127
|
+
if (this.results.length === 0) return;
|
|
128
|
+
this.selectedIndex = Math.min(this.results.length - 1, this.selectedIndex + 1);
|
|
129
|
+
this.resultsList.setSelectedIndex(this.selectedIndex);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (isEnter(keyData)) {
|
|
134
|
+
const selected = this.results[this.selectedIndex];
|
|
135
|
+
if (selected) {
|
|
136
|
+
this.onSelect(selected.prompt);
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (isEscape(keyData)) {
|
|
142
|
+
this.onCancel();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
this.searchInput.handleInput(keyData);
|
|
147
|
+
this.updateResults();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private updateResults(): void {
|
|
151
|
+
const query = this.searchInput.getValue().trim();
|
|
152
|
+
this.results = query
|
|
153
|
+
? this.historyStorage.search(query, this.resultLimit)
|
|
154
|
+
: this.historyStorage.getRecent(this.resultLimit);
|
|
155
|
+
this.selectedIndex = 0;
|
|
156
|
+
this.resultsList.setResults(this.results, this.selectedIndex);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -27,7 +27,9 @@ import { nanoid } from "nanoid";
|
|
|
27
27
|
import { getAuthPath, getDebugLogPath } from "../../config";
|
|
28
28
|
import type { AgentSession, AgentSessionEvent } from "../../core/agent-session";
|
|
29
29
|
import type { ExtensionUIContext } from "../../core/extensions/index";
|
|
30
|
+
import { HistoryStorage } from "../../core/history-storage";
|
|
30
31
|
import { KeybindingsManager } from "../../core/keybindings";
|
|
32
|
+
import { logger } from "../../core/logger";
|
|
31
33
|
import { type CustomMessage, createCompactionSummaryMessage } from "../../core/messages";
|
|
32
34
|
import { getRecentSessions, type SessionContext, SessionManager } from "../../core/session-manager";
|
|
33
35
|
import { loadSlashCommands } from "../../core/slash-commands";
|
|
@@ -51,6 +53,7 @@ import { CustomEditor } from "./components/custom-editor";
|
|
|
51
53
|
import { CustomMessageComponent } from "./components/custom-message";
|
|
52
54
|
import { DynamicBorder } from "./components/dynamic-border";
|
|
53
55
|
import { ExtensionDashboard } from "./components/extensions";
|
|
56
|
+
import { HistorySearchComponent } from "./components/history-search";
|
|
54
57
|
import { HookEditorComponent } from "./components/hook-editor";
|
|
55
58
|
import { HookInputComponent } from "./components/hook-input";
|
|
56
59
|
import { HookSelectorComponent } from "./components/hook-selector";
|
|
@@ -170,6 +173,8 @@ export class InteractiveMode {
|
|
|
170
173
|
// Slash commands loaded from files (for compaction queue handling)
|
|
171
174
|
private fileSlashCommands = new Set<string>();
|
|
172
175
|
|
|
176
|
+
private historyStorage?: HistoryStorage;
|
|
177
|
+
|
|
173
178
|
// Voice mode state
|
|
174
179
|
private voiceSupervisor: VoiceSupervisor;
|
|
175
180
|
private voiceAutoModeEnabled = false;
|
|
@@ -223,6 +228,12 @@ export class InteractiveMode {
|
|
|
223
228
|
this.statusContainer = new Container();
|
|
224
229
|
this.editor = new CustomEditor(getEditorTheme());
|
|
225
230
|
this.editor.setUseTerminalCursor(true);
|
|
231
|
+
try {
|
|
232
|
+
this.historyStorage = HistoryStorage.open();
|
|
233
|
+
this.editor.setHistoryStorage(this.historyStorage);
|
|
234
|
+
} catch (error) {
|
|
235
|
+
logger.warn("History storage unavailable", { error: String(error) });
|
|
236
|
+
}
|
|
226
237
|
this.editorContainer = new Container();
|
|
227
238
|
this.editorContainer.addChild(this.editor);
|
|
228
239
|
this.statusLine = new StatusLineComponent(session);
|
|
@@ -1047,6 +1058,7 @@ export class InteractiveMode {
|
|
|
1047
1058
|
// Global debug handler on TUI (works regardless of focus)
|
|
1048
1059
|
this.ui.onDebug = () => this.handleDebugCommand();
|
|
1049
1060
|
this.editor.onCtrlL = () => this.showModelSelector();
|
|
1061
|
+
this.editor.onCtrlR = () => this.showHistorySearch();
|
|
1050
1062
|
this.editor.onCtrlO = () => this.toggleToolOutputExpansion();
|
|
1051
1063
|
this.editor.onCtrlT = () => this.toggleThinkingBlockVisibility();
|
|
1052
1064
|
this.editor.onCtrlG = () => this.openExternalEditor();
|
|
@@ -2500,6 +2512,27 @@ export class InteractiveMode {
|
|
|
2500
2512
|
});
|
|
2501
2513
|
}
|
|
2502
2514
|
|
|
2515
|
+
private showHistorySearch(): void {
|
|
2516
|
+
const historyStorage = this.historyStorage;
|
|
2517
|
+
if (!historyStorage) return;
|
|
2518
|
+
|
|
2519
|
+
this.showSelector((done) => {
|
|
2520
|
+
const component = new HistorySearchComponent(
|
|
2521
|
+
historyStorage,
|
|
2522
|
+
(prompt) => {
|
|
2523
|
+
done();
|
|
2524
|
+
this.editor.setText(prompt);
|
|
2525
|
+
this.ui.requestRender();
|
|
2526
|
+
},
|
|
2527
|
+
() => {
|
|
2528
|
+
done();
|
|
2529
|
+
this.ui.requestRender();
|
|
2530
|
+
},
|
|
2531
|
+
);
|
|
2532
|
+
return { component, focus: component };
|
|
2533
|
+
});
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2503
2536
|
/**
|
|
2504
2537
|
* Show the Extension Control Center dashboard.
|
|
2505
2538
|
* Replaces /status with a unified view of all providers and extensions.
|
|
@@ -3261,6 +3294,7 @@ export class InteractiveMode {
|
|
|
3261
3294
|
| \`Shift+Ctrl+P\` | Cycle role models (temporary) |
|
|
3262
3295
|
| \`Ctrl+Y\` | Select model (temporary) |
|
|
3263
3296
|
| \`Ctrl+L\` | Select model (set roles) |
|
|
3297
|
+
| \`Ctrl+R\` | Search prompt history |
|
|
3264
3298
|
| \`Ctrl+O\` | Toggle tool output expansion |
|
|
3265
3299
|
| \`Ctrl+T\` | Toggle thinking block visibility |
|
|
3266
3300
|
| \`Ctrl+G\` | Edit message in external editor |
|