yoyo-pi 0.1.4
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/LICENSE +21 -0
- package/README.md +91 -0
- package/README.zh-CN.md +91 -0
- package/docs/previews/agent-status.svg +83 -0
- package/docs/previews/filetree.png +0 -0
- package/docs/previews/multiple-choice.png +0 -0
- package/docs/previews/pi-tui-agent-status.html +379 -0
- package/docs/previews/pi-tui-status-bar.html +481 -0
- package/docs/previews/single-choice.png +0 -0
- package/docs/previews/status-bar.png +0 -0
- package/docs/previews/todo-sidebar.png +0 -0
- package/extensions/choice-picker.ts +1404 -0
- package/extensions/clear-context.ts +164 -0
- package/extensions/gr0k-hack/index.ts +1231 -0
- package/extensions/kenx-infra/index.ts +1601 -0
- package/extensions/kenx-infra/themes/dark.json +80 -0
- package/extensions/kenx-infra/themes/light.json +80 -0
- package/extensions/kenx-infra/themes/paper.json +80 -0
- package/extensions/plan-mode/README.md +58 -0
- package/extensions/plan-mode/agents/plan-agent.md +98 -0
- package/extensions/plan-mode/index.ts +1956 -0
- package/extensions/plan-mode/sandbox.ts +332 -0
- package/extensions/vim-mode.ts +561 -0
- package/package.json +58 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clear/restore context snapshots.
|
|
3
|
+
*
|
|
4
|
+
* /clear
|
|
5
|
+
* Saves the current branch context to .tmp/<random>.jsonl, then starts a new empty session.
|
|
6
|
+
*
|
|
7
|
+
* /restore <name>
|
|
8
|
+
* Restores .tmp/<name>.jsonl into a fresh session file and switches to it.
|
|
9
|
+
* /resume remains pi's built-in session picker.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
13
|
+
import * as fsp from "node:fs/promises";
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
import type { ExtensionAPI, ExtensionCommandContext, SessionHeader } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import { CURRENT_SESSION_VERSION } from "@earendil-works/pi-coding-agent";
|
|
17
|
+
|
|
18
|
+
const SNAPSHOT_DIR = ".tmp";
|
|
19
|
+
const SNAPSHOT_EXT = ".jsonl";
|
|
20
|
+
const SNAPSHOT_NAME_RE = /^[A-Za-z0-9_-]+$/;
|
|
21
|
+
|
|
22
|
+
function snapshotDir(cwd: string): string {
|
|
23
|
+
return path.resolve(cwd, SNAPSHOT_DIR);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function snapshotPath(cwd: string, name: string): string {
|
|
27
|
+
return path.join(snapshotDir(cwd), `${name}${SNAPSHOT_EXT}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function randomName(): string {
|
|
31
|
+
return randomBytes(5).toString("hex");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizeSnapshotName(raw: string): string | undefined {
|
|
35
|
+
const firstArg = raw.trim().split(/\s+/)[0] ?? "";
|
|
36
|
+
const name = firstArg.endsWith(SNAPSHOT_EXT) ? firstArg.slice(0, -SNAPSHOT_EXT.length) : firstArg;
|
|
37
|
+
if (!name || !SNAPSHOT_NAME_RE.test(name)) return undefined;
|
|
38
|
+
return name;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function createUniqueSnapshotPath(cwd: string): Promise<{ name: string; filePath: string }> {
|
|
42
|
+
await fsp.mkdir(snapshotDir(cwd), { recursive: true });
|
|
43
|
+
|
|
44
|
+
for (let i = 0; i < 100; i++) {
|
|
45
|
+
const name = randomName();
|
|
46
|
+
const filePath = snapshotPath(cwd, name);
|
|
47
|
+
try {
|
|
48
|
+
await fsp.access(filePath);
|
|
49
|
+
} catch {
|
|
50
|
+
return { name, filePath };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
throw new Error("Failed to generate a unique snapshot name");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function saveCurrentBranch(ctx: ExtensionCommandContext): Promise<{ name: string; filePath: string; entryCount: number }> {
|
|
58
|
+
const { name, filePath } = await createUniqueSnapshotPath(ctx.cwd);
|
|
59
|
+
const now = new Date().toISOString();
|
|
60
|
+
const currentFile = ctx.sessionManager.getSessionFile();
|
|
61
|
+
const currentHeader = ctx.sessionManager.getHeader();
|
|
62
|
+
|
|
63
|
+
const header: SessionHeader = {
|
|
64
|
+
type: "session",
|
|
65
|
+
version: CURRENT_SESSION_VERSION,
|
|
66
|
+
id: randomUUID(),
|
|
67
|
+
timestamp: now,
|
|
68
|
+
cwd: currentHeader?.cwd ?? ctx.cwd,
|
|
69
|
+
...(currentFile ? { parentSession: currentFile } : {}),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const entries = ctx.sessionManager.getBranch();
|
|
73
|
+
const content = [header, ...entries].map((entry) => JSON.stringify(entry)).join("\n") + "\n";
|
|
74
|
+
await fsp.writeFile(filePath, content, { encoding: "utf8", flag: "wx" });
|
|
75
|
+
|
|
76
|
+
return { name, filePath, entryCount: entries.length };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function materializeSnapshot(snapshotFile: string, ctx: ExtensionCommandContext): Promise<string> {
|
|
80
|
+
const raw = await fsp.readFile(snapshotFile, "utf8");
|
|
81
|
+
const lines = raw.split(/\r?\n/).filter((line) => line.trim().length > 0);
|
|
82
|
+
if (lines.length === 0) {
|
|
83
|
+
throw new Error(`Snapshot is empty: ${snapshotFile}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const first = JSON.parse(lines[0]!) as SessionHeader;
|
|
87
|
+
if (first.type !== "session") {
|
|
88
|
+
throw new Error(`Snapshot is not a pi session file: ${snapshotFile}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const id = randomUUID();
|
|
92
|
+
const now = new Date().toISOString();
|
|
93
|
+
const header: SessionHeader = {
|
|
94
|
+
...first,
|
|
95
|
+
version: first.version ?? CURRENT_SESSION_VERSION,
|
|
96
|
+
id,
|
|
97
|
+
timestamp: now,
|
|
98
|
+
cwd: ctx.cwd,
|
|
99
|
+
parentSession: snapshotFile,
|
|
100
|
+
};
|
|
101
|
+
lines[0] = JSON.stringify(header);
|
|
102
|
+
|
|
103
|
+
const sessionDir = ctx.sessionManager.getSessionDir() || snapshotDir(ctx.cwd);
|
|
104
|
+
await fsp.mkdir(sessionDir, { recursive: true });
|
|
105
|
+
const destPath = path.join(sessionDir, `${now.replace(/[:.]/g, "-")}_${id}${SNAPSHOT_EXT}`);
|
|
106
|
+
await fsp.writeFile(destPath, lines.join("\n") + "\n", { encoding: "utf8", flag: "wx" });
|
|
107
|
+
return destPath;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export default function clearContextExtension(pi: ExtensionAPI) {
|
|
111
|
+
pi.registerCommand("clear", {
|
|
112
|
+
description: "Save current context to .tmp/<name>.jsonl and start a clean session",
|
|
113
|
+
handler: async (_args, ctx) => {
|
|
114
|
+
await ctx.waitForIdle();
|
|
115
|
+
|
|
116
|
+
const snapshot = await saveCurrentBranch(ctx);
|
|
117
|
+
const shortPath = path.join(SNAPSHOT_DIR, `${snapshot.name}${SNAPSHOT_EXT}`);
|
|
118
|
+
|
|
119
|
+
const result = await ctx.newSession({
|
|
120
|
+
parentSession: snapshot.filePath,
|
|
121
|
+
withSession: async (newCtx) => {
|
|
122
|
+
newCtx.ui.notify(`Context saved to ${shortPath}. Restore with /restore ${snapshot.name}`, "info");
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (result.cancelled) {
|
|
127
|
+
ctx.ui.notify(`Context saved to ${shortPath}, but clearing was cancelled.`, "warning");
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
pi.registerCommand("restore", {
|
|
133
|
+
description: "Restore a /clear snapshot by name: /restore <name>",
|
|
134
|
+
handler: async (args, ctx) => {
|
|
135
|
+
const name = normalizeSnapshotName(args);
|
|
136
|
+
if (!name) {
|
|
137
|
+
ctx.ui.notify("Usage: /restore <snapshot-name>", "warning");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
await ctx.waitForIdle();
|
|
142
|
+
|
|
143
|
+
const sourcePath = snapshotPath(ctx.cwd, name);
|
|
144
|
+
try {
|
|
145
|
+
await fsp.access(sourcePath);
|
|
146
|
+
} catch {
|
|
147
|
+
ctx.ui.notify(`No snapshot found at ${path.join(SNAPSHOT_DIR, `${name}${SNAPSHOT_EXT}`)}`, "error");
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const restorePath = await materializeSnapshot(sourcePath, ctx);
|
|
152
|
+
const result = await ctx.switchSession(restorePath, {
|
|
153
|
+
withSession: async (newCtx) => {
|
|
154
|
+
newCtx.ui.notify(`Restored context from ${path.join(SNAPSHOT_DIR, `${name}${SNAPSHOT_EXT}`)}`, "info");
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (result.cancelled) {
|
|
159
|
+
await fsp.rm(restorePath, { force: true });
|
|
160
|
+
ctx.ui.notify("Restore cancelled.", "warning");
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
}
|