pi-undo 0.1.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/README.md +64 -0
- package/index.ts +75 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# pi-undo
|
|
2
|
+
|
|
3
|
+
A pi extension that undoes the last user turn in the current session via `/undo`.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# local path
|
|
9
|
+
pi install /absolute/path/to/pi-undo
|
|
10
|
+
|
|
11
|
+
# or try once without installing
|
|
12
|
+
pi -e /absolute/path/to/pi-undo
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
| Command | Effect |
|
|
18
|
+
|---------|--------|
|
|
19
|
+
| `/undo` | Undo the last user turn |
|
|
20
|
+
| `/undo 2` | Undo the last two user turns |
|
|
21
|
+
|
|
22
|
+
Behavior matches selecting the previous user message in `/tree`:
|
|
23
|
+
|
|
24
|
+
1. Moves the session leaf to the parent of that user message (append-only branch).
|
|
25
|
+
2. Puts the undone prompt text back in the editor so you can edit and resubmit.
|
|
26
|
+
3. Leaves the abandoned path in the session tree (nothing is deleted).
|
|
27
|
+
|
|
28
|
+
## What it does **not** do
|
|
29
|
+
|
|
30
|
+
- Does **not** revert filesystem changes from tools (`edit`, `write`, `bash`, …).
|
|
31
|
+
Session history and disk state are separate. Use something like the
|
|
32
|
+
[git-checkpoint](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/examples/extensions/git-checkpoint.ts)
|
|
33
|
+
example if you want code restored when branching.
|
|
34
|
+
- Does **not** run while the agent is streaming. Abort first, then `/undo`.
|
|
35
|
+
|
|
36
|
+
## Prompt cache / read-write cache safety
|
|
37
|
+
|
|
38
|
+
Undo goes through `ctx.navigateTree()`:
|
|
39
|
+
|
|
40
|
+
- Session storage stays append-only (tree branch, no rewrite/truncation of JSONL).
|
|
41
|
+
- Agent message state is rebuilt from the retained branch prefix.
|
|
42
|
+
- The same session id is kept, so provider prompt-cache affinity remains valid
|
|
43
|
+
for the shared prefix (everything still on the active path after the rewind).
|
|
44
|
+
|
|
45
|
+
Avoid hand-editing the session file or clearing messages outside the tree API —
|
|
46
|
+
that is what breaks cache alignment.
|
|
47
|
+
|
|
48
|
+
## How it works
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
user₁ → asst₁ → user₂ → asst₂ ← leaf
|
|
52
|
+
▲
|
|
53
|
+
└── /undo targets this user message
|
|
54
|
+
|
|
55
|
+
After /undo:
|
|
56
|
+
user₁ → asst₁ ← new leaf
|
|
57
|
+
└── (user₂ → asst₂ still in tree as abandoned branch)
|
|
58
|
+
|
|
59
|
+
Editor contains the text of user₂ for re-edit/resubmit.
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## License
|
|
63
|
+
|
|
64
|
+
MIT
|
package/index.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /undo - walk the active session branch back one user turn.
|
|
3
|
+
*
|
|
4
|
+
* Uses ctx.navigateTree() so session state stays append-only (tree branch),
|
|
5
|
+
* agent messages rebuild from the retained prefix, and prompt-cache affinity
|
|
6
|
+
* is preserved for everything still on the active path.
|
|
7
|
+
*
|
|
8
|
+
* Does not revert filesystem changes made by tools during the undone turn.
|
|
9
|
+
* Pair with git-checkpoint (or similar) if you need code restore on branch.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
|
|
14
|
+
function findLastUserMessageId(ctx: ExtensionCommandContext): string | undefined {
|
|
15
|
+
const branch = ctx.sessionManager.getBranch();
|
|
16
|
+
for (let i = branch.length - 1; i >= 0; i--) {
|
|
17
|
+
const entry = branch[i];
|
|
18
|
+
if (entry.type === "message" && entry.message.role === "user") {
|
|
19
|
+
return entry.id;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function parseSteps(args: string): number | undefined {
|
|
26
|
+
const trimmed = args.trim();
|
|
27
|
+
if (!trimmed) return 1;
|
|
28
|
+
if (!/^\d+$/.test(trimmed)) return undefined;
|
|
29
|
+
const n = Number(trimmed);
|
|
30
|
+
if (!Number.isInteger(n) || n < 1) return undefined;
|
|
31
|
+
return n;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default function (pi: ExtensionAPI) {
|
|
35
|
+
pi.registerCommand("undo", {
|
|
36
|
+
description: "Undo the last user turn (usage: /undo [n])",
|
|
37
|
+
handler: async (args, ctx) => {
|
|
38
|
+
const steps = parseSteps(args);
|
|
39
|
+
if (steps === undefined) {
|
|
40
|
+
ctx.ui.notify("Usage: /undo [n] (n = positive integer, default 1)", "warning");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!ctx.isIdle()) {
|
|
45
|
+
ctx.ui.notify("Agent is busy. Abort the current turn first, then /undo.", "warning");
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let undone = 0;
|
|
50
|
+
for (let i = 0; i < steps; i++) {
|
|
51
|
+
const targetId = findLastUserMessageId(ctx);
|
|
52
|
+
if (!targetId) {
|
|
53
|
+
if (undone === 0) {
|
|
54
|
+
ctx.ui.notify("Nothing to undo", "info");
|
|
55
|
+
} else {
|
|
56
|
+
ctx.ui.notify(`Undid ${undone} turn${undone === 1 ? "" : "s"} (no more on this branch)`, "info");
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Navigate to the user message: leaf moves to its parent, prompt
|
|
62
|
+
// text is restored to the editor. No branch summary — undo is a
|
|
63
|
+
// deliberate rewind, not a context-preserving switch.
|
|
64
|
+
const result = await ctx.navigateTree(targetId, { summarize: false });
|
|
65
|
+
if (result.cancelled) {
|
|
66
|
+
ctx.ui.notify(undone === 0 ? "Undo cancelled" : `Undid ${undone}, then cancelled`, "warning");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
undone++;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
ctx.ui.notify(undone === 1 ? "Undid last turn" : `Undid ${undone} turns`, "info");
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-undo",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A pi extension that undoes the last user turn in a session via /undo",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Richard Anaya",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "index.ts",
|
|
9
|
+
"types": "index.ts",
|
|
10
|
+
"files": [
|
|
11
|
+
"index.ts",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
16
|
+
"prepublishOnly": "echo 'Ready to publish'"
|
|
17
|
+
},
|
|
18
|
+
"pi": {
|
|
19
|
+
"extensions": [
|
|
20
|
+
"./index.ts"
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {},
|
|
24
|
+
"devDependencies": {},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18.0.0"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"pi-package",
|
|
30
|
+
"pi-extension",
|
|
31
|
+
"pi",
|
|
32
|
+
"cli",
|
|
33
|
+
"tui",
|
|
34
|
+
"undo",
|
|
35
|
+
"session",
|
|
36
|
+
"branch",
|
|
37
|
+
"terminal",
|
|
38
|
+
"ai"
|
|
39
|
+
]
|
|
40
|
+
}
|