@sassywares/pi-extensions 0.3.22 → 0.3.23
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/README.md +1 -0
- package/extensions/modes/index.ts +50 -28
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -154,6 +154,7 @@ A backend is only trusted after it proves both halves of the contract: reads ins
|
|
|
154
154
|
`/modes` shows the verdict. The mode badge sits on a footer line of its own, aligned to the right edge: `▶ work`, `⏸ ask · ro(sandbox-exec)`, or `⏸ ask · bash off`. To place it there without reimplementing pi's footer, the extension delegates to pi's own `FooterComponent` and appends the badge line, so directory, session name, token stats, model, and context usage stay exactly as pi renders them. If a future pi release changes what that component reads, the extension restores the native footer and falls back to a left-aligned status line.
|
|
155
155
|
|
|
156
156
|
- `--ask` starts a session in ask mode; the mode persists across resume.
|
|
157
|
+
- Switching mid-session applies immediately: the toolset is swapped and bash starts or stops being sandboxed. When ask mode is switched off, the model is told once that work mode is active, so it stops repeating an earlier claim that it was in ask mode. Only the newest mode notice is kept in context, and the ask prompt is refreshed on every turn, so a long session does not accumulate stale instructions.
|
|
157
158
|
- `shift+tab` is not used, because pi binds it to the thinking level. To match an editor that uses it for modes, bind `app.thinking.cycle` to something else in `~/.pi/agent/keybindings.json` and add a shortcut there.
|
|
158
159
|
- Ask mode is a guardrail for local changes, not a jail. Network access stays open, so a command can still change remote state if it holds a credential. Use pi's containerized setup when the boundary has to hold against a hostile model.
|
|
159
160
|
- **Windows is unsupported by design.** Ask mode needs an OS-level read-only sandbox that Windows does not offer cheaply, so the extension registers nothing there. `/ask` and `/work` do not exist, and every tool behaves as before.
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
* Windows is intentionally unsupported: no modes are registered there.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
18
17
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
19
18
|
import { createWrappedOperations, detectBackend, type SandboxBackend } from "./backends.ts";
|
|
20
19
|
import { createModesFooter, type ModeBadge } from "./footer.ts";
|
|
@@ -24,8 +23,8 @@ type Mode = "work" | "ask";
|
|
|
24
23
|
/** Tools that may run in ask mode. Everything else is hidden and blocked. */
|
|
25
24
|
const ASK_TOOLS = ["read", "grep", "find", "ls", "bash", "web_search", "web_fetch", "questionnaire"];
|
|
26
25
|
|
|
27
|
-
const
|
|
28
|
-
const
|
|
26
|
+
const ASK_CONTEXT_TYPE = "modes-ask-context";
|
|
27
|
+
const WORK_CONTEXT_TYPE = "modes-work-context";
|
|
29
28
|
const STATE_CUSTOM_TYPE = "modes";
|
|
30
29
|
const SHORTCUT = "alt+m";
|
|
31
30
|
|
|
@@ -33,15 +32,25 @@ interface PersistedState {
|
|
|
33
32
|
mode?: Mode;
|
|
34
33
|
}
|
|
35
34
|
|
|
36
|
-
const ASK_PROMPT =
|
|
35
|
+
const ASK_PROMPT = `[ASK MODE ACTIVE]
|
|
37
36
|
You are in ask mode: reading, searching, and answering only.
|
|
38
37
|
|
|
39
38
|
Rules:
|
|
40
39
|
- Do not modify files. The edit and write tools are not available.
|
|
41
40
|
- bash runs inside a read-only sandbox. Reading, searching, git history, and network access work; anything that writes fails with a permission error. Do not retry a failed write - report the limitation instead.
|
|
42
41
|
- Answer from evidence: cite file paths with line numbers, and URLs for web results.
|
|
42
|
+
- This instruction overrides earlier turns. If the conversation shows file edits, write tools, or unrestricted bash, that happened before ask mode was enabled and says nothing about the current turn.
|
|
43
43
|
- If a change is needed, describe it precisely and tell the user to switch to work mode (/work or alt+m).`;
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Sent once on the first turn after ask mode is switched off. Without it the
|
|
47
|
+
* model keeps answering from its own earlier "I am in ask mode" reply, because
|
|
48
|
+
* the ask instructions are gone and nothing replaces them.
|
|
49
|
+
*/
|
|
50
|
+
const WORK_PROMPT = `[WORK MODE ACTIVE]
|
|
51
|
+
Ask mode was switched off for this session. You can edit files, write new files, and run any bash command again.
|
|
52
|
+
Ignore any earlier statement that you were in ask mode: it no longer applies.`;
|
|
53
|
+
|
|
45
54
|
export default function modesExtension(pi: ExtensionAPI): void {
|
|
46
55
|
// Ask mode depends on an OS-level sandbox that Windows does not provide.
|
|
47
56
|
// Rather than ship a weaker guardrail, the extension does nothing there.
|
|
@@ -52,6 +61,7 @@ export default function modesExtension(pi: ExtensionAPI): void {
|
|
|
52
61
|
let backendReason = "not detected yet";
|
|
53
62
|
let backendChecked = false;
|
|
54
63
|
let workTools: string[] | undefined;
|
|
64
|
+
let announceWork = false;
|
|
55
65
|
let footerStatus: "none" | "custom" | "fallback" = "none";
|
|
56
66
|
let footerTui: { requestRender(): void } | undefined;
|
|
57
67
|
const wrappedCalls = new Set<string>();
|
|
@@ -156,6 +166,7 @@ export default function modesExtension(pi: ExtensionAPI): void {
|
|
|
156
166
|
if (next === "ask") {
|
|
157
167
|
await ensureBackend(ctx.cwd);
|
|
158
168
|
mode = "ask";
|
|
169
|
+
announceWork = false;
|
|
159
170
|
workTools = pi.getActiveTools();
|
|
160
171
|
pi.setActiveTools(askToolset());
|
|
161
172
|
updateStatus(ctx);
|
|
@@ -169,6 +180,7 @@ export default function modesExtension(pi: ExtensionAPI): void {
|
|
|
169
180
|
}
|
|
170
181
|
|
|
171
182
|
mode = "work";
|
|
183
|
+
announceWork = true;
|
|
172
184
|
pi.setActiveTools(workTools ?? pi.getActiveTools());
|
|
173
185
|
workTools = undefined;
|
|
174
186
|
updateStatus(ctx);
|
|
@@ -250,21 +262,48 @@ export default function modesExtension(pi: ExtensionAPI): void {
|
|
|
250
262
|
});
|
|
251
263
|
|
|
252
264
|
pi.on("before_agent_start", async (event) => {
|
|
253
|
-
if (mode !== "ask") return undefined;
|
|
254
265
|
if (event.messageType !== "user") return undefined;
|
|
266
|
+
|
|
267
|
+
if (mode === "ask") {
|
|
268
|
+
return {
|
|
269
|
+
message: {
|
|
270
|
+
customType: ASK_CONTEXT_TYPE,
|
|
271
|
+
content: ASK_PROMPT,
|
|
272
|
+
display: false,
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Announce the switch to work once, so the model stops repeating its own
|
|
278
|
+
// earlier claim that it was in ask mode.
|
|
279
|
+
if (!announceWork) return undefined;
|
|
280
|
+
announceWork = false;
|
|
255
281
|
return {
|
|
256
282
|
message: {
|
|
257
|
-
customType:
|
|
258
|
-
content:
|
|
283
|
+
customType: WORK_CONTEXT_TYPE,
|
|
284
|
+
content: WORK_PROMPT,
|
|
259
285
|
display: false,
|
|
260
286
|
},
|
|
261
287
|
};
|
|
262
288
|
});
|
|
263
289
|
|
|
264
|
-
//
|
|
290
|
+
// Keep only the newest mode notice, and only if it matches the current mode.
|
|
291
|
+
// This drops stale ask instructions after a switch and stops the per-turn ask
|
|
292
|
+
// prompt from accumulating for the length of a session.
|
|
265
293
|
pi.on("context", async (event) => {
|
|
266
|
-
|
|
267
|
-
|
|
294
|
+
const wanted = mode === "ask" ? ASK_CONTEXT_TYPE : WORK_CONTEXT_TYPE;
|
|
295
|
+
let keep = -1;
|
|
296
|
+
event.messages.forEach((message, index) => {
|
|
297
|
+
if ((message as { customType?: string }).customType === wanted) keep = index;
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
messages: event.messages.filter((message, index) => {
|
|
302
|
+
const customType = (message as { customType?: string }).customType;
|
|
303
|
+
if (customType !== ASK_CONTEXT_TYPE && customType !== WORK_CONTEXT_TYPE) return true;
|
|
304
|
+
return index === keep;
|
|
305
|
+
}),
|
|
306
|
+
};
|
|
268
307
|
});
|
|
269
308
|
|
|
270
309
|
pi.on("session_start", async (_event, ctx) => {
|
|
@@ -278,6 +317,7 @@ export default function modesExtension(pi: ExtensionAPI): void {
|
|
|
278
317
|
|
|
279
318
|
mode = state?.data?.mode ?? "work";
|
|
280
319
|
if (pi.getFlag("ask") === true) mode = "ask";
|
|
320
|
+
announceWork = false;
|
|
281
321
|
|
|
282
322
|
workTools = undefined;
|
|
283
323
|
if (mode === "ask") {
|
|
@@ -289,21 +329,3 @@ export default function modesExtension(pi: ExtensionAPI): void {
|
|
|
289
329
|
updateStatus(ctx);
|
|
290
330
|
});
|
|
291
331
|
}
|
|
292
|
-
|
|
293
|
-
/** True for the hidden ask-mode prompt and for user messages that carry it. */
|
|
294
|
-
function isAskContext(message: AgentMessage): boolean {
|
|
295
|
-
const candidate = message as AgentMessage & { customType?: string };
|
|
296
|
-
if (candidate.customType === CONTEXT_CUSTOM_TYPE) return true;
|
|
297
|
-
if (candidate.role === "assistant") return false;
|
|
298
|
-
const content = (candidate as { content?: unknown }).content;
|
|
299
|
-
if (typeof content === "string") return content.includes(CONTEXT_MARKER);
|
|
300
|
-
if (!Array.isArray(content)) return false;
|
|
301
|
-
return content.some(
|
|
302
|
-
(part) =>
|
|
303
|
-
!!part &&
|
|
304
|
-
typeof part === "object" &&
|
|
305
|
-
(part as { type?: unknown }).type === "text" &&
|
|
306
|
-
typeof (part as { text?: unknown }).text === "string" &&
|
|
307
|
-
(part as { text: string }).text.includes(CONTEXT_MARKER),
|
|
308
|
-
);
|
|
309
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sassywares/pi-extensions",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.23",
|
|
4
4
|
"description": "A zesty power pack for pi: auto-titled sessions, real web access, clean handoffs, isolated subagents, and ask/work modes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|