pi-extensions 0.1.30 → 0.1.32

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 CHANGED
@@ -8,7 +8,7 @@ Personal extensions for the [Pi coding agent](https://github.com/badlogic/pi-mon
8
8
  |-----------|-------------|
9
9
  | [/readfiles](files-widget/) | In-terminal file browser and viewer widget. Navigate files, view diffs, select code, send comments to agent - without leaving Pi, and without interrupting your agent |
10
10
  | [tab-status](tab-status/) | Manage as many parallel sessions as your mind can handle. Terminal tab indicators for <br>✅ done / 🚧 stuck / 🛑 timed out |
11
- | [ralph-wiggum](ralph-wiggum/) | Run arbitrarily-long tasks without diluting model attention. Flat version without subagents like [ralph-loop](https://github.com/anthropics/claude-plugins-official/tree/main/plugins/ralph-loop) |
11
+ | [pi-ralph-wiggum](pi-ralph-wiggum/) | Run arbitrarily-long tasks without diluting model attention. Flat version without subagents like [ralph-loop](https://github.com/anthropics/claude-plugins-official/tree/main/plugins/ralph-loop) |
12
12
  | [agent-guidance](agent-guidance/) | Switch between Claude/Codex/Gemini with model-specific guidance (CLAUDE.md, CODEX.md, GEMINI.md) |
13
13
  | [/usage](usage-extension/) | 📊 Usage statistics dashboard. See cost, tokens, and messages by provider/model across Today, This Week, Last Week, and All Time — with a compact view for narrow terminals |
14
14
  | [/paste](raw-paste/) | Paste editable text, not [paste #1 +21 lines]. Running `/paste` with optional keybinding |
@@ -21,7 +21,7 @@ Personal extensions for the [Pi coding agent](https://github.com/badlogic/pi-mon
21
21
  |-------|-------------|
22
22
  | [extending-pi](extending-pi/) | Guide for extending Pi — decide between skills, extensions, prompt templates, themes, or packages. |
23
23
  | ↳ [skill-creator](extending-pi/skill-creator/) | Detailed guidance for creating Pi skills. |
24
- | [ralph-wiggum](ralph-wiggum/) | Skill instructions for long-running development loops. |
24
+ | [pi-ralph-wiggum](pi-ralph-wiggum/) | Skill instructions for long-running development loops. |
25
25
 
26
26
  ## Install (pi package manager)
27
27
 
@@ -56,7 +56,7 @@ If you keep a local clone, add extensions to your `~/.pi/agent/settings.json`:
56
56
  "~/pi-extensions/arcade/picman.ts",
57
57
  "~/pi-extensions/arcade/tetris.ts",
58
58
  "~/pi-extensions/arcade/mario-not/mario-not.ts",
59
- "~/pi-extensions/ralph-wiggum",
59
+ "~/pi-extensions/pi-ralph-wiggum",
60
60
  "~/pi-extensions/agent-guidance/agent-guidance.ts",
61
61
  "~/pi-extensions/raw-paste",
62
62
  "~/pi-extensions/code-actions",
@@ -2,6 +2,11 @@
2
2
 
3
3
  All notable changes to this extension will be documented in this file.
4
4
 
5
+ ## [Unreleased]
6
+
7
+ ### Changed
8
+ - Make the inline comment editor multiline with wrapped footer rendering, `Enter` for a new line, and `Ctrl+Enter`/`Ctrl+D` to send.
9
+
5
10
  ## [0.1.16] - 2026-04-19
6
11
 
7
12
  ### Fixed
@@ -117,6 +117,8 @@ If missing, `/review` or `/diff` will show a clear install prompt.
117
117
  - `n` / `N`: next/prev match
118
118
  - `v`: select mode (line selection)
119
119
  - `c`: comment on selected lines (inline prompt)
120
+ - `Enter`: new line in the comment editor
121
+ - `Ctrl+Enter` or `Ctrl+D`: send the comment (`Alt+Enter` also works when supported)
120
122
  - `]` / `[`: next/prev changed file
121
123
  - `+` / `-`: increase/decrease viewer height
122
124
  - `q`: back to browser
@@ -66,8 +66,8 @@
66
66
 
67
67
  ### Comment Dialog
68
68
  - [x] `c` to open comment input
69
- - [ ] Multi-line text input
70
- - [x] `Enter` or `Ctrl+Enter` to confirm
69
+ - [x] Multi-line text input
70
+ - [x] `Enter` for newline and `Ctrl+Enter` / `Ctrl+D` to confirm
71
71
  - [x] `Esc` to cancel
72
72
 
73
73
  ### Send to Agent
@@ -4,17 +4,22 @@ const CONTROL_CHARS = /[\u0000-\u0008\u000B-\u001F\u007F]/g;
4
4
  const BRACKETED_PASTE_START = "\u001b[200~";
5
5
  const BRACKETED_PASTE_END = "\u001b[201~";
6
6
 
7
- function sanitizeTextInput(data: string): string {
7
+ interface SanitizeTextInputOptions {
8
+ preserveNewlines?: boolean;
9
+ }
10
+
11
+ function sanitizeTextInput(data: string, options: SanitizeTextInputOptions = {}): string {
8
12
  const normalized = decodeKittyPrintable(data) ?? data;
9
13
  if (!normalized || normalized.includes("\u001b")) {
10
14
  return "";
11
15
  }
12
16
 
13
- return normalized
14
- .replace(/\r\n?/g, "\n")
15
- .replace(/\n/g, " ")
16
- .replace(/\t/g, " ")
17
- .replace(CONTROL_CHARS, "");
17
+ const withNewlines = normalized.replace(/\r\n?/g, "\n");
18
+ const withoutTabs = options.preserveNewlines
19
+ ? withNewlines.replace(/\t/g, " ")
20
+ : withNewlines.replace(/\n/g, " ").replace(/\t/g, " ");
21
+
22
+ return withoutTabs.replace(CONTROL_CHARS, "");
18
23
  }
19
24
 
20
25
  function getPendingStartSuffix(data: string): string {
@@ -33,7 +38,11 @@ export interface TextInputBuffer {
33
38
  reset(): void;
34
39
  }
35
40
 
36
- export function createTextInputBuffer(): TextInputBuffer {
41
+ interface TextInputBufferOptions {
42
+ preserveNewlines?: boolean;
43
+ }
44
+
45
+ export function createTextInputBuffer(options: TextInputBufferOptions = {}): TextInputBuffer {
37
46
  let isInPaste = false;
38
47
  let pasteBuffer = "";
39
48
  let pendingStart = "";
@@ -52,14 +61,14 @@ export function createTextInputBuffer(): TextInputBuffer {
52
61
  const pendingSuffix = getPendingStartSuffix(combined);
53
62
  const completeText = pendingSuffix ? combined.slice(0, combined.length - pendingSuffix.length) : combined;
54
63
  pendingStart = pendingSuffix;
55
- return sanitizeTextInput(completeText);
64
+ return sanitizeTextInput(completeText, options);
56
65
  }
57
66
 
58
67
  const beforePaste = combined.slice(0, startIndex);
59
68
  const afterStart = combined.slice(startIndex + BRACKETED_PASTE_START.length);
60
69
  isInPaste = true;
61
70
  pasteBuffer = "";
62
- return sanitizeTextInput(beforePaste) + push(afterStart);
71
+ return sanitizeTextInput(beforePaste, options) + push(afterStart);
63
72
  }
64
73
 
65
74
  pasteBuffer += combined;
@@ -73,7 +82,7 @@ export function createTextInputBuffer(): TextInputBuffer {
73
82
  isInPaste = false;
74
83
  pasteBuffer = "";
75
84
 
76
- return sanitizeTextInput(pastedText) + push(remaining);
85
+ return sanitizeTextInput(pastedText, options) + push(remaining);
77
86
  };
78
87
 
79
88
  return {
@@ -1,5 +1,5 @@
1
1
  import type { Theme } from "@mariozechner/pi-coding-agent";
2
- import { Key, matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
2
+ import { Key, matchesKey, truncateToWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
3
3
  import { readFileSync, statSync } from "node:fs";
4
4
  import { relative } from "node:path";
5
5
 
@@ -14,6 +14,8 @@ import type { FileNode } from "./types";
14
14
  import { isUntrackedStatus } from "./utils";
15
15
  import { createTextInputBuffer } from "./input-utils";
16
16
 
17
+ const COMMENT_EDITOR_MAX_VISIBLE_LINES = 4;
18
+
17
19
  export interface CommentPayload {
18
20
  relPath: string;
19
21
  lineRange: string;
@@ -61,7 +63,8 @@ export function createViewer(
61
63
  theme: Theme,
62
64
  requestComment: (payload: CommentPayload, comment: string) => void
63
65
  ): ViewerController {
64
- const textInput = createTextInputBuffer();
66
+ const searchInput = createTextInputBuffer();
67
+ const commentInput = createTextInputBuffer({ preserveNewlines: true });
65
68
 
66
69
  const state: ViewerState = {
67
70
  file: null,
@@ -98,7 +101,8 @@ export function createViewer(
98
101
 
99
102
  function setMode(mode: ViewerMode): void {
100
103
  if (mode !== state.mode) {
101
- textInput.reset();
104
+ searchInput.reset();
105
+ commentInput.reset();
102
106
  }
103
107
 
104
108
  state.mode = mode;
@@ -264,6 +268,38 @@ export function createViewer(
264
268
  return truncateToWidth(header, width);
265
269
  }
266
270
 
271
+ function renderCommentEditor(width: number): string[] {
272
+ const contentWidth = Math.max(1, width - 2);
273
+ const wrappedLines: string[] = [];
274
+ const logicalLines = state.commentText.split("\n");
275
+
276
+ for (const line of logicalLines) {
277
+ if (line.length === 0) {
278
+ wrappedLines.push("");
279
+ continue;
280
+ }
281
+ wrappedLines.push(...wrapTextWithAnsi(line, contentWidth));
282
+ }
283
+
284
+ if (wrappedLines.length === 0) {
285
+ wrappedLines.push("");
286
+ }
287
+
288
+ const lastIndex = wrappedLines.length - 1;
289
+ wrappedLines[lastIndex] = `${wrappedLines[lastIndex]}█`;
290
+
291
+ const overflow = Math.max(0, wrappedLines.length - COMMENT_EDITOR_MAX_VISIBLE_LINES);
292
+ const visibleLines = wrappedLines.slice(-COMMENT_EDITOR_MAX_VISIBLE_LINES);
293
+ if (overflow > 0 && visibleLines.length > 0) {
294
+ visibleLines[0] = `…${visibleLines[0]}`;
295
+ }
296
+
297
+ return [
298
+ truncateToWidth(theme.fg("accent", "Comment:"), width),
299
+ ...visibleLines.map(line => truncateToWidth(` ${line}`, width)),
300
+ ];
301
+ }
302
+
267
303
  function renderFooter(width: number): string[] {
268
304
  const lines: string[] = [];
269
305
  const pct = state.content.length > 0
@@ -271,14 +307,13 @@ export function createViewer(
271
307
  : 0;
272
308
 
273
309
  if (state.mode === "comment") {
274
- const prompt = theme.fg("accent", `Comment: ${state.commentText}█`);
275
- lines.push(truncateToWidth(prompt, width));
310
+ lines.push(...renderCommentEditor(width));
276
311
  lines.push(theme.fg("borderMuted", "─".repeat(width)));
277
312
  }
278
313
 
279
314
  let help: string;
280
315
  if (state.mode === "comment") {
281
- help = theme.fg("dim", "Enter: send Esc: cancel");
316
+ help = theme.fg("dim", "Enter: newline Ctrl+Enter/Ctrl+D: send Esc: cancel");
282
317
  } else if (state.mode === "select") {
283
318
  help = theme.fg("dim", "j/k: extend c: comment Esc: cancel");
284
319
  } else if (state.mode === "search") {
@@ -363,19 +398,21 @@ export function createViewer(
363
398
  if (!state.file) return { type: "none" };
364
399
 
365
400
  if (state.mode === "comment") {
366
- if (matchesKey(data, Key.enter)) {
401
+ if (matchesKey(data, "ctrl+enter") || matchesKey(data, "ctrl+d") || matchesKey(data, "alt+enter")) {
367
402
  const comment = state.commentText.trim();
368
403
  if (comment) {
369
404
  sendComment(comment);
370
405
  } else {
371
406
  setMode("normal");
372
407
  }
408
+ } else if (matchesKey(data, Key.enter) || matchesKey(data, "shift+enter")) {
409
+ state.commentText += "\n";
373
410
  } else if (matchesKey(data, Key.escape)) {
374
411
  setMode("normal");
375
412
  } else if (matchesKey(data, Key.backspace)) {
376
413
  state.commentText = state.commentText.slice(0, -1);
377
414
  } else {
378
- const text = textInput.push(data);
415
+ const text = commentInput.push(data);
379
416
  if (text) {
380
417
  state.commentText += text;
381
418
  }
@@ -392,7 +429,7 @@ export function createViewer(
392
429
  state.searchQuery = state.searchQuery.slice(0, -1);
393
430
  updateSearchMatches();
394
431
  } else {
395
- const text = textInput.push(data);
432
+ const text = searchInput.push(data);
396
433
  if (text) {
397
434
  state.searchQuery += text;
398
435
  updateSearchMatches();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-extensions",
3
- "version": "0.1.30",
3
+ "version": "0.1.32",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "keywords": [
@@ -17,14 +17,14 @@
17
17
  "./code-actions/index.ts",
18
18
  "./files-widget/index.ts",
19
19
  "./raw-paste/index.ts",
20
- "./ralph-wiggum/index.ts",
20
+ "./pi-ralph-wiggum/index.ts",
21
21
  "./tab-status/tab-status.ts",
22
22
  "./usage-extension/index.ts"
23
23
  ],
24
24
  "skills": [
25
25
  "./extending-pi/SKILL.md",
26
26
  "./extending-pi/skill-creator/SKILL.md",
27
- "./ralph-wiggum/SKILL.md"
27
+ "./pi-ralph-wiggum/SKILL.md"
28
28
  ]
29
29
  }
30
30
  }
@@ -0,0 +1,33 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0 - 2026-04-19
4
+
5
+ ### Changed
6
+ - **BREAKING:** SKILL.md `name` renamed `ralph-wiggum` → `pi-ralph-wiggum` to match the parent directory (both in the repo and after `pi install npm:@tmustier/pi-ralph-wiggum`). This removes the `[Skill conflicts]` warning pi emitted on every startup, but it also changes the skill's public identifier — explicit invocations must now use `/skill:pi-ralph-wiggum` instead of `/skill:ralph-wiggum`. Thanks to @ishanmalik for reporting ([#12](https://github.com/tmustier/pi-extensions/issues/12)).
7
+ - Repo directory renamed `ralph-wiggum/` → `pi-ralph-wiggum/` as part of the same fix. Git-source users referencing `~/pi-extensions/ralph-wiggum/…` in their pi config should update the path to `~/pi-extensions/pi-ralph-wiggum/…`. The npm package name (`@tmustier/pi-ralph-wiggum`) is unchanged.
8
+ - Renamed the README's `Install` section to `Installation` so it matches the skill validator's expectations.
9
+
10
+ ## 0.1.7 - 2026-04-19
11
+
12
+ ### Fixed
13
+ - Ralph loops no longer silently stop after auto-compaction or `/compact`. On session reload, `currentLoop` is now rehydrated from the on-disk state (most-recently-updated active loop wins on ties), so `ralph_done`, `agent_end`, and `before_agent_start` continue to function. Thanks to @elecnix for the detailed report and proposed fix ([#11](https://github.com/tmustier/pi-extensions/issues/11)).
14
+
15
+ ## 0.1.5 - 2026-02-03
16
+
17
+ ### Added
18
+ - Add preview image metadata for the extension listing.
19
+
20
+ ## 0.1.4 - 2026-02-02
21
+
22
+ ### Changed
23
+ - **BREAKING:** Updated tool execute signatures for Pi v0.51.0 compatibility (`signal` parameter now comes before `onUpdate`)
24
+ - **BREAKING:** Changed `before_agent_start` handler to use `systemPrompt` instead of deprecated `systemPromptAppend` (Pi v0.39.0+)
25
+
26
+ ## 0.1.3 - 2026-01-26
27
+ - Added note clarifying this is a flat version without subagents.
28
+
29
+ ## 0.1.1 - 2026-01-25
30
+ - Clarified that agents must write the task file themselves (tool does not auto-create it).
31
+
32
+ ## 0.1.0 - 2026-01-13
33
+ - Initial release.
@@ -10,7 +10,7 @@ This one is cool because:
10
10
 
11
11
  **Note: This is a flat version without subagents, similar to the [Anthropic plugins implementation](https://github.com/anthropics/claude-code-plugins/tree/main/ralph-loop).**
12
12
 
13
- ## Install
13
+ ## Installation
14
14
 
15
15
  ```bash
16
16
  pi install npm:@tmustier/pi-ralph-wiggum
@@ -27,8 +27,8 @@ Then filter to just this extension in `~/.pi/agent/settings.json`:
27
27
  "packages": [
28
28
  {
29
29
  "source": "git:github.com/tmustier/pi-extensions",
30
- "extensions": ["ralph-wiggum/index.ts"],
31
- "skills": ["ralph-wiggum/SKILL.md"]
30
+ "extensions": ["pi-ralph-wiggum/index.ts"],
31
+ "skills": ["pi-ralph-wiggum/SKILL.md"]
32
32
  }
33
33
  ]
34
34
  }
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: ralph-wiggum
2
+ name: pi-ralph-wiggum
3
3
  description: Long-running iterative development loops with pacing control and verifiable progress. Use when tasks require multiple iterations, many discrete steps, or periodic reflection with clear checkpoints; avoid for simple one-shot tasks or quick fixes.
4
4
  ---
5
5
 
@@ -92,6 +92,14 @@ export default function (pi: ExtensionAPI) {
92
92
  }
93
93
  }
94
94
 
95
+ function safeMtimeMs(filePath: string): number {
96
+ try {
97
+ return fs.statSync(filePath).mtimeMs;
98
+ } catch {
99
+ return 0;
100
+ }
101
+ }
102
+
95
103
  function tryRemoveDir(dirPath: string): boolean {
96
104
  try {
97
105
  if (fs.existsSync(dirPath)) {
@@ -784,6 +792,21 @@ Examples:
784
792
 
785
793
  pi.on("session_start", async (_event, ctx) => {
786
794
  const active = listLoops(ctx).filter((l) => l.status === "active");
795
+
796
+ // Rehydrate currentLoop from disk. The module is re-initialized on
797
+ // session reload (including auto-compaction and /compact), which would
798
+ // otherwise leave `currentLoop` null and silently break ralph_done,
799
+ // agent_end, and before_agent_start. Pick the most-recently-updated
800
+ // active loop when there are multiple, using the state file mtime.
801
+ if (!currentLoop && active.length > 0) {
802
+ const mostRecent = active.reduce((best, candidate) => {
803
+ const bestMtime = safeMtimeMs(getPath(ctx, best.name, ".state.json"));
804
+ const candidateMtime = safeMtimeMs(getPath(ctx, candidate.name, ".state.json"));
805
+ return candidateMtime > bestMtime ? candidate : best;
806
+ });
807
+ currentLoop = mostRecent.name;
808
+ }
809
+
787
810
  if (active.length > 0 && ctx.hasUI) {
788
811
  const lines = active.map(
789
812
  (l) => ` • ${l.name} (iteration ${l.iteration}${l.maxIterations > 0 ? `/${l.maxIterations}` : ""})`,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tmustier/pi-ralph-wiggum",
3
- "version": "0.1.6",
3
+ "version": "0.2.0",
4
4
  "description": "Long-running agent loops for iterative development in Pi.",
5
5
  "license": "MIT",
6
6
  "author": "Thomas Mustier",
@@ -10,10 +10,10 @@
10
10
  "repository": {
11
11
  "type": "git",
12
12
  "url": "git+https://github.com/tmustier/pi-extensions.git",
13
- "directory": "ralph-wiggum"
13
+ "directory": "pi-ralph-wiggum"
14
14
  },
15
15
  "bugs": "https://github.com/tmustier/pi-extensions/issues",
16
- "homepage": "https://github.com/tmustier/pi-extensions/tree/main/ralph-wiggum",
16
+ "homepage": "https://github.com/tmustier/pi-extensions/tree/main/pi-ralph-wiggum",
17
17
  "pi": {
18
18
  "extensions": [
19
19
  "index.ts"
@@ -1,21 +0,0 @@
1
- # Changelog
2
-
3
- ## 0.1.5 - 2026-02-03
4
-
5
- ### Added
6
- - Add preview image metadata for the extension listing.
7
-
8
- ## 0.1.4 - 2026-02-02
9
-
10
- ### Changed
11
- - **BREAKING:** Updated tool execute signatures for Pi v0.51.0 compatibility (`signal` parameter now comes before `onUpdate`)
12
- - **BREAKING:** Changed `before_agent_start` handler to use `systemPrompt` instead of deprecated `systemPromptAppend` (Pi v0.39.0+)
13
-
14
- ## 0.1.3 - 2026-01-26
15
- - Added note clarifying this is a flat version without subagents.
16
-
17
- ## 0.1.1 - 2026-01-25
18
- - Clarified that agents must write the task file themselves (tool does not auto-create it).
19
-
20
- ## 0.1.0 - 2026-01-13
21
- - Initial release.
File without changes