pi-input-history 1.0.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/.github/workflows/publish.yml +18 -0
- package/LICENSE +21 -0
- package/README.md +60 -0
- package/assets/screenshot.png +0 -0
- package/index.ts +368 -0
- package/package.json +35 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
name: Publish to npm
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
- uses: actions/setup-node@v4
|
|
13
|
+
with:
|
|
14
|
+
node-version: 20
|
|
15
|
+
registry-url: https://registry.npmjs.org
|
|
16
|
+
- run: npm publish --access public
|
|
17
|
+
env:
|
|
18
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 ouzhenkun
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# pi-input-history
|
|
2
|
+
|
|
3
|
+
**Cross-session prompt history and fuzzy Ctrl+R search for pi.**
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/pi-input-history)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
## Why
|
|
9
|
+
|
|
10
|
+
Pi's built-in ↑/↓ history only covers the current session and is lost on reload. This extension persists your last 100 prompts across sessions and adds fuzzy **Ctrl+R** search to find any past prompt instantly.
|
|
11
|
+
|
|
12
|
+

|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pi install npm:pi-input-history
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Or from git:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pi install git:github.com/ouzhenkun/pi-input-history
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
### Persistent History
|
|
29
|
+
|
|
30
|
+
On session start, your last 100 prompts across all sessions are loaded into the editor. Use **↑/↓** arrows to browse them as usual.
|
|
31
|
+
|
|
32
|
+
### Reverse Search (Ctrl+R)
|
|
33
|
+
|
|
34
|
+
1. Press **Ctrl+R** to open the search overlay.
|
|
35
|
+
2. Type to fuzzy-filter history (subsequence matching, space-separated multi-token).
|
|
36
|
+
3. Matched characters are highlighted with your theme's accent color.
|
|
37
|
+
4. Navigate and accept:
|
|
38
|
+
|
|
39
|
+
| Key | Action |
|
|
40
|
+
| --- | --- |
|
|
41
|
+
| `Ctrl+R` / `↑` | Cycle to older match |
|
|
42
|
+
| `Ctrl+S` / `↓` | Cycle to newer match |
|
|
43
|
+
| `Enter` | Accept match into editor |
|
|
44
|
+
| `Esc` / `Ctrl+G` | Cancel |
|
|
45
|
+
|
|
46
|
+
## Features
|
|
47
|
+
|
|
48
|
+
- **Cross-session persistence** — history survives across sessions automatically.
|
|
49
|
+
- **Fuzzy subsequence matching** — type partial characters in order, multi-token support with spaces.
|
|
50
|
+
- **Character-level highlighting** — matched positions shown with accent color underline.
|
|
51
|
+
- **Deduplication** — no duplicate entries across sessions.
|
|
52
|
+
- **Current session awareness** — merges live branch history with cached cross-session history.
|
|
53
|
+
|
|
54
|
+
## Acknowledgments
|
|
55
|
+
|
|
56
|
+
The Ctrl+R reverse search component is inspired by [pi-readline-search](https://github.com/mrshu/pi-readline-search) by [@mrshu](https://github.com/mrshu).
|
|
57
|
+
|
|
58
|
+
## License
|
|
59
|
+
|
|
60
|
+
MIT
|
|
Binary file
|
package/index.ts
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent History + Ctrl+R Reverse Search
|
|
3
|
+
*
|
|
4
|
+
* - Loads recent prompts from previous sessions into up/down history on startup.
|
|
5
|
+
* - Ctrl+R opens a reverse search overlay (fuzzy subsequence matching).
|
|
6
|
+
*
|
|
7
|
+
* Hotkeys while searching:
|
|
8
|
+
* - Ctrl+R / ↑ : older match
|
|
9
|
+
* - Ctrl+S / ↓ : newer match
|
|
10
|
+
* - Enter : accept match (fills editor)
|
|
11
|
+
* - Esc/Ctrl+G : cancel
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
CustomEditor,
|
|
16
|
+
SessionManager,
|
|
17
|
+
type ExtensionAPI,
|
|
18
|
+
} from "@earendil-works/pi-coding-agent";
|
|
19
|
+
import type { UserMessage } from "@earendil-works/pi-ai";
|
|
20
|
+
import {
|
|
21
|
+
Input,
|
|
22
|
+
Key,
|
|
23
|
+
matchesKey,
|
|
24
|
+
truncateToWidth,
|
|
25
|
+
type Component,
|
|
26
|
+
type Focusable,
|
|
27
|
+
type TUI,
|
|
28
|
+
} from "@earendil-works/pi-tui";
|
|
29
|
+
|
|
30
|
+
const MAX_MESSAGES = 100;
|
|
31
|
+
|
|
32
|
+
// ─── Extension Entry ───────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
export default function (pi: ExtensionAPI) {
|
|
35
|
+
let historyCache: string[] = [];
|
|
36
|
+
|
|
37
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
38
|
+
const items = await loadRecentPrompts(ctx.cwd, MAX_MESSAGES);
|
|
39
|
+
historyCache = items;
|
|
40
|
+
|
|
41
|
+
if (items.length === 0) return;
|
|
42
|
+
|
|
43
|
+
const prevComponentFactory = ctx.ui.getEditorComponent();
|
|
44
|
+
ctx.ui.setEditorComponent((tui, theme, keybindings) => {
|
|
45
|
+
const editor =
|
|
46
|
+
prevComponentFactory?.(tui, theme, keybindings) ??
|
|
47
|
+
new CustomEditor(tui, theme, keybindings);
|
|
48
|
+
|
|
49
|
+
for (let i = items.length - 1; i >= 0; i--) {
|
|
50
|
+
editor.addToHistory?.(items[i]!);
|
|
51
|
+
}
|
|
52
|
+
return editor;
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Ctrl+R: reverse search
|
|
57
|
+
pi.registerShortcut("ctrl+r", {
|
|
58
|
+
description: "Reverse search through prompt history",
|
|
59
|
+
handler: async (ctx) => {
|
|
60
|
+
// Merge cached history with current session's branch history
|
|
61
|
+
const branchHistory = collectBranchHistory(ctx);
|
|
62
|
+
const merged = mergeHistory(branchHistory, historyCache);
|
|
63
|
+
|
|
64
|
+
if (merged.length === 0) {
|
|
65
|
+
ctx.ui.notify("No prompt history yet.", "info");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const selected = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
|
|
70
|
+
return new ReverseSearchComponent(tui, theme, merged, done);
|
|
71
|
+
}, { overlay: true, overlayOptions: { anchor: "bottom-center", width: "100%" } });
|
|
72
|
+
|
|
73
|
+
if (selected === null) return;
|
|
74
|
+
ctx.ui.setEditorText(selected);
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─── Reverse Search Component ──────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
type Done = (value: string | null) => void;
|
|
82
|
+
|
|
83
|
+
/** Subsequence fuzzy match: all chars in needle appear in haystack in order. */
|
|
84
|
+
function subsequence(haystack: string, needle: string): boolean {
|
|
85
|
+
let hi = 0;
|
|
86
|
+
for (let ni = 0; ni < needle.length; ni++) {
|
|
87
|
+
const idx = haystack.indexOf(needle[ni], hi);
|
|
88
|
+
if (idx === -1) return false;
|
|
89
|
+
hi = idx + 1;
|
|
90
|
+
}
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function fuzzyMatch(item: string, query: string): boolean {
|
|
95
|
+
if (!query) return true;
|
|
96
|
+
const lower = item.toLowerCase();
|
|
97
|
+
const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
98
|
+
return tokens.every((t) => subsequence(lower, t));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function toSingleLinePreview(text: string): string {
|
|
102
|
+
return text.replace(/\s+/g, " ").trim();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Highlight matched characters (subsequence) with underline + accent color. */
|
|
106
|
+
function highlightMatch(text: string, query: string, theme: any, maxWidth: number): string {
|
|
107
|
+
// Truncate plain text first to ensure it fits
|
|
108
|
+
const truncated = truncateToWidth(text, maxWidth);
|
|
109
|
+
// Strip any ANSI that truncateToWidth might have added for ellipsis
|
|
110
|
+
const plain = truncated.replace(/\x1b\[[0-9;]*m/g, "");
|
|
111
|
+
|
|
112
|
+
if (!query) return theme.fg("text", plain);
|
|
113
|
+
|
|
114
|
+
// Find positions of subsequence-matched characters
|
|
115
|
+
const lower = plain.toLowerCase();
|
|
116
|
+
const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
117
|
+
const matchPositions = new Set<number>();
|
|
118
|
+
|
|
119
|
+
for (const token of tokens) {
|
|
120
|
+
let hi = 0;
|
|
121
|
+
for (let ni = 0; ni < token.length; ni++) {
|
|
122
|
+
const idx = lower.indexOf(token[ni], hi);
|
|
123
|
+
if (idx !== -1) {
|
|
124
|
+
matchPositions.add(idx);
|
|
125
|
+
hi = idx + 1;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Build styled string — group consecutive chars to reduce ANSI overhead
|
|
131
|
+
let result = "";
|
|
132
|
+
let i = 0;
|
|
133
|
+
while (i < plain.length) {
|
|
134
|
+
if (matchPositions.has(i)) {
|
|
135
|
+
// Collect consecutive matched chars
|
|
136
|
+
let j = i;
|
|
137
|
+
while (j < plain.length && matchPositions.has(j)) j++;
|
|
138
|
+
// Accent color + underline
|
|
139
|
+
result += `\x1b[4m${theme.fg("accent", plain.slice(i, j))}\x1b[24m`;
|
|
140
|
+
i = j;
|
|
141
|
+
} else {
|
|
142
|
+
// Collect consecutive non-matched chars
|
|
143
|
+
let j = i;
|
|
144
|
+
while (j < plain.length && !matchPositions.has(j)) j++;
|
|
145
|
+
result += theme.fg("text", plain.slice(i, j));
|
|
146
|
+
i = j;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
class ReverseSearchComponent implements Component, Focusable {
|
|
153
|
+
private _focused = false;
|
|
154
|
+
private readonly input = new Input();
|
|
155
|
+
|
|
156
|
+
private query = "";
|
|
157
|
+
private matchIndices: number[] = [];
|
|
158
|
+
private matchPointer = 0;
|
|
159
|
+
|
|
160
|
+
constructor(
|
|
161
|
+
private readonly tui: TUI,
|
|
162
|
+
private readonly theme: any,
|
|
163
|
+
private readonly history: string[],
|
|
164
|
+
private readonly done: Done,
|
|
165
|
+
) {
|
|
166
|
+
this.input.onEscape = () => this.done(null);
|
|
167
|
+
this.input.onSubmit = () => {
|
|
168
|
+
const match = this.getCurrentMatch();
|
|
169
|
+
this.done(match ?? null);
|
|
170
|
+
};
|
|
171
|
+
this.recomputeMatches(true);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
get focused(): boolean {
|
|
175
|
+
return this._focused;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
set focused(value: boolean) {
|
|
179
|
+
this._focused = value;
|
|
180
|
+
this.input.focused = value;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private recomputeMatches(resetPointer: boolean): void {
|
|
184
|
+
const matches: number[] = [];
|
|
185
|
+
for (let i = 0; i < this.history.length; i++) {
|
|
186
|
+
if (fuzzyMatch(this.history[i]!, this.query)) {
|
|
187
|
+
matches.push(i);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
this.matchIndices = matches;
|
|
191
|
+
if (resetPointer) this.matchPointer = 0;
|
|
192
|
+
if (this.matchPointer >= this.matchIndices.length) {
|
|
193
|
+
this.matchPointer = Math.max(0, this.matchIndices.length - 1);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private getCurrentMatch(): string | undefined {
|
|
198
|
+
if (this.matchIndices.length === 0) return undefined;
|
|
199
|
+
const index = this.matchIndices[this.matchPointer];
|
|
200
|
+
return this.history[index!];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private cycleOlder(): void {
|
|
204
|
+
if (this.matchIndices.length === 0) return;
|
|
205
|
+
this.matchPointer = (this.matchPointer + 1) % this.matchIndices.length;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private cycleNewer(): void {
|
|
209
|
+
if (this.matchIndices.length === 0) return;
|
|
210
|
+
this.matchPointer = (this.matchPointer - 1 + this.matchIndices.length) % this.matchIndices.length;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
handleInput(data: string): void {
|
|
214
|
+
if (matchesKey(data, Key.ctrl("r")) || matchesKey(data, Key.up)) {
|
|
215
|
+
this.cycleOlder();
|
|
216
|
+
this.tui.requestRender();
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (matchesKey(data, Key.ctrl("s")) || matchesKey(data, Key.down)) {
|
|
221
|
+
this.cycleNewer();
|
|
222
|
+
this.tui.requestRender();
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (matchesKey(data, Key.ctrl("g"))) {
|
|
227
|
+
this.done(null);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const before = this.input.getValue();
|
|
232
|
+
this.input.handleInput(data);
|
|
233
|
+
const after = this.input.getValue();
|
|
234
|
+
|
|
235
|
+
if (after !== before) {
|
|
236
|
+
this.query = after;
|
|
237
|
+
this.recomputeMatches(true);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
this.tui.requestRender();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
render(width: number): string[] {
|
|
244
|
+
const t = this.theme;
|
|
245
|
+
const currentMatch = this.getCurrentMatch();
|
|
246
|
+
|
|
247
|
+
// Reserve space for prefix and counter (use fixed max counter width)
|
|
248
|
+
const prefix = "(reverse-search) ";
|
|
249
|
+
const maxCounterWidth = 10; // " [xx/xx]" is enough
|
|
250
|
+
const availableWidth = Math.max(10, width - prefix.length - maxCounterWidth);
|
|
251
|
+
|
|
252
|
+
const counterText = this.matchIndices.length > 0
|
|
253
|
+
? ` [${this.matchPointer + 1}/${this.matchIndices.length}]`
|
|
254
|
+
: " [0/0]";
|
|
255
|
+
|
|
256
|
+
const matchPreview = currentMatch
|
|
257
|
+
? highlightMatch(toSingleLinePreview(currentMatch), this.query, t, availableWidth)
|
|
258
|
+
: t.fg("warning", "no match");
|
|
259
|
+
|
|
260
|
+
const counter = t.fg("dim", counterText);
|
|
261
|
+
|
|
262
|
+
const header =
|
|
263
|
+
t.fg("accent", prefix) +
|
|
264
|
+
matchPreview +
|
|
265
|
+
counter;
|
|
266
|
+
|
|
267
|
+
const inputLine = truncateToWidth(this.input.render(width)[0] ?? "", width);
|
|
268
|
+
const help = truncateToWidth(t.fg("dim", "ctrl+r/↑ older • ctrl+s/↓ newer • enter accept • esc cancel"), width);
|
|
269
|
+
|
|
270
|
+
return [truncateToWidth(header, width), inputLine, help];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
invalidate(): void {
|
|
274
|
+
this.input.invalidate();
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ─── History Collection ────────────────────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
/** Collect user messages from the current session branch (for up-to-date search). */
|
|
281
|
+
function collectBranchHistory(ctx: any): string[] {
|
|
282
|
+
const history: string[] = [];
|
|
283
|
+
try {
|
|
284
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
285
|
+
if (entry.type !== "message") continue;
|
|
286
|
+
const message = entry.message as Record<string, any>;
|
|
287
|
+
if (message.role !== "user") continue;
|
|
288
|
+
const text = extractText(message.content)?.trim();
|
|
289
|
+
if (text && text.length > 0) history.push(text);
|
|
290
|
+
}
|
|
291
|
+
} catch {}
|
|
292
|
+
return history.reverse(); // newest first
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Merge branch history (current session) with cached cross-session history, deduplicated. */
|
|
296
|
+
function mergeHistory(branchHistory: string[], cached: string[]): string[] {
|
|
297
|
+
const seen = new Set<string>();
|
|
298
|
+
const merged: string[] = [];
|
|
299
|
+
for (const item of branchHistory) {
|
|
300
|
+
if (!seen.has(item)) {
|
|
301
|
+
seen.add(item);
|
|
302
|
+
merged.push(item);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
for (const item of cached) {
|
|
306
|
+
if (!seen.has(item)) {
|
|
307
|
+
seen.add(item);
|
|
308
|
+
merged.push(item);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return merged;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function loadRecentPrompts(
|
|
315
|
+
cwd: string,
|
|
316
|
+
maxMessages: number,
|
|
317
|
+
): Promise<string[]> {
|
|
318
|
+
try {
|
|
319
|
+
const sessions = await SessionManager.list(cwd);
|
|
320
|
+
const sorted = sessions.sort(
|
|
321
|
+
(a, b) => b.modified.getTime() - a.modified.getTime(),
|
|
322
|
+
);
|
|
323
|
+
const allMessages: string[] = [];
|
|
324
|
+
const seen = new Set<string>();
|
|
325
|
+
|
|
326
|
+
for (const session of sorted) {
|
|
327
|
+
if (allMessages.length >= maxMessages) break;
|
|
328
|
+
const userMessages = extractUserMessages(session.path);
|
|
329
|
+
for (const msg of userMessages) {
|
|
330
|
+
if (allMessages.length >= maxMessages) break;
|
|
331
|
+
const trimmed = msg.trim();
|
|
332
|
+
if (trimmed && !seen.has(trimmed)) {
|
|
333
|
+
seen.add(trimmed);
|
|
334
|
+
allMessages.push(trimmed);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return allMessages;
|
|
339
|
+
} catch {
|
|
340
|
+
return [];
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function extractUserMessages(sessionPath: string): string[] {
|
|
345
|
+
try {
|
|
346
|
+
const entries = SessionManager.open(sessionPath).getEntries();
|
|
347
|
+
const messages: string[] = [];
|
|
348
|
+
for (const entry of entries) {
|
|
349
|
+
if (entry.type !== "message" || entry.message.role !== "user") continue;
|
|
350
|
+
const text = extractText(entry.message.content);
|
|
351
|
+
if (text) messages.push(text);
|
|
352
|
+
}
|
|
353
|
+
// Reverse so newest messages come first within each session
|
|
354
|
+
return messages.reverse();
|
|
355
|
+
} catch {
|
|
356
|
+
return [];
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function extractText(content: UserMessage["content"]): string | null {
|
|
361
|
+
if (typeof content === "string") return content || null;
|
|
362
|
+
return (
|
|
363
|
+
content.find(
|
|
364
|
+
(c): c is { type: "text"; text: string } =>
|
|
365
|
+
c.type === "text" && typeof c.text === "string" && c.text.length > 0,
|
|
366
|
+
)?.text ?? null
|
|
367
|
+
);
|
|
368
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-input-history",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Cross-session prompt history and fuzzy Ctrl+R search for pi.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi",
|
|
7
|
+
"pi-package",
|
|
8
|
+
"pi-extension",
|
|
9
|
+
"history",
|
|
10
|
+
"reverse-search",
|
|
11
|
+
"ctrl-r",
|
|
12
|
+
"prompt-history",
|
|
13
|
+
"fuzzy-search"
|
|
14
|
+
],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": "ouzhenkun",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/ouzhenkun/pi-input-history.git"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/ouzhenkun/pi-input-history",
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/ouzhenkun/pi-input-history/issues"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
27
|
+
"@earendil-works/pi-tui": "*",
|
|
28
|
+
"@earendil-works/pi-ai": "*"
|
|
29
|
+
},
|
|
30
|
+
"pi": {
|
|
31
|
+
"extensions": [
|
|
32
|
+
"./index.ts"
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
}
|