@mjakl/pi-interlude 0.9.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/LICENSE +21 -0
- package/README.md +71 -0
- package/index.ts +125 -0
- package/package.json +68 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mjakl
|
|
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,71 @@
|
|
|
1
|
+
# pi-interlude
|
|
2
|
+
|
|
3
|
+
Ever had a nice prompt prepared and the agent comes back with a question, or you need to check something before continuing? Deleting the draft is wasted effort, copy/pasting to make space for the interlude prompt is cumbersome. `zsh` has this fantastic stash command - `Esc-q` - that allows you to temporarily remove the current command and it will restore it after your interlude command is finished. `pi-interlude` brings this to pi. Install it, reload, enter something, press `Ctrl-i`, enter something else, send it, and watch with awe as your previous prompt reappears 🎉.
|
|
4
|
+
|
|
5
|
+
A pi extension that lets you stash the current draft, send a one-off interlude message, and then restore the original draft.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
### Option 1: Install from npm (recommended)
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pi install npm:@mjakl/pi-interlude
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### Option 2: Install via git
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pi install git:github.com/mjakl/pi-interlude
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Option 3: Install local package
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pi install ./
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## What it does
|
|
28
|
+
|
|
29
|
+
1. Press the interlude shortcut
|
|
30
|
+
2. Your current editor text is stashed and the input box is cleared
|
|
31
|
+
3. Type and send a temporary message
|
|
32
|
+
4. Your previous draft is restored into the editor
|
|
33
|
+
|
|
34
|
+
Press the shortcut again before sending to restore the stashed draft manually.
|
|
35
|
+
|
|
36
|
+
Note: the extension currently stashes editor text only. If your draft includes attachments, those are not restored.
|
|
37
|
+
|
|
38
|
+
## Default shortcuts
|
|
39
|
+
|
|
40
|
+
- `f6`
|
|
41
|
+
- `ctrl+i` (`i` for "interlude")
|
|
42
|
+
|
|
43
|
+
`f6` is the robust default.
|
|
44
|
+
`ctrl+i` is a mnemonic secondary shortcut, but in many terminals it is indistinguishable from `tab`, so it may conflict with autocomplete.
|
|
45
|
+
|
|
46
|
+
## Configuration
|
|
47
|
+
|
|
48
|
+
The extension reads a custom `interlude` key from pi's existing keybindings file:
|
|
49
|
+
|
|
50
|
+
- `~/.pi/agent/keybindings.json`
|
|
51
|
+
|
|
52
|
+
Example:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"interlude": ["f6", "ctrl+i"]
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Single shortcut example:
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"interlude": "f6"
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
If `interlude` is not set, the extension defaults to `f6` and `ctrl+i`.
|
|
69
|
+
|
|
70
|
+
After changing `keybindings.json`, run `/reload` in pi.
|
|
71
|
+
|
package/index.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_SHORTCUTS = ["f6", "ctrl+i"] as const;
|
|
7
|
+
const STATUS_KEY = "interlude";
|
|
8
|
+
const KEYBINDINGS_PATH = path.join(os.homedir(), ".pi", "agent", "keybindings.json");
|
|
9
|
+
|
|
10
|
+
type ShortcutConfig = string | string[] | undefined;
|
|
11
|
+
|
|
12
|
+
interface KeybindingsConfig {
|
|
13
|
+
interlude?: ShortcutConfig;
|
|
14
|
+
[key: string]: unknown;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readKeybindings(filePath: string): KeybindingsConfig {
|
|
18
|
+
try {
|
|
19
|
+
if (!fs.existsSync(filePath)) return {};
|
|
20
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
21
|
+
const parsed = JSON.parse(raw);
|
|
22
|
+
if (!parsed || typeof parsed !== "object") return {};
|
|
23
|
+
return parsed as KeybindingsConfig;
|
|
24
|
+
} catch {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeShortcuts(value: ShortcutConfig): string[] {
|
|
30
|
+
const values = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
|
|
31
|
+
const normalized = values
|
|
32
|
+
.map((shortcut) => shortcut.trim().toLowerCase())
|
|
33
|
+
.filter((shortcut) => shortcut.length > 0);
|
|
34
|
+
return normalized.length > 0 ? [...new Set(normalized)] : [...DEFAULT_SHORTCUTS];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default function (pi: ExtensionAPI) {
|
|
38
|
+
const keybindings = readKeybindings(KEYBINDINGS_PATH);
|
|
39
|
+
const shortcuts = normalizeShortcuts(keybindings.interlude);
|
|
40
|
+
let stashedDraft: string | null = null;
|
|
41
|
+
let armed = false;
|
|
42
|
+
|
|
43
|
+
function shortcutLabel(): string {
|
|
44
|
+
return shortcuts.join(", ");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function updateStatus(ctx: ExtensionContext): void {
|
|
48
|
+
if (!ctx.hasUI) return;
|
|
49
|
+
if (armed && stashedDraft !== null) {
|
|
50
|
+
ctx.ui.setStatus(STATUS_KEY, `interlude armed: next sent message restores draft (${shortcutLabel()})`);
|
|
51
|
+
} else {
|
|
52
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function clearStash(ctx: ExtensionContext): void {
|
|
57
|
+
stashedDraft = null;
|
|
58
|
+
armed = false;
|
|
59
|
+
updateStatus(ctx);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function stashOrRestore(ctx: ExtensionContext): void {
|
|
63
|
+
if (!ctx.hasUI) return;
|
|
64
|
+
const currentText = ctx.ui.getEditorText();
|
|
65
|
+
|
|
66
|
+
if (armed && stashedDraft !== null) {
|
|
67
|
+
ctx.ui.setEditorText(stashedDraft);
|
|
68
|
+
clearStash(ctx);
|
|
69
|
+
ctx.ui.notify("Interlude draft restored", "info");
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!currentText) {
|
|
74
|
+
ctx.ui.notify("Editor is empty, nothing to stash", "warning");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
stashedDraft = currentText;
|
|
79
|
+
armed = true;
|
|
80
|
+
ctx.ui.setEditorText("");
|
|
81
|
+
updateStatus(ctx);
|
|
82
|
+
ctx.ui.notify("Draft stashed. Send one message and your previous draft will come back.", "info");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const shortcut of shortcuts) {
|
|
86
|
+
// Shortcut IDs come from runtime user config, so we can't satisfy pi's KeyId
|
|
87
|
+
// string-literal type without a cast here.
|
|
88
|
+
pi.registerShortcut(shortcut as any, {
|
|
89
|
+
description: "Stash the current draft, send one interlude message, then restore the draft",
|
|
90
|
+
handler: async (ctx) => {
|
|
91
|
+
stashOrRestore(ctx);
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
pi.on("input", async (event, ctx) => {
|
|
97
|
+
if (!armed || stashedDraft === null) return { action: "continue" };
|
|
98
|
+
if (event.source === "extension") return { action: "continue" };
|
|
99
|
+
if (!event.text.trim()) return { action: "continue" };
|
|
100
|
+
|
|
101
|
+
const draftToRestore = stashedDraft;
|
|
102
|
+
clearStash(ctx);
|
|
103
|
+
ctx.ui.setEditorText(draftToRestore);
|
|
104
|
+
return { action: "continue" };
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
pi.on("before_agent_start", async (_event, ctx) => {
|
|
108
|
+
if (!armed || stashedDraft === null) return;
|
|
109
|
+
const draftToRestore = stashedDraft;
|
|
110
|
+
clearStash(ctx);
|
|
111
|
+
ctx.ui.setEditorText(draftToRestore);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
115
|
+
updateStatus(ctx);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
pi.on("session_switch", async (_event, ctx) => {
|
|
119
|
+
clearStash(ctx);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
pi.on("session_fork", async (_event, ctx) => {
|
|
123
|
+
clearStash(ctx);
|
|
124
|
+
});
|
|
125
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mjakl/pi-interlude",
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "Pi extension for stashing the current draft, sending an interlude message, and restoring the draft.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "node --test",
|
|
9
|
+
"check": "npm test && npm pack --dry-run >/dev/null",
|
|
10
|
+
"prepublishOnly": "npm run check"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"index.ts",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"pi": {
|
|
18
|
+
"extensions": [
|
|
19
|
+
"./index.ts"
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"pi",
|
|
24
|
+
"extension",
|
|
25
|
+
"interlude",
|
|
26
|
+
"pi-package"
|
|
27
|
+
],
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/mjakl/pi-interlude.git"
|
|
31
|
+
},
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/mjakl/pi-interlude/issues"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/mjakl/pi-interlude#readme",
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^25.2.3",
|
|
42
|
+
"typescript": "^5.9.3"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@mariozechner/pi-agent-core": "*",
|
|
46
|
+
"@mariozechner/pi-ai": "*",
|
|
47
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
48
|
+
"@mariozechner/pi-tui": "*",
|
|
49
|
+
"@sinclair/typebox": "*"
|
|
50
|
+
},
|
|
51
|
+
"peerDependenciesMeta": {
|
|
52
|
+
"@mariozechner/pi-agent-core": {
|
|
53
|
+
"optional": true
|
|
54
|
+
},
|
|
55
|
+
"@mariozechner/pi-coding-agent": {
|
|
56
|
+
"optional": true
|
|
57
|
+
},
|
|
58
|
+
"@mariozechner/pi-tui": {
|
|
59
|
+
"optional": true
|
|
60
|
+
},
|
|
61
|
+
"@mariozechner/pi-ai": {
|
|
62
|
+
"optional": true
|
|
63
|
+
},
|
|
64
|
+
"@sinclair/typebox": {
|
|
65
|
+
"optional": true
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|