pi-next-cue 1.0.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/.github/workflows/publish.yml +23 -0
- package/LICENSE +21 -0
- package/README.md +81 -0
- package/index.ts +354 -0
- package/package.json +22 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: Publish to npm
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
id-token: write
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- uses: actions/setup-node@v4
|
|
17
|
+
with:
|
|
18
|
+
node-version: 20
|
|
19
|
+
registry-url: https://registry.npmjs.org
|
|
20
|
+
|
|
21
|
+
- run: npm publish --provenance --access public
|
|
22
|
+
env:
|
|
23
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 ouzhenkun
|
|
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,81 @@
|
|
|
1
|
+
# pi-next-cue
|
|
2
|
+
|
|
3
|
+
**Predicts your next prompt after each agent turn — Tab to fill, Enter to send.**
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/pi-next-cue)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
## Why
|
|
9
|
+
|
|
10
|
+
After the agent finishes, you usually know what to type next. Pi-next-cue predicts it and shows a hint above your editor — saving keystrokes and keeping you in flow.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pi install npm:pi-next-cue
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Requires Pi v0.37.0+.
|
|
19
|
+
|
|
20
|
+
## How It Works
|
|
21
|
+
|
|
22
|
+
The hint widget shows **one** of two states, depending on context:
|
|
23
|
+
|
|
24
|
+
**Predicted prompt** (after agent responds):
|
|
25
|
+
```
|
|
26
|
+
→ run the tests
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Your last message** (for reference):
|
|
30
|
+
```
|
|
31
|
+
↩ fix the login redirect bug
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
- **→** Next prompt you're likely to send — generated by a lightweight LLM after each agent turn
|
|
35
|
+
- **↩** The last message you sent — so you never lose context of what you asked
|
|
36
|
+
|
|
37
|
+
When the editor is empty:
|
|
38
|
+
- **Tab** fills the hint into the editor for editing
|
|
39
|
+
- **Enter** sends the hint directly
|
|
40
|
+
|
|
41
|
+
When you start typing, the hint stays visible but won't interfere — Tab and Enter only trigger when the editor is empty.
|
|
42
|
+
|
|
43
|
+
## Features
|
|
44
|
+
|
|
45
|
+
- **Adaptive tone** — matches your language and communication style from recent messages
|
|
46
|
+
- **Correction learning** — remembers when you ignore suggestions, avoids repeating rejected directions
|
|
47
|
+
- **Tool-aware** — considers whether the last tool succeeded or failed when predicting
|
|
48
|
+
- **Lightweight** — single LLM call with minimal context, no background indexing
|
|
49
|
+
|
|
50
|
+
## Configuration
|
|
51
|
+
|
|
52
|
+
Pi-next-cue uses your session's current model by default. To use a specific fast/cheap model or customize keybindings, create `~/.pi/agent/pi-next-cue.json`:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"provider": "deepseek",
|
|
57
|
+
"model": "deepseek-chat",
|
|
58
|
+
"keys": {
|
|
59
|
+
"fill": "tab",
|
|
60
|
+
"send": "enter"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
All fields are optional. Defaults: `tab` to fill, `enter` to send. Key names follow Pi's [keybinding format](https://pi.dev/docs/latest/keybindings).
|
|
66
|
+
|
|
67
|
+
## Events
|
|
68
|
+
|
|
69
|
+
Other extensions can control hint visibility:
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
// Hide the hint (e.g. when showing a dialog)
|
|
73
|
+
pi.events.emit("pi-next-cue:pause");
|
|
74
|
+
|
|
75
|
+
// Restore the hint
|
|
76
|
+
pi.events.emit("pi-next-cue:resume");
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## License
|
|
80
|
+
|
|
81
|
+
MIT
|
package/index.ts
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-next-cue
|
|
3
|
+
*
|
|
4
|
+
* Predicts your next prompt after each agent turn. Shows a hint widget above
|
|
5
|
+
* the editor with two states:
|
|
6
|
+
* ↩ your last input (recall)
|
|
7
|
+
* → predicted next prompt (cue)
|
|
8
|
+
*
|
|
9
|
+
* Tab fills the hint into the editor; Enter sends it directly.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import {
|
|
15
|
+
CustomEditor,
|
|
16
|
+
getAgentDir,
|
|
17
|
+
type ExtensionAPI,
|
|
18
|
+
} from "@earendil-works/pi-coding-agent";
|
|
19
|
+
import { complete, type UserMessage } from "@earendil-works/pi-ai";
|
|
20
|
+
import { matchesKey } from "@earendil-works/pi-tui";
|
|
21
|
+
|
|
22
|
+
const SYSTEM_PROMPT = `Predict the user's most likely next reply as one short message.
|
|
23
|
+
|
|
24
|
+
What to suggest:
|
|
25
|
+
- Task completed -> the next logical workflow step
|
|
26
|
+
- Confirmation requested -> likely yes/no/choice
|
|
27
|
+
- Options presented -> likely pick, if context supports one
|
|
28
|
+
- Open-ended question -> a concrete answer or direction, only if inferable
|
|
29
|
+
- Tool failed -> a specific retry/fix based on the failure
|
|
30
|
+
- Tool succeeded -> the next useful step
|
|
31
|
+
- Agent proposed a clear next action -> a short affirmation is often enough
|
|
32
|
+
- If context is too thin to predict a useful reply, return [skip]
|
|
33
|
+
|
|
34
|
+
Tone:
|
|
35
|
+
- Match the language and register of recent user messages
|
|
36
|
+
- Casual when the moment is casual, direct when there is momentum
|
|
37
|
+
- Do not force excitement, praise, or drama
|
|
38
|
+
|
|
39
|
+
Rules:
|
|
40
|
+
- Return ONE message, under 60 chars, nothing else
|
|
41
|
+
- If no useful suggestion exists, return exactly: [skip]
|
|
42
|
+
- The suggestion must advance or unblock the workflow
|
|
43
|
+
- A short confirmation counts as advancing when it lets the agent proceed
|
|
44
|
+
- Be specific to this conversation, not generic
|
|
45
|
+
- Never repeat or rephrase the assistant's last message
|
|
46
|
+
- Never suggest an action the assistant already completed
|
|
47
|
+
- If the assistant only offered a pending action, a brief confirmation is allowed
|
|
48
|
+
- Learn from user corrections in the conversation; avoid rejected directions
|
|
49
|
+
- If a slash command is the obvious next action, suggest only the command
|
|
50
|
+
- Never output a generic question like "what's next" or "what should I do" — return [skip] instead`;
|
|
51
|
+
|
|
52
|
+
const SKIP_TOKEN = "[skip]";
|
|
53
|
+
const MAX_CORRECTIONS = 5;
|
|
54
|
+
|
|
55
|
+
type HintType = "recall" | "cue";
|
|
56
|
+
|
|
57
|
+
export default function (pi: ExtensionAPI) {
|
|
58
|
+
let currentHint: string | null = null;
|
|
59
|
+
let hintType: HintType | null = null;
|
|
60
|
+
let widgetCtx: any = null;
|
|
61
|
+
let suggestionAbort: AbortController | null = null;
|
|
62
|
+
|
|
63
|
+
// Correction tracking
|
|
64
|
+
const corrections: Array<{ suggested: string; actual: string }> = [];
|
|
65
|
+
let lastSuggestion: string | null = null;
|
|
66
|
+
|
|
67
|
+
// Tool outcome tracking
|
|
68
|
+
let lastToolOutcome: { tool: string; ok: boolean; tail: string } | null =
|
|
69
|
+
null;
|
|
70
|
+
|
|
71
|
+
function setHint(text: string | null, type: HintType | null) {
|
|
72
|
+
currentHint = text;
|
|
73
|
+
hintType = type;
|
|
74
|
+
if (!widgetCtx || paused) return;
|
|
75
|
+
if (!text) {
|
|
76
|
+
widgetCtx.ui.setWidget("hint", undefined);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const icon = type === "recall" ? "↩" : "→";
|
|
80
|
+
const display = text.length > 80 ? text.slice(0, 77) + "..." : text;
|
|
81
|
+
widgetCtx.ui.setWidget("hint", [
|
|
82
|
+
`\x1b[38;5;240m${icon} ${display.replace(/\n/g, " ")}\x1b[0m`,
|
|
83
|
+
]);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Pause/resume hint visibility (for dialogs, overlays, etc.)
|
|
87
|
+
let paused = false;
|
|
88
|
+
|
|
89
|
+
function pauseHint() {
|
|
90
|
+
paused = true;
|
|
91
|
+
if (widgetCtx) widgetCtx.ui.setWidget("hint", undefined);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function resumeHint() {
|
|
95
|
+
paused = false;
|
|
96
|
+
if (currentHint) setHint(currentHint, hintType);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
pi.events.on("pi-next-cue:pause", pauseHint);
|
|
100
|
+
pi.events.on("pi-next-cue:resume", resumeHint);
|
|
101
|
+
|
|
102
|
+
// Load config from ~/.pi/agent/pi-next-cue.json
|
|
103
|
+
let userConfig: { provider?: string; model?: string; keys?: { fill?: string; send?: string } } = {};
|
|
104
|
+
try {
|
|
105
|
+
const configPath = path.join(getAgentDir(), "pi-next-cue.json");
|
|
106
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
107
|
+
userConfig = JSON.parse(raw);
|
|
108
|
+
} catch {
|
|
109
|
+
// No config file — use defaults
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const fillKey = userConfig.keys?.fill || "tab";
|
|
113
|
+
const sendKey = userConfig.keys?.send || "enter";
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Resolve the model to use for suggestion generation.
|
|
117
|
+
* Tries user config first, then falls back to session default.
|
|
118
|
+
*/
|
|
119
|
+
function resolveModel(ctx: any) {
|
|
120
|
+
if (userConfig.provider && userConfig.model) {
|
|
121
|
+
const found = ctx.modelRegistry.find(userConfig.provider, userConfig.model);
|
|
122
|
+
if (found) return found;
|
|
123
|
+
}
|
|
124
|
+
return ctx.model;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function generateSuggestion(ctx: any) {
|
|
128
|
+
if (suggestionAbort) {
|
|
129
|
+
suggestionAbort.abort();
|
|
130
|
+
suggestionAbort = null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const model = resolveModel(ctx);
|
|
134
|
+
if (!model) return;
|
|
135
|
+
|
|
136
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
137
|
+
if (!auth.ok || !auth.apiKey) return;
|
|
138
|
+
|
|
139
|
+
// Gather recent messages for context
|
|
140
|
+
const branch = ctx.sessionManager.getBranch();
|
|
141
|
+
const recentMessages: Array<{ role: string; text: string }> = [];
|
|
142
|
+
const recentTools: string[] = [];
|
|
143
|
+
|
|
144
|
+
for (let i = branch.length - 1; i >= 0 && recentMessages.length < 6; i--) {
|
|
145
|
+
const entry = branch[i];
|
|
146
|
+
if (entry.type !== "message") continue;
|
|
147
|
+
const msg = entry.message;
|
|
148
|
+
if (!("role" in msg)) continue;
|
|
149
|
+
|
|
150
|
+
if (msg.role === "toolResult" && recentTools.length < 5) {
|
|
151
|
+
const toolName = msg.toolName || "unknown";
|
|
152
|
+
if (!recentTools.includes(toolName)) recentTools.push(toolName);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (msg.role !== "user" && msg.role !== "assistant") continue;
|
|
157
|
+
|
|
158
|
+
const textParts = msg.content
|
|
159
|
+
.filter((c: any) => c.type === "text")
|
|
160
|
+
.map((c: any) => c.text)
|
|
161
|
+
.join("\n");
|
|
162
|
+
|
|
163
|
+
if (textParts.trim()) {
|
|
164
|
+
const maxLen = msg.role === "assistant" ? 1000 : 500;
|
|
165
|
+
const text =
|
|
166
|
+
msg.role === "assistant" && textParts.length > maxLen
|
|
167
|
+
? "..." + textParts.slice(-maxLen)
|
|
168
|
+
: textParts.slice(0, maxLen);
|
|
169
|
+
recentMessages.unshift({ role: msg.role, text });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (recentMessages.length === 0) return;
|
|
174
|
+
|
|
175
|
+
// Build context
|
|
176
|
+
const parts: string[] = [];
|
|
177
|
+
|
|
178
|
+
if (corrections.length > 0) {
|
|
179
|
+
const corrLines = corrections
|
|
180
|
+
.slice(-3)
|
|
181
|
+
.map((c) => `- suggested "${c.suggested}" → user typed "${c.actual}"`)
|
|
182
|
+
.join("\n");
|
|
183
|
+
parts.push(`[Corrections]\n${corrLines}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (lastToolOutcome) {
|
|
187
|
+
const status = lastToolOutcome.ok ? "✓" : "✗";
|
|
188
|
+
parts.push(
|
|
189
|
+
`[Last Tool] ${status} ${lastToolOutcome.tool}: ${lastToolOutcome.tail}`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (recentTools.length > 0) {
|
|
194
|
+
parts.push(`[Tools Used] ${recentTools.join(", ")}`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const contextLines = recentMessages.map((m, i) => {
|
|
198
|
+
const prefix = m.role === "user" ? "User" : "Assistant";
|
|
199
|
+
const isLast = i === recentMessages.length - 1;
|
|
200
|
+
return isLast ? `[LATEST] ${prefix}: ${m.text}` : `${prefix}: ${m.text}`;
|
|
201
|
+
});
|
|
202
|
+
parts.push(contextLines.join("\n\n"));
|
|
203
|
+
|
|
204
|
+
const userMessage: UserMessage = {
|
|
205
|
+
role: "user",
|
|
206
|
+
content: [{ type: "text", text: parts.join("\n\n") }],
|
|
207
|
+
timestamp: Date.now(),
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const abort = new AbortController();
|
|
211
|
+
suggestionAbort = abort;
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
const response = await complete(
|
|
215
|
+
model,
|
|
216
|
+
{ systemPrompt: SYSTEM_PROMPT, messages: [userMessage] },
|
|
217
|
+
{
|
|
218
|
+
apiKey: auth.apiKey,
|
|
219
|
+
headers: auth.headers,
|
|
220
|
+
signal: abort.signal,
|
|
221
|
+
maxTokens: 40,
|
|
222
|
+
onPayload: (payload: any) => {
|
|
223
|
+
// Disable thinking for models that support it (e.g. DeepSeek)
|
|
224
|
+
payload.thinking = { type: "disabled" };
|
|
225
|
+
return payload;
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
if (response.stopReason === "aborted") return;
|
|
231
|
+
|
|
232
|
+
const suggestion = response.content
|
|
233
|
+
.filter((c: any) => c.type === "text")
|
|
234
|
+
.map((c: any) => c.text)
|
|
235
|
+
.join("")
|
|
236
|
+
.trim()
|
|
237
|
+
.replace(/^["'`]|["'`]$/g, "")
|
|
238
|
+
.slice(0, 60);
|
|
239
|
+
|
|
240
|
+
if (suggestion && suggestion !== SKIP_TOKEN && !abort.signal.aborted) {
|
|
241
|
+
setHint(suggestion, "cue");
|
|
242
|
+
lastSuggestion = suggestion;
|
|
243
|
+
}
|
|
244
|
+
} catch {
|
|
245
|
+
// Suggestion is optional — fail silently
|
|
246
|
+
} finally {
|
|
247
|
+
if (suggestionAbort === abort) suggestionAbort = null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Track tool outcomes
|
|
252
|
+
pi.on("tool_execution_end", async (event: any) => {
|
|
253
|
+
const content =
|
|
254
|
+
typeof event.result === "string"
|
|
255
|
+
? event.result
|
|
256
|
+
: Array.isArray(event.result)
|
|
257
|
+
? event.result
|
|
258
|
+
.filter((b: any) => b.type === "text")
|
|
259
|
+
.map((b: any) => b.text)
|
|
260
|
+
.join("")
|
|
261
|
+
: "";
|
|
262
|
+
const tail =
|
|
263
|
+
content.length > 150 ? "..." + content.slice(-150) : content;
|
|
264
|
+
lastToolOutcome = {
|
|
265
|
+
tool: event.toolName || "unknown",
|
|
266
|
+
ok: !event.isError,
|
|
267
|
+
tail: tail.replace(/\n/g, " ").slice(0, 150),
|
|
268
|
+
};
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Track user messages → show ↩ hint + record corrections
|
|
272
|
+
pi.on("message_end", async (event) => {
|
|
273
|
+
if (event.message.role === "user") {
|
|
274
|
+
if (suggestionAbort) {
|
|
275
|
+
suggestionAbort.abort();
|
|
276
|
+
suggestionAbort = null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const content = event.message.content;
|
|
280
|
+
if (!Array.isArray(content)) return;
|
|
281
|
+
const text = content
|
|
282
|
+
.filter((b: any) => b.type === "text")
|
|
283
|
+
.map((b: any) => b.text)
|
|
284
|
+
.join("");
|
|
285
|
+
|
|
286
|
+
if (text.trim() && lastSuggestion && text.trim() !== lastSuggestion) {
|
|
287
|
+
corrections.push({ suggested: lastSuggestion, actual: text.trim() });
|
|
288
|
+
if (corrections.length > MAX_CORRECTIONS) corrections.shift();
|
|
289
|
+
}
|
|
290
|
+
lastSuggestion = null;
|
|
291
|
+
|
|
292
|
+
if (text.trim()) setHint(text, "recall");
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// Generate suggestion after agent completes
|
|
297
|
+
pi.on("agent_end", async (event: any, ctx) => {
|
|
298
|
+
if (event.willRetry || !event.messages?.length) return;
|
|
299
|
+
|
|
300
|
+
const lastAssistant = [...event.messages]
|
|
301
|
+
.reverse()
|
|
302
|
+
.find((m: any) => m.role === "assistant");
|
|
303
|
+
if (!lastAssistant) return;
|
|
304
|
+
|
|
305
|
+
const hasText = lastAssistant.content?.some(
|
|
306
|
+
(b: any) => b.type === "text" && b.text?.trim(),
|
|
307
|
+
);
|
|
308
|
+
if (!hasText) return;
|
|
309
|
+
|
|
310
|
+
generateSuggestion(ctx);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
pi.on("session_start", async (event: any, ctx) => {
|
|
314
|
+
if (ctx.mode !== "tui") return;
|
|
315
|
+
widgetCtx = ctx;
|
|
316
|
+
|
|
317
|
+
if (event.reason === "reload") {
|
|
318
|
+
generateSuggestion(ctx);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Extend editor: Tab fills hint, Enter sends it
|
|
322
|
+
const prevFactory = ctx.ui.getEditorComponent();
|
|
323
|
+
|
|
324
|
+
ctx.ui.setEditorComponent((tui, theme, keybindings) => {
|
|
325
|
+
const base = prevFactory
|
|
326
|
+
? prevFactory(tui, theme, keybindings)
|
|
327
|
+
: new CustomEditor(tui, theme, keybindings);
|
|
328
|
+
|
|
329
|
+
const originalHandleInput = base.handleInput.bind(base);
|
|
330
|
+
|
|
331
|
+
base.handleInput = (data: string) => {
|
|
332
|
+
const text = base.getText();
|
|
333
|
+
const isEmpty = !text || text.trim() === "";
|
|
334
|
+
|
|
335
|
+
if (isEmpty && currentHint) {
|
|
336
|
+
if (matchesKey(data, fillKey)) {
|
|
337
|
+
base.setText(currentHint);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (matchesKey(data, sendKey)) {
|
|
341
|
+
const hint = currentHint;
|
|
342
|
+
setHint(null, null);
|
|
343
|
+
pi.sendUserMessage(hint);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
originalHandleInput(data);
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
return base;
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-next-cue",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Predicts your next prompt after each agent turn — shown as a hint above the editor",
|
|
5
|
+
"keywords": ["pi", "pi-package", "pi-extension"],
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "ouzhenkun",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/ouzhenkun/pi-next-cue.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/ouzhenkun/pi-next-cue",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/ouzhenkun/pi-next-cue/issues"
|
|
15
|
+
},
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
18
|
+
},
|
|
19
|
+
"pi": {
|
|
20
|
+
"extensions": ["./index.ts"]
|
|
21
|
+
}
|
|
22
|
+
}
|