@andares/pi-toolkits 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.
@@ -0,0 +1,101 @@
1
+ /**
2
+ * stash feature — PromptStash state machine tests.
3
+ *
4
+ * All branches are deterministic via injected `now`:
5
+ * - empty-cache first press = stash + clear
6
+ * - single press = swap; spaced second press = toggle back
7
+ * - double press (within 400ms) = stash current + clear editor, old stash
8
+ * discarded
9
+ * - two presses beyond the window = normal swap (not clear)
10
+ * - triple press: the third press is a fresh swap
11
+ */
12
+ import { describe, expect, it } from "vitest";
13
+ import { PromptStash, STASH_DOUBLE_PRESS_MS } from "./stash.js";
14
+
15
+ describe("PromptStash", () => {
16
+ it("first press with empty cache stashes the text and clears the editor", () => {
17
+ const stash = new PromptStash();
18
+ const result = stash.press("write tests first", 1_000);
19
+ expect(result).toEqual({
20
+ text: "",
21
+ kind: "swap",
22
+ stashed: "write tests first",
23
+ });
24
+ expect(stash.peek()).toBe("write tests first");
25
+ });
26
+
27
+ it("swaps editor text with the stash on a normal press", () => {
28
+ const stash = new PromptStash();
29
+ stash.press("first prompt", 1_000); // stash = "first prompt"
30
+ // User types a new prompt, then presses the hotkey.
31
+ const result = stash.press("second prompt", 2_000);
32
+ expect(result.text).toBe("first prompt"); // old stash fills the editor
33
+ expect(result.stashed).toBe("second prompt"); // current text went to cache
34
+ expect(stash.peek()).toBe("second prompt");
35
+ });
36
+
37
+ it("toggles back and forth between the two prompts on spaced presses", () => {
38
+ const stash = new PromptStash();
39
+ stash.press("A", 1_000);
40
+ stash.press("B", 2_000); // editor gets A, cache = B
41
+ // Press again (outside the double-press window): swap back.
42
+ const back = stash.press("A", 3_000); // editor currently shows A
43
+ expect(back.text).toBe("B");
44
+ expect(back.stashed).toBe("A");
45
+ // And again: swap forward.
46
+ const forth = stash.press("B", 4_000);
47
+ expect(forth.text).toBe("A");
48
+ expect(forth.stashed).toBe("B");
49
+ });
50
+
51
+ it("double press within the window stashes current text and clears the editor", () => {
52
+ const stash = new PromptStash();
53
+ stash.press("old stash", 1_000); // cache = "old stash"
54
+ // User types "current prompt", presses once (swap), then quickly again.
55
+ const first = stash.press("current prompt", 2_000);
56
+ expect(first.text).toBe("old stash"); // editor momentarily shows old stash
57
+ const second = stash.press(
58
+ "old stash",
59
+ 2_000 + STASH_DOUBLE_PRESS_MS - 1, // within the window
60
+ );
61
+ expect(second.kind).toBe("stash-clear");
62
+ expect(second.text).toBe(""); // editor cleared
63
+ expect(second.stashed).toBe("current prompt"); // cache kept the current prompt
64
+ expect(stash.peek()).toBe("current prompt");
65
+ });
66
+
67
+ it("exactly at the window boundary counts as a double press", () => {
68
+ const stash = new PromptStash();
69
+ stash.press("A", 1_000);
70
+ stash.press("B", 2_000);
71
+ const boundary = stash.press("A", 2_000 + STASH_DOUBLE_PRESS_MS);
72
+ expect(boundary.kind).toBe("stash-clear");
73
+ });
74
+
75
+ it("two presses beyond the window behave as a normal swap, not clear", () => {
76
+ const stash = new PromptStash();
77
+ stash.press("A", 1_000);
78
+ stash.press("B", 2_000);
79
+ const late = stash.press("A", 2_000 + STASH_DOUBLE_PRESS_MS + 1);
80
+ expect(late.kind).toBe("swap");
81
+ expect(late.text).toBe("B"); // toggled back to the previous stash
82
+ });
83
+
84
+ it("triple press: the third press is a fresh swap (window consumed)", () => {
85
+ const stash = new PromptStash();
86
+ stash.press("A", 1_000);
87
+ stash.press("B", 2_000);
88
+ stash.press("A", 2_050); // double press → stash-clear, window consumed
89
+ const third = stash.press("", 2_100); // editor is empty now
90
+ expect(third.kind).toBe("swap");
91
+ expect(third.text).toBe("B"); // the previously stashed prompt comes back
92
+ expect(third.stashed).toBe("");
93
+ });
94
+
95
+ it("peek exposes the stash without changing state", () => {
96
+ const stash = new PromptStash();
97
+ expect(stash.peek()).toBe("");
98
+ stash.press("hello", 1_000);
99
+ expect(stash.peek()).toBe("hello");
100
+ });
101
+ });
@@ -0,0 +1,83 @@
1
+ /**
2
+ * prompt stash — pure state machine.
3
+ *
4
+ * One cache slot for one prompt. The hotkey (ctrl+alt+y — chosen for
5
+ * compatibility across pi, Linux terminals, Windows Terminal and Windows OS,
6
+ * see README) swaps the editor text with the stashed prompt:
7
+ *
8
+ * press → stash current text, fill editor with old stash
9
+ * press again (later) → toggles back (swap again)
10
+ * press twice quickly → stash current text AND clear the editor
11
+ * (the just-swapped-out prompt is discarded)
12
+ *
13
+ * With an empty stash, a single press already stashes the current text and
14
+ * clears the editor (there is nothing to swap in).
15
+ *
16
+ * The double-press window is short (400ms) so two deliberate presses toggle
17
+ * normally, while a quick double-tap performs the stash-and-clear action.
18
+ */
19
+ import type { KeyId } from "@earendil-works/pi-tui";
20
+
21
+ /**
22
+ * Stash hotkey.
23
+ *
24
+ * Compatibility rationale (verified against pi 0.84 keybindings, live
25
+ * matchesKey probes and terminal/OS behavior):
26
+ * - pi: no default binding for ctrl+alt+<letter> (only ctrl+alt+] is taken);
27
+ * extension shortcuts dispatch first while the editor is focused
28
+ * - terminals: alt encodes as ESC-prefix, so ctrl+alt+y arrives as "\x1b\x19"
29
+ * and is distinguishable from ctrl+y WITHOUT kitty keyboard protocol
30
+ * (verified via matchesKey). ctrl+shift+<letter> was rejected because on
31
+ * terminals without kitty protocol it collapses to ctrl+<letter> — for m
32
+ * that is Enter, which would submit the prompt. Plain ctrl+<letter> is
33
+ * fully occupied by pi defaults or terminal control chars (ctrl+q is the
34
+ * only free one but carries XON flow-control baggage).
35
+ * - Windows Terminal: no default ctrl+alt+letter binding
36
+ * - Windows OS: only ctrl+alt+del is reserved at the system level
37
+ * - Linux desktops: ctrl+alt+t/l/d/arrows and F-keys (TTY) are taken; y is free
38
+ */
39
+ export const STASH_KEY: KeyId = "ctrl+alt+y";
40
+
41
+ /** Two presses within this window count as the "stash and clear" action. */
42
+ export const STASH_DOUBLE_PRESS_MS = 400;
43
+
44
+ export interface StashPressResult {
45
+ /** Text to place in the editor. */
46
+ text: string;
47
+ /** What this press did. */
48
+ kind: "swap" | "stash-clear";
49
+ /** Stash content after this press (for status/logging). */
50
+ stashed: string;
51
+ }
52
+
53
+ export class PromptStash {
54
+ private cache = "";
55
+ private lastPressAt = 0;
56
+
57
+ constructor(private readonly doublePressMs = STASH_DOUBLE_PRESS_MS) {}
58
+
59
+ /**
60
+ * Handle one hotkey press with the given current editor text.
61
+ * `now` is injectable for deterministic tests.
62
+ */
63
+ press(current: string, now = Date.now()): StashPressResult {
64
+ if (
65
+ this.lastPressAt !== 0 &&
66
+ now - this.lastPressAt <= this.doublePressMs
67
+ ) {
68
+ // Double press: keep the just-stashed prompt in the cache and clear
69
+ // the editor (discard the prompt that was momentarily swapped in).
70
+ this.lastPressAt = 0;
71
+ return { text: "", kind: "stash-clear", stashed: this.cache };
72
+ }
73
+ this.lastPressAt = now;
74
+ const previous = this.cache;
75
+ this.cache = current;
76
+ return { text: previous, kind: "swap", stashed: current };
77
+ }
78
+
79
+ /** Current stash content without touching state. */
80
+ peek(): string {
81
+ return this.cache;
82
+ }
83
+ }
package/src/index.ts CHANGED
@@ -6,11 +6,20 @@
6
6
  *
7
7
  * Current features:
8
8
  * - ask — read-only Q&A mode (/ask)
9
+ * - favorites — model favorites (ctrl+F/ctrl+J in /model, favorites-only cycling)
10
+ * - stash — prompt stash (ctrl+alt+y swap / double-press stash-and-clear)
11
+ * - review — third-party code review with a configured review model (third-review)
9
12
  */
10
13
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
11
14
  import { registerAsk } from "./features/ask/index.js";
15
+ import { registerFavorites } from "./features/favorites/index.js";
16
+ import { registerReview } from "./features/review/index.js";
17
+ import { registerStash } from "./features/stash/index.js";
12
18
 
13
19
  export default function piToolkits(pi: ExtensionAPI): void {
14
20
  registerAsk(pi);
21
+ registerFavorites(pi);
22
+ registerReview(pi);
23
+ registerStash(pi);
15
24
  // registerXxx(pi); // future features
16
25
  }