@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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +71 -0
  3. package/index.ts +125 -0
  4. 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
+ }