@rol1n/pi-kit 0.0.0-stage → 0.1.1

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 (120) hide show
  1. package/README.md +73 -2
  2. package/ask-u-questions/README.md +26 -0
  3. package/ask-u-questions/dialog-ui.ts +80 -0
  4. package/ask-u-questions/index.ts +92 -0
  5. package/ask-u-questions/multi-choice.ts +258 -0
  6. package/ask-u-questions/single-choice.ts +180 -0
  7. package/ask-u-questions/tool-renderers.ts +67 -0
  8. package/ask-u-questions/types.ts +192 -0
  9. package/continue-signal/README.md +20 -0
  10. package/continue-signal/index.ts +69 -0
  11. package/custom-providers/README.md +20 -0
  12. package/custom-providers/asr/handlers/dashscope.ts +154 -0
  13. package/custom-providers/asr/index.ts +37 -0
  14. package/custom-providers/asr/models.ts +18 -0
  15. package/custom-providers/asr/types.ts +20 -0
  16. package/custom-providers/chat/index.ts +33 -0
  17. package/custom-providers/chat/magpie.ts +258 -0
  18. package/custom-providers/chat/models.ts +111 -0
  19. package/custom-providers/classifier/index.ts +6 -0
  20. package/custom-providers/classifier/models.ts +1 -0
  21. package/custom-providers/image/handlers/dashscope.ts +240 -0
  22. package/custom-providers/image/handlers/openrouter.ts +185 -0
  23. package/custom-providers/image/index.ts +96 -0
  24. package/custom-providers/image/models.ts +71 -0
  25. package/custom-providers/image/types.ts +22 -0
  26. package/custom-providers/index.ts +64 -0
  27. package/custom-providers/types.ts +17 -0
  28. package/image-generate/README.md +25 -0
  29. package/image-generate/files.ts +49 -0
  30. package/image-generate/index.ts +220 -0
  31. package/image-generate/model-selector.ts +150 -0
  32. package/memory-log/README.md +54 -0
  33. package/memory-log/agent/consolidator/prompt.ts +47 -0
  34. package/memory-log/agent/consolidator/tools.ts +160 -0
  35. package/memory-log/agent/cost.ts +32 -0
  36. package/memory-log/agent/index.ts +42 -0
  37. package/memory-log/agent/observer/prompt.ts +73 -0
  38. package/memory-log/agent/observer/tool.ts +73 -0
  39. package/memory-log/commands/compact.ts +35 -0
  40. package/memory-log/commands/consolidate.ts +47 -0
  41. package/memory-log/commands/status.ts +50 -0
  42. package/memory-log/config.ts +253 -0
  43. package/memory-log/debug-log.ts +76 -0
  44. package/memory-log/hooks/compaction-hook.ts +161 -0
  45. package/memory-log/hooks/compaction-trigger.ts +160 -0
  46. package/memory-log/hooks/consolidator-trigger.ts +143 -0
  47. package/memory-log/hooks/observer-trigger.ts +176 -0
  48. package/memory-log/ids.ts +78 -0
  49. package/memory-log/index.ts +149 -0
  50. package/memory-log/ledger/fold.ts +77 -0
  51. package/memory-log/ledger/index.ts +7 -0
  52. package/memory-log/ledger/pool.ts +38 -0
  53. package/memory-log/ledger/progress.ts +181 -0
  54. package/memory-log/ledger/projection.ts +115 -0
  55. package/memory-log/ledger/render.ts +38 -0
  56. package/memory-log/ledger/serialize.ts +161 -0
  57. package/memory-log/ledger/types.ts +201 -0
  58. package/memory-log/memory/index-render.ts +45 -0
  59. package/memory-log/memory/paths.ts +120 -0
  60. package/memory-log/memory/session.ts +64 -0
  61. package/memory-log/runtime.ts +127 -0
  62. package/memory-log/spawn/launch.ts +133 -0
  63. package/memory-log/spawn/runs.ts +83 -0
  64. package/memory-log/tokens.ts +66 -0
  65. package/memory-log/ui/status-controller.ts +225 -0
  66. package/memory-log/ui/timeline.ts +103 -0
  67. package/model-vision/README.md +22 -0
  68. package/multi-harness/README.md +63 -0
  69. package/multi-harness/adapters.mjs +72 -0
  70. package/multi-harness/harness-done.mjs +14 -0
  71. package/multi-harness/index.ts +375 -0
  72. package/multi-harness/ipc.mjs +37 -0
  73. package/multi-harness/opencode-plugin.mjs +99 -0
  74. package/multi-harness/process-runner.mjs +115 -0
  75. package/multi-harness/turn-hook.mjs +29 -0
  76. package/multi-harness/worker-bridge.ts +52 -0
  77. package/oai-subquota/README.md +22 -0
  78. package/oai-subquota/index.ts +286 -0
  79. package/oai-subquota/snapshot.ts +293 -0
  80. package/oai-subquota/ui.ts +183 -0
  81. package/package.json +92 -4
  82. package/prompt-snipaste/README.md +27 -0
  83. package/prompt-snipaste/index.ts +143 -0
  84. package/prompt-snipaste/menu.ts +159 -0
  85. package/prompt-snipaste/prompts/ask-questions.md +8 -0
  86. package/prompt-snipaste/prompts/delegate-exploration.md +7 -0
  87. package/prompt-snipaste/prompts/diagnose-report.md +7 -0
  88. package/prompt-snipaste/prompts/frontend-tech-preset.md +18 -0
  89. package/prompt-snipaste/prompts/orchestrator-mode.md +7 -0
  90. package/prompt-snipaste/prompts/session-kickoff.md +7 -0
  91. package/prompt-snipaste/prompts/verify-not-assume.md +7 -0
  92. package/prompt-snipaste/renderers.ts +87 -0
  93. package/prompt-snipaste/snippets.ts +206 -0
  94. package/prompt-snipaste/types.ts +18 -0
  95. package/screenshot/README.md +14 -0
  96. package/srp-theme/README.md +18 -0
  97. package/srp-theme/footer.ts +193 -0
  98. package/srp-theme/index.ts +172 -0
  99. package/srp-theme/tps-meter.ts +441 -0
  100. package/subagent/README.md +35 -0
  101. package/subagent/bridge.ts +171 -0
  102. package/subagent/index.ts +590 -0
  103. package/subagent/mux.ts +621 -0
  104. package/subagent/session.ts +166 -0
  105. package/subagent/ui.ts +100 -0
  106. package/tui-asr/README.md +21 -0
  107. package/tui-asr/dictation.ts +272 -0
  108. package/tui-asr/index.ts +181 -0
  109. package/tui-asr/input.ts +87 -0
  110. package/tui-asr/model-selector.ts +163 -0
  111. package/tui-asr/status.ts +79 -0
  112. package/web-reach/README.md +19 -0
  113. package/web-reach/html.ts +83 -0
  114. package/web-reach/index.ts +82 -0
  115. package/web-reach/page.ts +214 -0
  116. package/web-reach/rsc.ts +161 -0
  117. package/web-reach/search.ts +85 -0
  118. package/web-reach/transport.ts +35 -0
  119. package/windows-wheel-fix/README.md +12 -0
  120. package/windows-wheel-fix/index.ts +44 -0
@@ -0,0 +1,183 @@
1
+ import type { ExtensionCommandContext, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
2
+ import { Text, visibleWidth } from "@earendil-works/pi-tui";
3
+ import type { CodexUsageSnapshot, LimitWindow } from "./snapshot.ts";
4
+
5
+ const USAGE_SETTINGS_URL = "https://chatgpt.com/codex/settings/usage";
6
+ const MONTH_NAMES = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
7
+
8
+ function pct(value: number | undefined): string {
9
+ if (value === undefined || !Number.isFinite(value)) return "?%";
10
+ return Number.isInteger(value) ? `${value}%` : `${value.toFixed(1)}%`;
11
+ }
12
+
13
+ function padLabel(label: string, width = 28): string {
14
+ return `${label}:`.padEnd(width, " ");
15
+ }
16
+
17
+ function formatTime(d: Date): string {
18
+ return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
19
+ }
20
+
21
+ function sameDay(a: Date, b: Date): boolean {
22
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
23
+ }
24
+
25
+ function formatReset(resetAt: number | undefined, now = new Date()): string {
26
+ if (!resetAt) return "";
27
+ const reset = new Date(resetAt * 1000);
28
+ if (!Number.isFinite(reset.getTime())) return "";
29
+ const time = formatTime(reset);
30
+ if (sameDay(reset, now)) return `resets ${time}`;
31
+ const date = `${reset.getDate()} ${MONTH_NAMES[reset.getMonth()]}`;
32
+ return reset.getFullYear() === now.getFullYear() ? `resets ${time} on ${date}` : `resets ${time} on ${date} ${reset.getFullYear()}`;
33
+ }
34
+
35
+ export function formatDuration(seconds: number | undefined): string {
36
+ if (seconds === undefined || !Number.isFinite(seconds) || seconds <= 0) return "";
37
+ const rounded = Math.round(seconds);
38
+ const days = Math.floor(rounded / 86_400);
39
+ const hours = Math.floor((rounded % 86_400) / 3600);
40
+ const mins = Math.floor((rounded % 3600) / 60);
41
+ if (days > 0) return `${days}d${hours ? `${hours}h` : ""}`;
42
+ if (hours > 0) return `${hours}h${mins ? `${mins}m` : ""}`;
43
+ return `${mins || 1}m`;
44
+ }
45
+
46
+ function resetSeconds(window: LimitWindow | undefined, now: Date): number | undefined {
47
+ if (!window) return undefined;
48
+ if (window.resetAfterSeconds !== undefined) return window.resetAfterSeconds;
49
+ return window.resetAt !== undefined ? Math.max(0, Math.round(window.resetAt - now.getTime() / 1000)) : undefined;
50
+ }
51
+
52
+ export function bar(leftPercent: number | undefined, width = 20): string {
53
+ const left = Math.max(0, Math.min(100, leftPercent ?? 0));
54
+ const filled = Math.max(0, Math.min(width, Math.round((left / 100) * width)));
55
+ return `[${"█".repeat(filled)}${"░".repeat(width - filled)}]`;
56
+ }
57
+
58
+ function formatWindowLine(label: string, window: LimitWindow | undefined, now: Date): string | undefined {
59
+ if (!window) return undefined;
60
+ const reset = formatReset(window.resetAt, now) || (formatDuration(window.resetAfterSeconds) ? `resets in ${formatDuration(window.resetAfterSeconds)}` : "");
61
+ const suffix = reset ? ` (${reset})` : "";
62
+ return `${padLabel(label)}${bar(window.leftPercent)} ${pct(window.leftPercent)} left${suffix}`;
63
+ }
64
+
65
+ function formatCreditsLine(snapshot: CodexUsageSnapshot): string | undefined {
66
+ const c = snapshot.credits;
67
+ if (!c) return undefined;
68
+ if (c.unlimited) return `${padLabel("Credits")}unlimited`;
69
+ if (c.balance !== undefined) {
70
+ const val = Number(c.balance);
71
+ if (!Number.isFinite(val) || val <= 0) return undefined;
72
+ return `${padLabel("Credits")}${Math.floor(val).toLocaleString()} credits`;
73
+ }
74
+ return c.hasCredits ? `${padLabel("Credits")}available` : undefined;
75
+ }
76
+
77
+ function formatResetsLine(snapshot: CodexUsageSnapshot): string | undefined {
78
+ const count = snapshot.resets?.availableCount;
79
+ return typeof count === "number" && count > 0 ? `${padLabel("Resets")}${count} available` : undefined;
80
+ }
81
+
82
+ export function formatStatusline(snapshot: CodexUsageSnapshot, now = new Date()): string {
83
+ const parts: string[] = [];
84
+ const primary = snapshot.defaultLimit?.primary;
85
+ const secondary = snapshot.defaultLimit?.secondary;
86
+
87
+ if (primary) {
88
+ parts.push(`5h:${pct(primary.leftPercent)}`);
89
+ }
90
+
91
+ if (secondary) {
92
+ parts.push(`7d:${pct(secondary.leftPercent)}`);
93
+ const r7d = formatDuration(resetSeconds(secondary, now));
94
+ if (r7d) parts.push(`↺ ${r7d}`);
95
+ }
96
+
97
+ return parts.length > 0 ? parts.join(" ") : "unavailable";
98
+ }
99
+
100
+ function makeBox(lines: string[], reqWidth?: number): string {
101
+ const contentWidth = Math.max(...lines.map((l) => visibleWidth(l)), 1);
102
+ const width = Math.max(contentWidth, reqWidth ?? 0);
103
+ const top = `╭${"─".repeat(width + 2)}╮`;
104
+ const bottom = `╰${"─".repeat(width + 2)}╯`;
105
+ const body = lines.map((l) => `│ ${l}${" ".repeat(Math.max(0, width - visibleWidth(l)))} │`);
106
+ return [top, ...body, bottom].join("\n");
107
+ }
108
+
109
+ export function formatBoxedStatus(snapshot: CodexUsageSnapshot, now = new Date()): string {
110
+ const lines: string[] = [
111
+ ">_ Codex usage",
112
+ "",
113
+ `Visit ${USAGE_SETTINGS_URL} for up-to-date`,
114
+ "information on rate limits and credits",
115
+ "",
116
+ ];
117
+
118
+ const acct: string[] = [];
119
+ if (snapshot.account.email) acct.push(snapshot.account.email);
120
+ if (snapshot.account.plan) acct.push(`(${snapshot.account.plan[0]?.toUpperCase()}${snapshot.account.plan.slice(1)})`);
121
+ if (acct.length > 0) lines.push(`${padLabel("Account")}${acct.join(" ")}`);
122
+ if (snapshot.fetchedAt) lines.push(`${padLabel("Updated")}${new Date(snapshot.fetchedAt).toLocaleString()}`);
123
+ lines.push("");
124
+
125
+ const p = formatWindowLine("5h limit", snapshot.defaultLimit?.primary, now);
126
+ const s = formatWindowLine("Weekly limit", snapshot.defaultLimit?.secondary, now);
127
+ if (p) lines.push(p);
128
+ if (s) lines.push(s);
129
+
130
+ const credits = formatCreditsLine(snapshot);
131
+ if (credits) lines.push(credits);
132
+
133
+ const resets = formatResetsLine(snapshot);
134
+ if (resets) lines.push(resets);
135
+
136
+ return makeBox(lines);
137
+ }
138
+
139
+ function colorForLeft(left: number): "success" | "warning" | "error" {
140
+ return left >= 60 ? "success" : left >= 25 ? "warning" : "error";
141
+ }
142
+
143
+ function colorizeStatusLine(line: string, theme: Theme): string {
144
+ let out = line;
145
+ const match = line.match(/(\[[█░]+\])\s+(\d+(?:\.\d+)?)% left/);
146
+ if (match?.[1] && match[2]) {
147
+ const barText = match[1];
148
+ const pctText = match[2];
149
+ const color = colorForLeft(Number(pctText) || 0);
150
+ out = out.replace(barText, theme.fg(color, barText));
151
+ out = out.replace(`${pctText}% left`, theme.fg(color, `${pctText}% left`));
152
+ }
153
+ out = out.replace(">_ Codex usage", theme.fg("accent", theme.bold(">_ Codex usage")));
154
+ out = out.replace(/(Account|Updated|5h limit|Weekly limit|Credits|Resets):/g, (lbl) => theme.fg("muted", lbl));
155
+ out = out.replace(/(Visit https:\/\/chatgpt\.com\/codex\/settings\/usage for up-to-date|information on rate limits and credits)/g, (txt) => theme.fg("dim", txt));
156
+
157
+ if (out.startsWith("╭") || out.startsWith("╰")) return theme.fg("dim", out);
158
+ if (out.startsWith("│") && out.endsWith("│")) {
159
+ return `${theme.fg("dim", out[0] ?? "")}${out.slice(1, -1)}${theme.fg("dim", out.at(-1) ?? "")}`;
160
+ }
161
+ return out;
162
+ }
163
+
164
+ export function renderStatusMessage(message: { content: string }, _opt: unknown, theme: Theme): Text {
165
+ return new Text(message.content.split("\n").map((l) => colorizeStatusLine(l, theme)).join("\n"), 0, 0);
166
+ }
167
+
168
+ export function dimText(ctx: ExtensionContext | ExtensionCommandContext, text: string): string {
169
+ try {
170
+ return ctx.ui?.theme?.fg ? ctx.ui.theme.fg("dim", text) : text;
171
+ } catch {
172
+ return text;
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Format quota text for Pi's shared extension-status row.
178
+ * It must stay compact because Pi combines all extension statuses before
179
+ * applying the footer width limit.
180
+ */
181
+ export function formatRightAlignedStatusline(snapshot: CodexUsageSnapshot, now = new Date()): string {
182
+ return formatStatusline(snapshot, now);
183
+ }
package/package.json CHANGED
@@ -1,6 +1,94 @@
1
1
  {
2
2
  "name": "@rol1n/pi-kit",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "A selectable collection of Pi extensions and prompt templates.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/RolinShmily/pi-kit.git"
8
+ },
9
+ "homepage": "https://github.com/RolinShmily/pi-kit#readme",
10
+ "bugs": {
11
+ "url": "https://github.com/RolinShmily/pi-kit/issues"
12
+ },
13
+ "type": "module",
14
+ "keywords": [
15
+ "pi-package"
16
+ ],
17
+ "engines": {
18
+ "node": ">=22.19.0"
19
+ },
20
+ "scripts": {
21
+ "manifest": "node scripts/package.mjs sync",
22
+ "check": "node scripts/package.mjs check",
23
+ "prepare:extension": "node scripts/package.mjs extension",
24
+ "publish:ci": "node scripts/package.mjs publish",
25
+ "prepack": "node scripts/package.mjs check"
26
+ },
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "files": [
31
+ "ask-u-questions/README.md",
32
+ "continue-signal/README.md",
33
+ "custom-providers/README.md",
34
+ "image-generate/README.md",
35
+ "memory-log/README.md",
36
+ "model-vision/README.md",
37
+ "multi-harness/README.md",
38
+ "oai-subquota/README.md",
39
+ "prompt-snipaste/README.md",
40
+ "screenshot/README.md",
41
+ "srp-theme/README.md",
42
+ "subagent/README.md",
43
+ "tui-asr/README.md",
44
+ "web-reach/README.md",
45
+ "windows-wheel-fix/README.md",
46
+ "ask-u-questions",
47
+ "continue-signal",
48
+ "custom-providers",
49
+ "image-generate",
50
+ "memory-log",
51
+ "multi-harness",
52
+ "subagent",
53
+ "oai-subquota",
54
+ "prompt-snipaste",
55
+ "srp-theme",
56
+ "tui-asr",
57
+ "web-reach",
58
+ "windows-wheel-fix",
59
+ "!**/test/**",
60
+ "!**/tests/**",
61
+ "!**/*.{test,spec}.{ts,js,mjs,cjs}",
62
+ "!**/.env",
63
+ "!**/.env.*"
64
+ ],
65
+ "peerDependencies": {
66
+ "@earendil-works/pi-ai": "*",
67
+ "@earendil-works/pi-agent-core": "*",
68
+ "@earendil-works/pi-coding-agent": "*",
69
+ "@earendil-works/pi-tui": "*",
70
+ "typebox": "*"
71
+ },
72
+ "pi": {
73
+ "extensions": [
74
+ "./ask-u-questions/index.ts",
75
+ "./continue-signal/index.ts",
76
+ "./custom-providers/index.ts",
77
+ "./image-generate/index.ts",
78
+ "./memory-log/index.ts",
79
+ "./multi-harness/index.ts",
80
+ "./oai-subquota/index.ts",
81
+ "./prompt-snipaste/index.ts",
82
+ "./srp-theme/index.ts",
83
+ "./subagent/index.ts",
84
+ "./tui-asr/index.ts",
85
+ "./web-reach/index.ts",
86
+ "./windows-wheel-fix/index.ts"
87
+ ],
88
+ "skills": [],
89
+ "prompts": [
90
+ "./prompt-snipaste/prompts"
91
+ ],
92
+ "themes": []
93
+ }
94
+ }
@@ -0,0 +1,27 @@
1
+ # prompt-snipaste
2
+
3
+ Select prompt snippets to prepend or append to the next user message, with live preview and a session transcript card. Bundles 7 built-in prompt snippets under `prompts/`.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ pi install npm:@rol1n/prompt-snipaste
9
+ ```
10
+
11
+ Also included in `@rol1n/pi-kit`.
12
+
13
+ ## Use
14
+
15
+ Open with `/snipaste` or `Alt+S`. Navigate with arrow keys, toggle with `Space`, preview with `Tab`, confirm with `Enter`, and cancel with `Escape`.
16
+
17
+ ## Frontmatter
18
+
19
+ ```markdown
20
+ ---
21
+ name: Review checklist
22
+ description: Add review criteria to the next message
23
+ placement: append
24
+ order: 100
25
+ ---
26
+ Check correctness, error handling, and missing validation.
27
+ ```
@@ -0,0 +1,143 @@
1
+ /** Select prompt snippets for the next user message with /snipaste or Alt+S. */
2
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+ import { Key } from "@earendil-works/pi-tui";
4
+ import type { PromptCardData, Snippet } from "./types.ts";
5
+ import { loadSnippets } from "./snippets.ts";
6
+ import { showSnippetMenu } from "./menu.ts";
7
+ import { buildPromptCardComponent, updateSnippetWidget } from "./renderers.ts";
8
+
9
+ export default function promptSnipaste(pi: ExtensionAPI) {
10
+ let enabled = new Set<string>();
11
+ let snippets: Snippet[] = [];
12
+
13
+ function updateWidget(ctx: ExtensionContext) {
14
+ updateSnippetWidget(ctx, () => ({ snippets, enabled }));
15
+ }
16
+
17
+ async function openMenu(ctx: ExtensionContext) {
18
+ if (ctx.mode !== "tui") {
19
+ ctx.ui.notify("Prompt snippets menu is only available in interactive terminals", "warning");
20
+ return;
21
+ }
22
+
23
+ snippets = loadSnippets(ctx.cwd, pi);
24
+ enabled = new Set([...enabled].filter((id) => snippets.some((s) => s.id === id)));
25
+
26
+ if (snippets.length === 0) {
27
+ ctx.ui.notify("No Markdown files found in prompt directories", "warning");
28
+ updateWidget(ctx);
29
+ return;
30
+ }
31
+
32
+ const working = new Set(enabled);
33
+
34
+ const confirmed = await showSnippetMenu(ctx, snippets, working);
35
+
36
+ if (confirmed) {
37
+ enabled = working;
38
+ }
39
+ updateWidget(ctx);
40
+ }
41
+
42
+ let pendingTurnSnippets: Snippet[] | null = null;
43
+
44
+ pi.registerEntryRenderer<PromptCardData>("prompt-snipaste-card", (entry, _options, theme) => {
45
+ const data = entry.data;
46
+ if (!data?.snippets || data.snippets.length === 0) return undefined;
47
+ return buildPromptCardComponent(data.snippets, theme);
48
+ });
49
+
50
+ pi.on("session_start", (_event, ctx) => {
51
+ enabled = new Set();
52
+ pendingTurnSnippets = null;
53
+ snippets = loadSnippets(ctx.cwd, pi);
54
+ updateWidget(ctx);
55
+ });
56
+
57
+ pi.on("input", async (_event, ctx) => {
58
+ if (enabled.size === 0) return undefined;
59
+
60
+ snippets = loadSnippets(ctx.cwd, pi);
61
+ const active = snippets.filter((s) => enabled.has(s.id));
62
+ enabled = new Set();
63
+ updateWidget(ctx);
64
+
65
+ if (active.length === 0) return undefined;
66
+
67
+ pendingTurnSnippets = active;
68
+
69
+ pi.appendEntry<PromptCardData>("prompt-snipaste-card", {
70
+ snippets: active.map((s) => ({
71
+ id: s.id,
72
+ name: s.name,
73
+ placement: s.placement,
74
+ order: s.order,
75
+ body: s.body,
76
+ })),
77
+ });
78
+
79
+ return undefined;
80
+ });
81
+
82
+ pi.on("context", async (event, _ctx) => {
83
+ if (!pendingTurnSnippets || pendingTurnSnippets.length === 0) return undefined;
84
+
85
+ const active = pendingTurnSnippets;
86
+ pendingTurnSnippets = null;
87
+
88
+ const prepends = active.filter((s) => s.placement === "prepend");
89
+ const appends = active.filter((s) => s.placement === "append");
90
+
91
+ const messages = [...event.messages];
92
+ for (let i = messages.length - 1; i >= 0; i--) {
93
+ const msg = messages[i];
94
+ if (msg.role === "user") {
95
+ if (typeof msg.content === "string") {
96
+ const parts: string[] = [];
97
+
98
+ if (prepends.length > 0) {
99
+ parts.push(prepends.map((s) => s.body.trim()).join("\n\n"));
100
+ }
101
+ if (msg.content.trim()) {
102
+ parts.push(msg.content.trim());
103
+ }
104
+ if (appends.length > 0) {
105
+ parts.push(appends.map((s) => s.body.trim()).join("\n\n"));
106
+ }
107
+
108
+ messages[i] = {
109
+ ...msg,
110
+ content: parts.join("\n\n"),
111
+ };
112
+ } else if (Array.isArray(msg.content)) {
113
+ const prependText = prepends.map((s) => s.body.trim()).join("\n\n");
114
+ const appendText = appends.map((s) => s.body.trim()).join("\n\n");
115
+ const newContent = [...msg.content];
116
+ if (prependText) {
117
+ newContent.unshift({ type: "text", text: prependText });
118
+ }
119
+ if (appendText) {
120
+ newContent.push({ type: "text", text: appendText });
121
+ }
122
+ messages[i] = {
123
+ ...msg,
124
+ content: newContent as any,
125
+ };
126
+ }
127
+ return { messages };
128
+ }
129
+ }
130
+
131
+ return undefined;
132
+ });
133
+
134
+ pi.registerCommand("snipaste", {
135
+ description: "Select prepended or appended prompt snippets to inject into the next message",
136
+ handler: (_args, ctx) => openMenu(ctx),
137
+ });
138
+
139
+ pi.registerShortcut(Key.alt("s"), {
140
+ description: "Open prompt snippets menu",
141
+ handler: (ctx) => openMenu(ctx),
142
+ });
143
+ }
@@ -0,0 +1,159 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { Key, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
3
+ import type { Snippet } from "./types.ts";
4
+
5
+ /** Edit a working selection; the caller commits it only after confirmation. */
6
+ export function showSnippetMenu(ctx: ExtensionContext, snippets: Snippet[], working: Set<string>): Promise<boolean> {
7
+ return ctx.ui.custom<boolean>((tui, theme, _keybindings, done) => {
8
+ const prepends = snippets.filter((s) => s.placement === "prepend");
9
+ const appends = snippets.filter((s) => s.placement === "append");
10
+ const items = [...prepends, ...appends];
11
+
12
+ let mode: "list" | "preview" = "list";
13
+ let cursor = 0;
14
+ let listScroll = 0;
15
+ let previewScroll = 0;
16
+
17
+ const itemRow = (snippet: Snippet, idx: number, width: number): string => {
18
+ const pointer = idx === cursor ? theme.fg("accent", "> ") : " ";
19
+ const checkbox = working.has(snippet.id) ? theme.fg("success", "[x]") : theme.fg("dim", "[ ]");
20
+ const desc = snippet.description ? theme.fg("dim", ` — ${snippet.description}`) : "";
21
+ return truncateToWidth(`${pointer}${checkbox} ${theme.bold(snippet.name)}${desc}`, width);
22
+ };
23
+
24
+ const buildListRows = (width: number): { text: string; itemIndex: number | null }[] => {
25
+ const rows: { text: string; itemIndex: number | null }[] = [];
26
+ rows.push({ text: theme.fg("dim", "↑ Prepend Prompts"), itemIndex: null });
27
+ prepends.forEach((s, i) => rows.push({ text: itemRow(s, i, width), itemIndex: i }));
28
+ rows.push({ text: "", itemIndex: null });
29
+ rows.push({ text: theme.fg("dim", "↓ Append Prompts"), itemIndex: null });
30
+ appends.forEach((s, i) => rows.push({ text: itemRow(s, prepends.length + i, width), itemIndex: prepends.length + i }));
31
+ return rows;
32
+ };
33
+
34
+ const buildPreviewRows = (snippet: Snippet, width: number): string[] => {
35
+ const rows: string[] = [];
36
+ rows.push(truncateToWidth(theme.bold(snippet.name), width));
37
+ rows.push(truncateToWidth(theme.fg("dim", `${snippet.placement === "prepend" ? "Prepend" : "Append"} · Order ${snippet.order} · ${snippet.id}`), width));
38
+ rows.push(theme.fg("dim", "─".repeat(Math.min(width, 40))));
39
+ for (const line of snippet.body.split("\n")) {
40
+ for (const wrapped of wrapTextWithAnsi(line, width)) {
41
+ rows.push(truncateToWidth(wrapped, width));
42
+ }
43
+ }
44
+ return rows;
45
+ };
46
+
47
+ const viewport = (
48
+ lines: string[],
49
+ scroll: number,
50
+ maxView: number,
51
+ focusRow?: number,
52
+ ): { out: string[]; scroll: number } => {
53
+ const clipped = lines.length > maxView;
54
+ const view = clipped ? Math.max(1, maxView - 2) : maxView;
55
+
56
+ let s = Math.min(Math.max(0, scroll), Math.max(0, lines.length - view));
57
+ if (focusRow !== undefined) {
58
+ if (focusRow < s) s = focusRow;
59
+ else if (focusRow >= s + view) s = focusRow - view + 1;
60
+ }
61
+
62
+ const visible = lines.slice(s, s + view);
63
+ if (!clipped) return { out: visible, scroll: s };
64
+
65
+ const above = s;
66
+ const below = lines.length - (s + view);
67
+ return {
68
+ out: [
69
+ above > 0 ? theme.fg("dim", ` ↑ ${above} more`) : "",
70
+ ...visible,
71
+ below > 0 ? theme.fg("dim", ` ↓ ${below} more`) : "",
72
+ ],
73
+ scroll: s,
74
+ };
75
+ };
76
+
77
+ return {
78
+ render(width: number): string[] {
79
+ const maxView = Math.max(5, tui.terminal.rows - 10);
80
+
81
+ let content: string[];
82
+ let title: string;
83
+ let hints: string;
84
+ if (mode === "list") {
85
+ const rows = buildListRows(width);
86
+ const cursorRow = rows.findIndex((r) => r.itemIndex === cursor);
87
+ const v = viewport(rows.map((r) => r.text), listScroll, maxView, cursorRow);
88
+ content = v.out;
89
+ listScroll = v.scroll;
90
+ title = "Prompt Snippets";
91
+ hints = "↑↓ Navigate • Space Select • Tab Preview • Enter Confirm • Esc Cancel";
92
+ } else {
93
+ const snippet = items[cursor];
94
+ const rows = buildPreviewRows(snippet, width);
95
+ const v = viewport(rows, previewScroll, maxView);
96
+ content = v.out;
97
+ previewScroll = v.scroll;
98
+ title = `Preview: ${snippet.name}`;
99
+ hints = "↑↓ Scroll • Tab/Esc Back to list";
100
+ }
101
+
102
+ const innerW = width - 4;
103
+ const titleVisW = visibleWidth(title);
104
+ const hintsVisW = visibleWidth(hints);
105
+ const titlePad = Math.max(0, innerW - titleVisW);
106
+ const hintsPad = Math.max(0, innerW - hintsVisW);
107
+
108
+ const rawLines = [
109
+ theme.fg("border", `╭─ ${theme.bold(title)} ${"─".repeat(titlePad)}╮`),
110
+ ...content.map((l) => {
111
+ const pad = Math.max(0, innerW - visibleWidth(l));
112
+ return theme.fg("border", "│ ") + l + " ".repeat(pad) + theme.fg("border", " │");
113
+ }),
114
+ theme.fg("border", `├${"─".repeat(innerW + 2)}┤`),
115
+ theme.fg("border", "│ ") + theme.fg("dim", hints) + " ".repeat(hintsPad) + theme.fg("border", " │"),
116
+ theme.fg("border", `╰${"─".repeat(innerW + 2)}╯`),
117
+ ];
118
+
119
+ return rawLines.map((line) => truncateToWidth(line, width));
120
+ },
121
+ invalidate() {},
122
+ handleInput(data: string) {
123
+ if (mode === "list") {
124
+ if (matchesKey(data, Key.up)) {
125
+ cursor = (cursor - 1 + items.length) % items.length;
126
+ tui.requestRender();
127
+ } else if (matchesKey(data, Key.down)) {
128
+ cursor = (cursor + 1) % items.length;
129
+ tui.requestRender();
130
+ } else if (matchesKey(data, Key.space)) {
131
+ const id = items[cursor].id;
132
+ if (working.has(id)) working.delete(id);
133
+ else working.add(id);
134
+ tui.requestRender();
135
+ } else if (matchesKey(data, Key.tab)) {
136
+ mode = "preview";
137
+ previewScroll = 0;
138
+ tui.requestRender();
139
+ } else if (matchesKey(data, Key.enter)) {
140
+ done(true);
141
+ } else if (matchesKey(data, Key.escape)) {
142
+ done(false);
143
+ }
144
+ } else {
145
+ if (matchesKey(data, Key.up)) {
146
+ previewScroll--;
147
+ tui.requestRender();
148
+ } else if (matchesKey(data, Key.down)) {
149
+ previewScroll++;
150
+ tui.requestRender();
151
+ } else if (matchesKey(data, Key.tab) || matchesKey(data, Key.escape)) {
152
+ mode = "list";
153
+ tui.requestRender();
154
+ }
155
+ }
156
+ },
157
+ };
158
+ });
159
+ }
@@ -0,0 +1,8 @@
1
+ ---
2
+ name: Ask questions
3
+ description: Ask questions to reach a shared understanding.
4
+ placement: append
5
+ order: 10
6
+ ---
7
+ Use the `ask_user_question` tool to ask questions until you are 100% sure you know exactly what to do.
8
+ Do not act on this until I confirm we have reached a shared understanding.
@@ -0,0 +1,7 @@
1
+ ---
2
+ name: Delegate exploration
3
+ description: Keep context lean. Subagents explore, you verify critical parts
4
+ placement: append
5
+ order: 30
6
+ ---
7
+ Keep your context window lean. Delegate codebase exploration to subagents — ask them specific, targeted questions instead of reading many files yourself. Read files directly only to verify critical parts.
@@ -0,0 +1,7 @@
1
+ ---
2
+ name: Diagnose, don't fix
3
+ description: Read-only, report findings and a proposed fix
4
+ placement: append
5
+ order: 40
6
+ ---
7
+ Investigate and diagnose the issue. Do not change any code. Report back with your findings and a proposed fix. Do not apply the fix yet.
@@ -0,0 +1,18 @@
1
+ ---
2
+ name: Frontend tech preset
3
+ description: Reuse a unified React/Next.js frontend stack
4
+ placement: prepend
5
+ order: 15
6
+ ---
7
+ For new frontend work, prefer this [React](https://react.dev/) stack; respect existing project constraints. Reuse mature components, keep one design language, avoid overlapping libraries, and add dependencies only when needed and approved.
8
+
9
+ - Full-stack & routing: [Next.js App Router](https://nextjs.org/docs/app); no extra router.
10
+ - UI: [shadcn/ui](https://ui.shadcn.com/create), Nova style; own and customize the component source.
11
+ - Custom styling: [Tailwind CSS](https://tailwindcss.com/docs).
12
+ - Fonts: [Google Fonts](https://fonts.google.com/), loaded via `next/font/google`.
13
+ - Icons: [Lucide React](https://lucide.dev/guide/packages/lucide-react).
14
+ - Animation: [Motion for React](https://motion.dev/docs/react).
15
+ - Resizable layouts: [react-resizable-panels](https://github.com/bvaughn/react-resizable-panels).
16
+ - Drag & drop: [dnd-kit](https://dndkit.com/), React integration.
17
+ - Client state: [Zustand](https://zustand.docs.pmnd.rs/).
18
+ - 3D/WebGPU: [Three.js](https://threejs.org/), `WebGPURenderer`; optional React integration: [React Three Fiber](https://r3f.docs.pmnd.rs/).
@@ -0,0 +1,7 @@
1
+ ---
2
+ name: Orchestrator mode
3
+ description: Pure high-level orchestrator session
4
+ placement: prepend
5
+ order: 30
6
+ ---
7
+ This is a pure high-level orchestrator session. Outsource mechanical work — file exploration, code reading, implementation — to subagents. Keep your own context window lean so your reasoning stays sharp; do not bloat it by reading code yourself.
@@ -0,0 +1,7 @@
1
+ ---
2
+ name: Session kickoff
3
+ description: Get oriented, report back before starting work
4
+ placement: prepend
5
+ order: 10
6
+ ---
7
+ Familiarize yourself with this project before we start. Once you have a clear picture, report back. Do not begin any work until we have aligned on what's next.
@@ -0,0 +1,7 @@
1
+ ---
2
+ name: Verify, don't assume
3
+ description: Do not assume — verify
4
+ placement: append
5
+ order: 20
6
+ ---
7
+ Do not assume — verify. Verify critical facts rather than guessing. And ask me, if you cannot verify something. Only begin once you are 100% sure of what to do. If you catch yourself being even slightly unsure, that's a sign to check first.