@yusukeshib/pi-stash 0.1.0 → 0.3.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/README.md +21 -16
- package/extensions/index.ts +111 -66
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pi-stash
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A per-session stash of reusable prompt fragments for the [Pi coding agent](https://pi.dev).
|
|
4
4
|
|
|
5
5
|
Jot a prompt down whenever you think of it mid-task, then pop it into the editor
|
|
6
6
|
when you actually need it. Think of it as a stack-like scratchpad for prompts —
|
|
@@ -50,7 +50,20 @@ pi install npm:@yusukeshib/pi-stash
|
|
|
50
50
|
|---------|--------|
|
|
51
51
|
| `/stash <text>` | **Push** — save the given text onto the stash. |
|
|
52
52
|
| `/stash` | **Pop** — pick a saved entry, insert it into the editor, and remove it from the stash. Run repeatedly to stack several fragments together. |
|
|
53
|
-
| `/stash-clear` | Delete every stashed entry (with confirm). |
|
|
53
|
+
| `/stash-clear` | Delete every stashed entry in this session (with confirm). |
|
|
54
|
+
|
|
55
|
+
### Shortcut
|
|
56
|
+
|
|
57
|
+
| Key | Action |
|
|
58
|
+
|-----|--------|
|
|
59
|
+
| `Ctrl+S` | If the editor has text, **push** it onto the stash and clear the editor. If the editor is empty, **pop** a saved entry into it. |
|
|
60
|
+
|
|
61
|
+
`Ctrl+S` is the one-key way to do what `/stash` does: park the prompt you're
|
|
62
|
+
halfway through typing, or pull one back — without typing the command. Rebind it
|
|
63
|
+
in `~/.pi/agent/keybindings.json` if it clashes with another shortcut.
|
|
64
|
+
|
|
65
|
+
While the stash is non-empty, a red `stash:N` badge is shown in the footer so
|
|
66
|
+
you never forget you have prompts parked.
|
|
54
67
|
|
|
55
68
|
## How it works
|
|
56
69
|
|
|
@@ -65,21 +78,13 @@ build up by hand during real work:
|
|
|
65
78
|
|
|
66
79
|
## Storage
|
|
67
80
|
|
|
68
|
-
Entries
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
~/.pi/agent/prompt-stash.json
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
Override the location with the `PI_STASH_PATH` environment variable:
|
|
76
|
-
|
|
77
|
-
```bash
|
|
78
|
-
export PI_STASH_PATH="$HOME/.config/pi/stash.json"
|
|
79
|
-
```
|
|
81
|
+
Entries are stored **inside the session itself** as custom session entries —
|
|
82
|
+
each Pi session has its own independent stash. It survives restarts and
|
|
83
|
+
`/resume`, and follows branching (`/fork`, `/clone`) correctly: a forked
|
|
84
|
+
session sees the stash as it was at the fork point.
|
|
80
85
|
|
|
81
|
-
|
|
82
|
-
|
|
86
|
+
Nothing is written outside the session file, and the stash does not
|
|
87
|
+
participate in the LLM context.
|
|
83
88
|
|
|
84
89
|
## License
|
|
85
90
|
|
package/extensions/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* pi-stash — a
|
|
3
|
-
* agent. Jot a prompt down whenever you think of it, then later pull
|
|
4
|
-
* list and pop one into the editor to compose your next message.
|
|
2
|
+
* pi-stash — a per-session stash of reusable prompt fragments for the Pi
|
|
3
|
+
* coding agent. Jot a prompt down whenever you think of it, then later pull
|
|
4
|
+
* up the list and pop one into the editor to compose your next message.
|
|
5
5
|
*
|
|
6
6
|
* Unlike file-based prompt templates (static `/name` commands), this is an
|
|
7
7
|
* ad-hoc, mutable, stack-like backlog you build up by hand during real work.
|
|
@@ -12,43 +12,31 @@
|
|
|
12
12
|
* remove it from the stash. Run repeatedly to stack fragments.
|
|
13
13
|
* /stash-clear Delete every entry (with confirm).
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* Shortcut:
|
|
16
|
+
* Ctrl+S If the editor holds text, push it onto the stash (and clear
|
|
17
|
+
* the editor); otherwise pop a saved entry into the editor.
|
|
18
|
+
* A one-key way to park the prompt you're typing, or pull one
|
|
19
|
+
* back, without typing `/stash`.
|
|
20
|
+
*
|
|
21
|
+
* Storage: stash entries are persisted INSIDE the session itself via custom
|
|
22
|
+
* session entries (`pi.appendEntry`). Each session has its own stash, it
|
|
23
|
+
* survives restarts/resume, and it follows branching (fork/clone) correctly.
|
|
24
|
+
*
|
|
25
|
+
* While the stash is non-empty, a red badge with the entry count is shown in
|
|
26
|
+
* the footer so you don't forget about pending prompts.
|
|
19
27
|
*/
|
|
20
28
|
|
|
21
|
-
import { type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
22
|
-
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
23
|
-
import { homedir } from "node:os";
|
|
24
|
-
import { dirname, join } from "node:path";
|
|
29
|
+
import { type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
25
30
|
|
|
26
31
|
interface StashEntry {
|
|
27
32
|
text: string;
|
|
28
33
|
addedAt: number;
|
|
29
34
|
}
|
|
30
35
|
|
|
31
|
-
const
|
|
36
|
+
const CUSTOM_TYPE = "pi-stash-state";
|
|
37
|
+
const STATUS_KEY = "pi-stash";
|
|
32
38
|
const PREVIEW_LEN = 72;
|
|
33
39
|
|
|
34
|
-
function load(): StashEntry[] {
|
|
35
|
-
try {
|
|
36
|
-
const raw = readFileSync(STORE_PATH, "utf8");
|
|
37
|
-
const parsed = JSON.parse(raw);
|
|
38
|
-
if (Array.isArray(parsed)) {
|
|
39
|
-
return parsed.filter((e): e is StashEntry => e && typeof e.text === "string");
|
|
40
|
-
}
|
|
41
|
-
} catch {
|
|
42
|
-
// missing or corrupt → empty stash
|
|
43
|
-
}
|
|
44
|
-
return [];
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function save(entries: StashEntry[]): void {
|
|
48
|
-
mkdirSync(dirname(STORE_PATH), { recursive: true });
|
|
49
|
-
writeFileSync(STORE_PATH, `${JSON.stringify(entries, null, 2)}\n`, "utf8");
|
|
50
|
-
}
|
|
51
|
-
|
|
52
40
|
/** One-line, length-bounded preview for select menus. */
|
|
53
41
|
function preview(text: string, index: number): string {
|
|
54
42
|
const oneLine = text.replace(/\s+/g, " ").trim();
|
|
@@ -58,58 +46,115 @@ function preview(text: string, index: number): string {
|
|
|
58
46
|
}
|
|
59
47
|
|
|
60
48
|
export default function (pi: ExtensionAPI) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
49
|
+
// In-memory stash for the current session; persisted as custom entries.
|
|
50
|
+
let entries: StashEntry[] = [];
|
|
51
|
+
|
|
52
|
+
function persist(): void {
|
|
53
|
+
pi.appendEntry(CUSTOM_TYPE, { entries });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Red, hard-to-miss footer badge while the stash is non-empty. */
|
|
57
|
+
function updateStatus(ctx: ExtensionContext): void {
|
|
58
|
+
if (entries.length > 0) {
|
|
59
|
+
// White on red background (raw ANSI so it stays red in any theme).
|
|
60
|
+
ctx.ui.setStatus(STATUS_KEY, `\x1b[41m\x1b[97m\x1b[1m stash:${entries.length} \x1b[0m`);
|
|
61
|
+
} else {
|
|
62
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Restore stash from the session (last persisted state on the active path).
|
|
67
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
68
|
+
entries = [];
|
|
69
|
+
for (const entry of ctx.sessionManager.getEntries()) {
|
|
70
|
+
if (entry.type === "custom" && entry.customType === CUSTOM_TYPE) {
|
|
71
|
+
const data = entry.data as { entries?: unknown } | undefined;
|
|
72
|
+
if (data && Array.isArray(data.entries)) {
|
|
73
|
+
entries = data.entries.filter(
|
|
74
|
+
(e): e is StashEntry => !!e && typeof (e as StashEntry).text === "string",
|
|
75
|
+
);
|
|
72
76
|
}
|
|
73
|
-
entries.push({ text, addedAt: Date.now() });
|
|
74
|
-
save(entries);
|
|
75
|
-
ctx.ui.notify(`Stashed (${entries.length} total)`, "info");
|
|
76
|
-
return;
|
|
77
77
|
}
|
|
78
|
+
}
|
|
79
|
+
updateStatus(ctx);
|
|
80
|
+
});
|
|
78
81
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Core stash behaviour shared by the `/stash` command and the Ctrl+S
|
|
84
|
+
* shortcut. With text → push; without text → pop into the editor.
|
|
85
|
+
*/
|
|
86
|
+
async function runStash(rawText: string, ctx: ExtensionContext): Promise<void> {
|
|
87
|
+
const text = (rawText ?? "").trim();
|
|
88
|
+
|
|
89
|
+
// PUSH: /stash <text>
|
|
90
|
+
if (text) {
|
|
91
|
+
if (entries.some((e) => e.text === text)) {
|
|
92
|
+
ctx.ui.notify("Already in stash", "info");
|
|
83
93
|
return;
|
|
84
94
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
ctx.ui.
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
95
|
+
entries.push({ text, addedAt: Date.now() });
|
|
96
|
+
persist();
|
|
97
|
+
updateStatus(ctx);
|
|
98
|
+
ctx.ui.notify(`Stashed (${entries.length} total)`, "info");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// POP: /stash → pick, insert into editor, remove from stash
|
|
103
|
+
if (entries.length === 0) {
|
|
104
|
+
ctx.ui.notify("Stash is empty. Add one with /stash <text>", "info");
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const labels = entries.map((e, i) => preview(e.text, i));
|
|
108
|
+
const choice = await ctx.ui.select("Pop prompt:", labels);
|
|
109
|
+
if (choice === undefined) return; // cancelled / timed out
|
|
110
|
+
const idx = labels.indexOf(choice);
|
|
111
|
+
if (idx < 0) return;
|
|
112
|
+
|
|
113
|
+
const chosen = entries[idx].text;
|
|
114
|
+
const current = ctx.ui.getEditorText() ?? "";
|
|
115
|
+
const next = current.trim().length > 0 ? `${current}\n${chosen}` : chosen;
|
|
116
|
+
ctx.ui.setEditorText(next);
|
|
117
|
+
|
|
118
|
+
// pop = remove the chosen entry
|
|
119
|
+
entries.splice(idx, 1);
|
|
120
|
+
persist();
|
|
121
|
+
updateStatus(ctx);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
pi.registerCommand("stash", {
|
|
125
|
+
description: "Push text (with arg) or pop an entry into the editor (no arg)",
|
|
126
|
+
handler: async (args, ctx) => {
|
|
127
|
+
await runStash(args ?? "", ctx);
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Ctrl+S → same as bare `/stash`: pop an entry into the editor. If the
|
|
132
|
+
// editor already holds text, push it onto the stash instead.
|
|
133
|
+
pi.registerShortcut("ctrl+s", {
|
|
134
|
+
description: "Stash: push editor text, or pop a saved prompt",
|
|
135
|
+
handler: async (ctx) => {
|
|
136
|
+
const editorText = (ctx.ui.getEditorText() ?? "").trim();
|
|
137
|
+
if (editorText) {
|
|
138
|
+
ctx.ui.setEditorText("");
|
|
139
|
+
await runStash(editorText, ctx);
|
|
140
|
+
} else {
|
|
141
|
+
await runStash("", ctx);
|
|
142
|
+
}
|
|
99
143
|
},
|
|
100
144
|
});
|
|
101
145
|
|
|
102
146
|
pi.registerCommand("stash-clear", {
|
|
103
|
-
description: "Delete every stashed prompt",
|
|
147
|
+
description: "Delete every stashed prompt in this session",
|
|
104
148
|
handler: async (_args, ctx) => {
|
|
105
|
-
const entries = load();
|
|
106
149
|
if (entries.length === 0) {
|
|
107
150
|
ctx.ui.notify("Stash is already empty", "info");
|
|
108
151
|
return;
|
|
109
152
|
}
|
|
110
153
|
const ok = await ctx.ui.confirm("Clear the entire stash?", `${entries.length} entries will be deleted`);
|
|
111
154
|
if (!ok) return;
|
|
112
|
-
|
|
155
|
+
entries = [];
|
|
156
|
+
persist();
|
|
157
|
+
updateStatus(ctx);
|
|
113
158
|
ctx.ui.notify("Stash cleared", "info");
|
|
114
159
|
},
|
|
115
160
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yusukeshib/pi-stash",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "A personal stash of reusable prompt fragments for the Pi coding agent. Push prompts you think of mid-task, then pop them into the editor when you need them.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|