pi-extensions 0.1.9
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/.ralph/import-cc-codex.md +31 -0
- package/.ralph/import-cc-codex.state.json +14 -0
- package/.ralph/mario-not-impl.md +69 -0
- package/.ralph/mario-not-impl.state.json +14 -0
- package/.ralph/mario-not-spec.md +163 -0
- package/.ralph/mario-not-spec.state.json +14 -0
- package/LICENSE +21 -0
- package/README.md +65 -0
- package/RELEASING.md +34 -0
- package/agent-guidance/CHANGELOG.md +4 -0
- package/agent-guidance/README.md +102 -0
- package/agent-guidance/agent-guidance.ts +147 -0
- package/agent-guidance/package.json +22 -0
- package/agent-guidance/setup.sh +75 -0
- package/agent-guidance/templates/CLAUDE.md +5 -0
- package/agent-guidance/templates/CODEX.md +92 -0
- package/agent-guidance/templates/GEMINI.md +5 -0
- package/arcade/CHANGELOG.md +4 -0
- package/arcade/README.md +85 -0
- package/arcade/assets/picman.png +0 -0
- package/arcade/assets/ping.png +0 -0
- package/arcade/assets/spice-invaders.png +0 -0
- package/arcade/assets/tetris.png +0 -0
- package/arcade/mario-not/README.md +30 -0
- package/arcade/mario-not/boss.js +103 -0
- package/arcade/mario-not/camera.js +59 -0
- package/arcade/mario-not/collision.js +91 -0
- package/arcade/mario-not/colors.js +36 -0
- package/arcade/mario-not/constants.js +97 -0
- package/arcade/mario-not/core.js +39 -0
- package/arcade/mario-not/death.js +77 -0
- package/arcade/mario-not/effects.js +84 -0
- package/arcade/mario-not/enemies.js +31 -0
- package/arcade/mario-not/engine.js +171 -0
- package/arcade/mario-not/fireballs.js +98 -0
- package/arcade/mario-not/items.js +24 -0
- package/arcade/mario-not/levels.js +403 -0
- package/arcade/mario-not/logic.js +104 -0
- package/arcade/mario-not/mario-not.ts +297 -0
- package/arcade/mario-not/player.js +244 -0
- package/arcade/mario-not/render.js +257 -0
- package/arcade/mario-not/spec.md +548 -0
- package/arcade/mario-not/state.js +246 -0
- package/arcade/mario-not/tests/e2e.test.js +855 -0
- package/arcade/mario-not/tests/engine.test.js +888 -0
- package/arcade/mario-not/tests/fixtures/story0-frame.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story1-camera.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story1-glyphs.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story10-item.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story11-hazards.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story12-used-block.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story13-pipes.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story14-goal.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story15-hud-narrow.txt +2 -0
- package/arcade/mario-not/tests/fixtures/story16-unknown-tile.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story17-mix.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story18-hud-score.txt +2 -0
- package/arcade/mario-not/tests/fixtures/story19-cue.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story2-enemy.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story20-camera-offset.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story21-hud-zero.txt +2 -0
- package/arcade/mario-not/tests/fixtures/story22-big-viewport.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story23-camera-negative.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story24-camera-width.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story25-camera-positive.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story26-hud-lives.txt +2 -0
- package/arcade/mario-not/tests/fixtures/story27-hud-coins.txt +2 -0
- package/arcade/mario-not/tests/fixtures/story28-item-viewport.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story29-enemy-viewport.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story3-hud.txt +2 -0
- package/arcade/mario-not/tests/fixtures/story30-hud-score.txt +2 -0
- package/arcade/mario-not/tests/fixtures/story31-particles-viewport.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story32-paused-frame.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story4-big.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story5-resume-hud.txt +2 -0
- package/arcade/mario-not/tests/fixtures/story6-particles.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story6-paused.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story7-powerup.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story8-hud-time.txt +2 -0
- package/arcade/mario-not/tests/fixtures/story9-hud-level.txt +2 -0
- package/arcade/mario-not/tiles.js +79 -0
- package/arcade/mario-not/tsconfig.json +14 -0
- package/arcade/mario-not/types.js +225 -0
- package/arcade/package.json +26 -0
- package/arcade/picman.ts +328 -0
- package/arcade/ping.ts +594 -0
- package/arcade/spice-invaders.ts +1104 -0
- package/arcade/tetris.ts +662 -0
- package/code-actions/CHANGELOG.md +4 -0
- package/code-actions/README.md +65 -0
- package/code-actions/actions.ts +107 -0
- package/code-actions/index.ts +148 -0
- package/code-actions/package.json +22 -0
- package/code-actions/search.ts +79 -0
- package/code-actions/snippets.ts +179 -0
- package/code-actions/ui.ts +120 -0
- package/files-widget/CHANGELOG.md +90 -0
- package/files-widget/DESIGN.md +452 -0
- package/files-widget/README.md +122 -0
- package/files-widget/TODO.md +141 -0
- package/files-widget/browser.ts +922 -0
- package/files-widget/comment.ts +5 -0
- package/files-widget/constants.ts +18 -0
- package/files-widget/demo.svg +1 -0
- package/files-widget/file-tree.ts +224 -0
- package/files-widget/file-viewer.ts +93 -0
- package/files-widget/git.ts +107 -0
- package/files-widget/index.ts +140 -0
- package/files-widget/input-utils.ts +3 -0
- package/files-widget/package.json +22 -0
- package/files-widget/types.ts +28 -0
- package/files-widget/utils.ts +26 -0
- package/files-widget/viewer.ts +424 -0
- package/import-cc-codex/research/import-chats-from-other-agents.md +135 -0
- package/import-cc-codex/spec.md +79 -0
- package/package.json +29 -0
- package/ralph-wiggum/CHANGELOG.md +7 -0
- package/ralph-wiggum/README.md +96 -0
- package/ralph-wiggum/SKILL.md +73 -0
- package/ralph-wiggum/index.ts +792 -0
- package/ralph-wiggum/package.json +25 -0
- package/raw-paste/CHANGELOG.md +7 -0
- package/raw-paste/README.md +52 -0
- package/raw-paste/index.ts +112 -0
- package/raw-paste/package.json +22 -0
- package/tab-status/CHANGELOG.md +4 -0
- package/tab-status/README.md +61 -0
- package/tab-status/assets/tab-status.png +0 -0
- package/tab-status/package.json +22 -0
- package/tab-status/tab-status.ts +179 -0
- package/usage-extension/CHANGELOG.md +17 -0
- package/usage-extension/README.md +120 -0
- package/usage-extension/index.ts +628 -0
- package/usage-extension/package.json +22 -0
- package/usage-extension/screenshot.png +0 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
|
|
3
|
+
import { MAX_TREE_DEPTH } from "./constants";
|
|
4
|
+
import type { DiffStats, FileNode, FlatNode } from "./types";
|
|
5
|
+
|
|
6
|
+
const collator = new Intl.Collator(undefined, { sensitivity: "base" });
|
|
7
|
+
|
|
8
|
+
function compareNodes(a: FileNode, b: FileNode): number {
|
|
9
|
+
if (a.isDirectory !== b.isDirectory) {
|
|
10
|
+
return a.isDirectory ? -1 : 1;
|
|
11
|
+
}
|
|
12
|
+
return collator.compare(a.name, b.name);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function shouldIgnoreSegment(segment: string, ignored: Set<string>): boolean {
|
|
16
|
+
return ignored.has(segment) || segment.startsWith(".");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function sortChildren(node: FileNode): void {
|
|
20
|
+
if (!node.children || node.children.length === 0) return;
|
|
21
|
+
node.children.sort(compareNodes);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function sortTree(node: FileNode): void {
|
|
25
|
+
sortChildren(node);
|
|
26
|
+
if (node.children) {
|
|
27
|
+
for (const child of node.children) {
|
|
28
|
+
if (child.isDirectory) {
|
|
29
|
+
sortTree(child);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function updateTreeStats(root: FileNode | null): void {
|
|
36
|
+
if (!root) return;
|
|
37
|
+
|
|
38
|
+
function traverse(node: FileNode): {
|
|
39
|
+
totalLines: number;
|
|
40
|
+
totalAdditions: number;
|
|
41
|
+
totalDeletions: number;
|
|
42
|
+
lineCountComplete: boolean;
|
|
43
|
+
hasChanges: boolean;
|
|
44
|
+
} {
|
|
45
|
+
if (!node.isDirectory) {
|
|
46
|
+
const totalLines = node.lineCount ?? 0;
|
|
47
|
+
const totalAdditions = node.diffStats?.additions ?? 0;
|
|
48
|
+
const totalDeletions = node.diffStats?.deletions ?? 0;
|
|
49
|
+
const lineCountComplete = node.lineCount !== undefined;
|
|
50
|
+
const hasChanges = Boolean(node.gitStatus || node.agentModified);
|
|
51
|
+
return { totalLines, totalAdditions, totalDeletions, lineCountComplete, hasChanges };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let totalLines = 0;
|
|
55
|
+
let totalAdditions = 0;
|
|
56
|
+
let totalDeletions = 0;
|
|
57
|
+
let lineCountComplete = true;
|
|
58
|
+
let hasChanges = false;
|
|
59
|
+
|
|
60
|
+
if (node.children) {
|
|
61
|
+
for (const child of node.children) {
|
|
62
|
+
const stats = traverse(child);
|
|
63
|
+
totalLines += stats.totalLines;
|
|
64
|
+
totalAdditions += stats.totalAdditions;
|
|
65
|
+
totalDeletions += stats.totalDeletions;
|
|
66
|
+
if (!stats.lineCountComplete) {
|
|
67
|
+
lineCountComplete = false;
|
|
68
|
+
}
|
|
69
|
+
if (stats.hasChanges) {
|
|
70
|
+
hasChanges = true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
node.totalLines = totalLines;
|
|
76
|
+
node.totalAdditions = totalAdditions;
|
|
77
|
+
node.totalDeletions = totalDeletions;
|
|
78
|
+
node.lineCountComplete = lineCountComplete;
|
|
79
|
+
node.hasChangedChildren = hasChanges;
|
|
80
|
+
|
|
81
|
+
return { totalLines, totalAdditions, totalDeletions, lineCountComplete, hasChanges };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
traverse(root);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function buildFileTreeFromPaths(
|
|
88
|
+
cwd: string,
|
|
89
|
+
filePaths: string[],
|
|
90
|
+
gitStatus: Map<string, string>,
|
|
91
|
+
diffStats: Map<string, DiffStats>,
|
|
92
|
+
ignored: Set<string>,
|
|
93
|
+
agentModified: Set<string>
|
|
94
|
+
): FileNode {
|
|
95
|
+
const root: FileNode = {
|
|
96
|
+
name: ".",
|
|
97
|
+
path: cwd,
|
|
98
|
+
isDirectory: true,
|
|
99
|
+
children: [],
|
|
100
|
+
expanded: true,
|
|
101
|
+
hasChangedChildren: false,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const directoryMap = new Map<string, FileNode>();
|
|
105
|
+
directoryMap.set("", root);
|
|
106
|
+
const seenFiles = new Set<string>();
|
|
107
|
+
|
|
108
|
+
for (const rawPath of filePaths) {
|
|
109
|
+
let normalized = rawPath.trim();
|
|
110
|
+
if (!normalized) continue;
|
|
111
|
+
if (normalized.startsWith("./")) {
|
|
112
|
+
normalized = normalized.slice(2);
|
|
113
|
+
}
|
|
114
|
+
normalized = normalized.replace(/\\/g, "/");
|
|
115
|
+
|
|
116
|
+
const parts = normalized.split("/").filter(Boolean);
|
|
117
|
+
if (parts.length === 0) continue;
|
|
118
|
+
const dirDepth = parts.length - 1;
|
|
119
|
+
if (dirDepth > MAX_TREE_DEPTH) continue;
|
|
120
|
+
|
|
121
|
+
let current = root;
|
|
122
|
+
let relPath = "";
|
|
123
|
+
let skip = false;
|
|
124
|
+
|
|
125
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
126
|
+
const part = parts[i];
|
|
127
|
+
if (shouldIgnoreSegment(part, ignored)) {
|
|
128
|
+
skip = true;
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
relPath = relPath ? `${relPath}/${part}` : part;
|
|
132
|
+
let dirNode = directoryMap.get(relPath);
|
|
133
|
+
if (!dirNode) {
|
|
134
|
+
const depth = i + 1;
|
|
135
|
+
dirNode = {
|
|
136
|
+
name: part,
|
|
137
|
+
path: join(cwd, relPath),
|
|
138
|
+
isDirectory: true,
|
|
139
|
+
children: [],
|
|
140
|
+
expanded: depth < 1,
|
|
141
|
+
hasChangedChildren: false,
|
|
142
|
+
};
|
|
143
|
+
directoryMap.set(relPath, dirNode);
|
|
144
|
+
current.children?.push(dirNode);
|
|
145
|
+
}
|
|
146
|
+
current = dirNode;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (skip) continue;
|
|
150
|
+
|
|
151
|
+
const fileName = parts[parts.length - 1];
|
|
152
|
+
if (shouldIgnoreSegment(fileName, ignored)) continue;
|
|
153
|
+
|
|
154
|
+
const fileRelPath = parts.join("/");
|
|
155
|
+
if (seenFiles.has(fileRelPath)) continue;
|
|
156
|
+
seenFiles.add(fileRelPath);
|
|
157
|
+
|
|
158
|
+
const filePath = join(cwd, fileRelPath);
|
|
159
|
+
const fileGitStatus = gitStatus.get(fileRelPath);
|
|
160
|
+
const fileDiffStats = diffStats.get(fileRelPath);
|
|
161
|
+
|
|
162
|
+
current.children?.push({
|
|
163
|
+
name: fileName,
|
|
164
|
+
path: filePath,
|
|
165
|
+
isDirectory: false,
|
|
166
|
+
gitStatus: fileGitStatus,
|
|
167
|
+
agentModified: agentModified.has(filePath),
|
|
168
|
+
diffStats: fileDiffStats,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
sortTree(root);
|
|
173
|
+
updateTreeStats(root);
|
|
174
|
+
return root;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function getIgnoredNames(): Set<string> {
|
|
178
|
+
return new Set([
|
|
179
|
+
"node_modules",
|
|
180
|
+
".git",
|
|
181
|
+
".DS_Store",
|
|
182
|
+
"__pycache__",
|
|
183
|
+
".pytest_cache",
|
|
184
|
+
".mypy_cache",
|
|
185
|
+
".next",
|
|
186
|
+
".nuxt",
|
|
187
|
+
"dist",
|
|
188
|
+
"build",
|
|
189
|
+
".venv",
|
|
190
|
+
"venv",
|
|
191
|
+
".env",
|
|
192
|
+
"coverage",
|
|
193
|
+
".nyc_output",
|
|
194
|
+
".turbo",
|
|
195
|
+
".cache",
|
|
196
|
+
]);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function flattenTree(
|
|
200
|
+
node: FileNode,
|
|
201
|
+
depth = 0,
|
|
202
|
+
isRoot = true,
|
|
203
|
+
includeCollapsed = false
|
|
204
|
+
): FlatNode[] {
|
|
205
|
+
const result: FlatNode[] = [];
|
|
206
|
+
|
|
207
|
+
// Skip the root "." node itself, just process its children
|
|
208
|
+
if (isRoot && node.name === ".") {
|
|
209
|
+
for (const child of node.children || []) {
|
|
210
|
+
result.push(...flattenTree(child, 0, false, includeCollapsed));
|
|
211
|
+
}
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
result.push({ node, depth });
|
|
216
|
+
|
|
217
|
+
if (node.isDirectory && node.children && (includeCollapsed || node.expanded)) {
|
|
218
|
+
for (const child of node.children) {
|
|
219
|
+
result.push(...flattenTree(child, depth + 1, false, includeCollapsed));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
import { isGitRepo } from "./git";
|
|
5
|
+
import { hasCommand, stripLeadingEmptyLines } from "./utils";
|
|
6
|
+
|
|
7
|
+
export function loadFileContent(
|
|
8
|
+
filePath: string,
|
|
9
|
+
cwd: string,
|
|
10
|
+
diffMode: boolean,
|
|
11
|
+
hasChanges: boolean,
|
|
12
|
+
width?: number
|
|
13
|
+
): string[] {
|
|
14
|
+
const isMarkdown = filePath.endsWith(".md");
|
|
15
|
+
const termWidth = width || process.stdout.columns || 80;
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
if (diffMode && hasChanges && isGitRepo(cwd)) {
|
|
19
|
+
try {
|
|
20
|
+
// Try different diff strategies
|
|
21
|
+
let diffOutput = "";
|
|
22
|
+
|
|
23
|
+
// First try: unstaged changes
|
|
24
|
+
const unstaged = execSync(`git diff -- "${filePath}"`, { cwd, encoding: "utf-8", timeout: 10000, stdio: "pipe" });
|
|
25
|
+
if (unstaged.trim()) {
|
|
26
|
+
diffOutput = unstaged;
|
|
27
|
+
} else {
|
|
28
|
+
// Second try: staged changes
|
|
29
|
+
const staged = execSync(`git diff --cached -- "${filePath}"`, { cwd, encoding: "utf-8", timeout: 10000, stdio: "pipe" });
|
|
30
|
+
if (staged.trim()) {
|
|
31
|
+
diffOutput = staged;
|
|
32
|
+
} else {
|
|
33
|
+
// Third try: diff against HEAD (for new files that are staged)
|
|
34
|
+
const headDiff = execSync(`git diff HEAD -- "${filePath}"`, { cwd, encoding: "utf-8", timeout: 10000, stdio: "pipe" });
|
|
35
|
+
if (headDiff.trim()) {
|
|
36
|
+
diffOutput = headDiff;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!diffOutput.trim()) {
|
|
42
|
+
return ["No diff available - file may be untracked or unchanged"];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (hasCommand("delta")) {
|
|
46
|
+
// Pipe through delta with line numbers for better readability
|
|
47
|
+
try {
|
|
48
|
+
const deltaOutput = execSync(
|
|
49
|
+
`delta --no-gitconfig --width=${termWidth} --line-numbers`,
|
|
50
|
+
{
|
|
51
|
+
cwd,
|
|
52
|
+
encoding: "utf-8",
|
|
53
|
+
timeout: 10000,
|
|
54
|
+
input: diffOutput,
|
|
55
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
return stripLeadingEmptyLines(deltaOutput.split("\n"));
|
|
59
|
+
} catch {
|
|
60
|
+
// Fall back to raw diff
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return diffOutput.split("\n");
|
|
65
|
+
} catch (e: any) {
|
|
66
|
+
return [`Diff error: ${e.message}`];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (isMarkdown && hasCommand("glow")) {
|
|
71
|
+
try {
|
|
72
|
+
const output = execSync(`glow -s dark -w ${termWidth} "${filePath}"`, { encoding: "utf-8", timeout: 10000 });
|
|
73
|
+
if (output.trim()) {
|
|
74
|
+
return stripLeadingEmptyLines(output.split("\n"));
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
// Fall through to bat
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (hasCommand("bat")) {
|
|
82
|
+
return execSync(
|
|
83
|
+
`bat --style=numbers --color=always --paging=never --wrap=auto --terminal-width=${termWidth} "${filePath}"`,
|
|
84
|
+
{ encoding: "utf-8", timeout: 10000 }
|
|
85
|
+
).split("\n");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
89
|
+
return raw.split("\n").map((line, i) => `${String(i + 1).padStart(4)} │ ${line}`);
|
|
90
|
+
} catch (e: any) {
|
|
91
|
+
return [`Error loading file: ${e.message}`];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
import type { DiffStats } from "./types";
|
|
4
|
+
|
|
5
|
+
export function isGitRepo(cwd: string): boolean {
|
|
6
|
+
try {
|
|
7
|
+
execSync("git rev-parse --is-inside-work-tree", { cwd, encoding: "utf-8", timeout: 2000, stdio: "pipe" });
|
|
8
|
+
return true;
|
|
9
|
+
} catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getGitStatus(cwd: string, options: { includeIgnored?: boolean } = {}): Map<string, string> {
|
|
15
|
+
const status = new Map<string, string>();
|
|
16
|
+
try {
|
|
17
|
+
const flags = ["--porcelain"];
|
|
18
|
+
if (options.includeIgnored !== false) {
|
|
19
|
+
flags.push("--ignored");
|
|
20
|
+
}
|
|
21
|
+
const output = execSync(`git status ${flags.join(" ")}`, { cwd, encoding: "utf-8", timeout: 5000, stdio: "pipe" });
|
|
22
|
+
for (const line of output.split("\n")) {
|
|
23
|
+
if (line.length < 3) continue;
|
|
24
|
+
const statusCode = line.slice(0, 2).trim() || "?";
|
|
25
|
+
const filePath = line.slice(3);
|
|
26
|
+
status.set(filePath, statusCode);
|
|
27
|
+
}
|
|
28
|
+
} catch {}
|
|
29
|
+
return status;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getGitFileList(cwd: string): string[] {
|
|
33
|
+
const files = new Set<string>();
|
|
34
|
+
try {
|
|
35
|
+
const tracked = execSync("git ls-files -z", { cwd, encoding: "utf-8", timeout: 5000, stdio: "pipe" });
|
|
36
|
+
for (const entry of tracked.split("\0")) {
|
|
37
|
+
if (entry) files.add(entry);
|
|
38
|
+
}
|
|
39
|
+
} catch {}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const statusOutput = execSync("git status --porcelain -uall -z", { cwd, encoding: "utf-8", timeout: 5000, stdio: "pipe" });
|
|
43
|
+
const entries = statusOutput.split("\0");
|
|
44
|
+
for (let i = 0; i < entries.length; i++) {
|
|
45
|
+
const entry = entries[i];
|
|
46
|
+
if (!entry) continue;
|
|
47
|
+
const statusCode = entry.slice(0, 2).trim();
|
|
48
|
+
let filePath = entry.slice(3);
|
|
49
|
+
if ((statusCode.startsWith("R") || statusCode.startsWith("C")) && entries[i + 1]) {
|
|
50
|
+
i += 1;
|
|
51
|
+
filePath = entries[i];
|
|
52
|
+
} else if (filePath.includes(" -> ")) {
|
|
53
|
+
filePath = filePath.split(" -> ").pop() || filePath;
|
|
54
|
+
}
|
|
55
|
+
if (filePath) {
|
|
56
|
+
files.add(filePath);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
} catch {}
|
|
60
|
+
|
|
61
|
+
return Array.from(files);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function getGitBranch(cwd: string): string {
|
|
65
|
+
try {
|
|
66
|
+
return execSync("git branch --show-current", { cwd, encoding: "utf-8", timeout: 2000, stdio: "pipe" }).trim();
|
|
67
|
+
} catch {
|
|
68
|
+
return "";
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function getGitDiffStats(cwd: string): Map<string, DiffStats> {
|
|
73
|
+
const stats = new Map<string, DiffStats>();
|
|
74
|
+
try {
|
|
75
|
+
// Get diff stats for modified files
|
|
76
|
+
const output = execSync("git diff --numstat HEAD", { cwd, encoding: "utf-8", timeout: 5000, stdio: "pipe" });
|
|
77
|
+
for (const line of output.split("\n")) {
|
|
78
|
+
const parts = line.split("\t");
|
|
79
|
+
if (parts.length >= 3) {
|
|
80
|
+
const additions = parseInt(parts[0], 10) || 0;
|
|
81
|
+
const deletions = parseInt(parts[1], 10) || 0;
|
|
82
|
+
const filePath = parts[2];
|
|
83
|
+
stats.set(filePath, { additions, deletions });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Also get stats for staged files
|
|
87
|
+
const stagedOutput = execSync("git diff --numstat --cached", { cwd, encoding: "utf-8", timeout: 5000, stdio: "pipe" });
|
|
88
|
+
for (const line of stagedOutput.split("\n")) {
|
|
89
|
+
const parts = line.split("\t");
|
|
90
|
+
if (parts.length >= 3) {
|
|
91
|
+
const additions = parseInt(parts[0], 10) || 0;
|
|
92
|
+
const deletions = parseInt(parts[1], 10) || 0;
|
|
93
|
+
const filePath = parts[2];
|
|
94
|
+
const existing = stats.get(filePath);
|
|
95
|
+
if (existing) {
|
|
96
|
+
stats.set(filePath, {
|
|
97
|
+
additions: existing.additions + additions,
|
|
98
|
+
deletions: existing.deletions + deletions,
|
|
99
|
+
});
|
|
100
|
+
} else {
|
|
101
|
+
stats.set(filePath, { additions, deletions });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} catch {}
|
|
106
|
+
return stats;
|
|
107
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi Editor Extension
|
|
3
|
+
*
|
|
4
|
+
* Provides an in-terminal file browser, viewer, and review workflow.
|
|
5
|
+
* Use /files to open the file browser, navigate with j/k, Enter to view.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
9
|
+
import { execSync, spawnSync } from "node:child_process";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
|
|
12
|
+
import { createFileBrowser } from "./browser";
|
|
13
|
+
import { POLL_INTERVAL_MS } from "./constants";
|
|
14
|
+
import { formatCommentMessage } from "./comment";
|
|
15
|
+
import { hasCommand } from "./utils";
|
|
16
|
+
|
|
17
|
+
export default function editorExtension(pi: ExtensionAPI): void {
|
|
18
|
+
const cwd = process.cwd();
|
|
19
|
+
const agentModifiedFiles = new Set<string>();
|
|
20
|
+
|
|
21
|
+
pi.registerCommand("files", {
|
|
22
|
+
description: "Open file browser",
|
|
23
|
+
handler: async (_args, ctx) => {
|
|
24
|
+
await ctx.ui.custom<void>((tui, theme, _kb, done) => {
|
|
25
|
+
let pollInterval: ReturnType<typeof setInterval> | null = null;
|
|
26
|
+
|
|
27
|
+
const cleanup = () => {
|
|
28
|
+
if (pollInterval) {
|
|
29
|
+
clearInterval(pollInterval);
|
|
30
|
+
pollInterval = null;
|
|
31
|
+
}
|
|
32
|
+
done();
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const requestComment = (payload: { relPath: string; lineRange: string; ext: string; selectedText: string }, comment: string) => {
|
|
36
|
+
pi.sendUserMessage(formatCommentMessage(payload, comment), {
|
|
37
|
+
deliverAs: "followUp",
|
|
38
|
+
streamingBehavior: "followUp" as any,
|
|
39
|
+
} as any);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const requestRender = () => tui.requestRender();
|
|
43
|
+
const browser = createFileBrowser(cwd, agentModifiedFiles, theme, cleanup, requestComment, requestRender);
|
|
44
|
+
|
|
45
|
+
pollInterval = setInterval(() => {
|
|
46
|
+
requestRender();
|
|
47
|
+
}, POLL_INTERVAL_MS);
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
render: (w) => browser.render(w),
|
|
51
|
+
handleInput: (data) => {
|
|
52
|
+
browser.handleInput(data);
|
|
53
|
+
requestRender();
|
|
54
|
+
},
|
|
55
|
+
invalidate: () => browser.invalidate(),
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
pi.registerCommand("review", {
|
|
62
|
+
description: "Open tuicr to review changes and send feedback to agent",
|
|
63
|
+
handler: async (_args, ctx) => {
|
|
64
|
+
if (!hasCommand("tuicr")) {
|
|
65
|
+
ctx.ui.notify("Install tuicr: brew install agavra/tap/tuicr", "error");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
ctx.ui.notify("Opening tuicr... Press :wq or y to copy review", "info");
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
spawnSync("tuicr", [], { cwd, stdio: "inherit" });
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const clipboard = execSync(
|
|
76
|
+
process.platform === "darwin" ? "pbpaste" : "xclip -selection clipboard -o",
|
|
77
|
+
{ encoding: "utf-8", timeout: 5000 }
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
if (
|
|
81
|
+
clipboard.includes("## Review") ||
|
|
82
|
+
clipboard.includes("```") ||
|
|
83
|
+
clipboard.includes("[Issue]") ||
|
|
84
|
+
clipboard.includes("[Suggestion]")
|
|
85
|
+
) {
|
|
86
|
+
pi.sendUserMessage(clipboard, { deliverAs: "steer" });
|
|
87
|
+
ctx.ui.notify("Review sent to agent", "success");
|
|
88
|
+
}
|
|
89
|
+
} catch {}
|
|
90
|
+
} catch (e: any) {
|
|
91
|
+
ctx.ui.notify(`tuicr error: ${e.message}`, "error");
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
pi.registerCommand("diff", {
|
|
97
|
+
description: "Open critique to view diffs",
|
|
98
|
+
handler: async (args, ctx) => {
|
|
99
|
+
if (!hasCommand("bun")) {
|
|
100
|
+
ctx.ui.notify("critique requires Bun: brew install oven-sh/bun/bun", "error");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const critiqueArgs = args ? args.split(" ") : [];
|
|
105
|
+
ctx.ui.notify("Opening critique...", "info");
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
spawnSync("bunx", ["critique", ...critiqueArgs], { cwd, stdio: "inherit" });
|
|
109
|
+
} catch (e: any) {
|
|
110
|
+
ctx.ui.notify(`critique error: ${e.message}`, "error");
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
pi.on("tool_result", async (event) => {
|
|
116
|
+
if (event.toolName === "write" || event.toolName === "edit") {
|
|
117
|
+
const filePath = event.input?.path as string | undefined;
|
|
118
|
+
if (filePath) {
|
|
119
|
+
agentModifiedFiles.add(join(cwd, filePath));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
125
|
+
const missing: string[] = [];
|
|
126
|
+
if (!hasCommand("bat")) missing.push("bat");
|
|
127
|
+
if (!hasCommand("delta")) missing.push("delta");
|
|
128
|
+
if (!hasCommand("glow")) missing.push("glow");
|
|
129
|
+
|
|
130
|
+
if (missing.length > 0) {
|
|
131
|
+
ctx.ui.notify(`Editor: install ${missing.join(", ")} for better experience`, "info");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
agentModifiedFiles.clear();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
pi.on("session_switch", async () => {
|
|
138
|
+
agentModifiedFiles.clear();
|
|
139
|
+
});
|
|
140
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tmustier/pi-files-widget",
|
|
3
|
+
"version": "0.1.9",
|
|
4
|
+
"description": "In-terminal file browser and viewer for Pi.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Thomas Mustier",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"pi-package"
|
|
9
|
+
],
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/tmustier/pi-extensions.git",
|
|
13
|
+
"directory": "files-widget"
|
|
14
|
+
},
|
|
15
|
+
"bugs": "https://github.com/tmustier/pi-extensions/issues",
|
|
16
|
+
"homepage": "https://github.com/tmustier/pi-extensions/tree/main/files-widget",
|
|
17
|
+
"pi": {
|
|
18
|
+
"extensions": [
|
|
19
|
+
"index.ts"
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface DiffStats {
|
|
2
|
+
additions: number;
|
|
3
|
+
deletions: number;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface FileNode {
|
|
7
|
+
name: string;
|
|
8
|
+
path: string;
|
|
9
|
+
isDirectory: boolean;
|
|
10
|
+
children?: FileNode[];
|
|
11
|
+
expanded?: boolean;
|
|
12
|
+
gitStatus?: string;
|
|
13
|
+
agentModified?: boolean;
|
|
14
|
+
lineCount?: number;
|
|
15
|
+
diffStats?: DiffStats;
|
|
16
|
+
hasChangedChildren?: boolean; // For directories
|
|
17
|
+
// Aggregated stats for directories
|
|
18
|
+
totalLines?: number;
|
|
19
|
+
totalAdditions?: number;
|
|
20
|
+
totalDeletions?: number;
|
|
21
|
+
lineCountComplete?: boolean;
|
|
22
|
+
loading?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface FlatNode {
|
|
26
|
+
node: FileNode;
|
|
27
|
+
depth: number;
|
|
28
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export function hasCommand(cmd: string): boolean {
|
|
4
|
+
try {
|
|
5
|
+
execSync(`which ${cmd}`, { stdio: "ignore" });
|
|
6
|
+
return true;
|
|
7
|
+
} catch {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function isUntrackedStatus(status?: string): boolean {
|
|
13
|
+
return status === "?" || status === "??";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function isIgnoredStatus(status?: string): boolean {
|
|
17
|
+
return status === "!" || status === "!!";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function stripLeadingEmptyLines(lines: string[]): string[] {
|
|
21
|
+
let startIdx = 0;
|
|
22
|
+
while (startIdx < lines.length && !lines[startIdx].trim()) {
|
|
23
|
+
startIdx++;
|
|
24
|
+
}
|
|
25
|
+
return lines.slice(startIdx);
|
|
26
|
+
}
|