pi-git-commit 1.0.5 → 1.0.7

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.
Files changed (3) hide show
  1. package/README.md +10 -6
  2. package/index.ts +69 -5
  3. package/package.json +4 -2
package/README.md CHANGED
@@ -1,12 +1,13 @@
1
1
  # pi-git-commit
2
2
 
3
- Keeps mutative git operations out of the agent's bash and provides a safe, reviewable commit flow in [pi-coding-agent](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent): a bash guard, a `git_commit` tool, and `/commit` + `/toggle-allow-git` commands.
3
+ Keeps mutative git operations out of the agent's bash and provides a safe, reviewable commit flow in [pi-coding-agent](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent): a bash guard, a `git_commit` tool, and `/commit`, `/stop-commit` and `/toggle-allow-git` commands.
4
4
 
5
5
  ## What you get
6
6
 
7
7
  - **Bash git guard.** Mutative git commands are blocked in the agent's bash tool — `add`, `stage`, `commit`, `push`, `pull`, `merge`, `rebase`, `reset`, `clean`, `rm`, `restore`, `switch`, `cherry-pick`, `revert`, `mv`, `init`, `clone`, index/object plumbing (`read-tree`, `checkout-index`, `merge-file`, `prune-packed`), plus mutative forms of `branch` (including creation, `-u`, `-f`, `-c`/`-C`/`--copy`, `-D`/`-M`, `--force`, `-t`/`--track`), `tag` (including creation), `checkout` (including whole-tree restores like `checkout -- .`), `stash`, `submodule`, `worktree`, `config`, `remote`, `apply`, `notes`, `update-ref`, `gc` and more. Read-only commands (`status`, `diff`, `log`, `fetch`, `branch`, `tag`, `stash list`, ...) stay allowed.
8
- - **`git_commit` tool.** The agent stages everything and commits with a `FIX` / `IMPROVE` / `NEW` type prefix. Inactive by default: `/commit` activates it for the commit flow and it is disabled again after use, so the agent cannot commit on its own at other times.
9
- - **`/commit` command.** Waits for queued messages to finish, stages all changes, shows the staged diff, activates the `git_commit` tool, and asks the agent to review it and commit via `git_commit` — never via bash.
8
+ - **`git_commit` tool.** The agent stages everything and commits with a `FIX` / `IMPROVE` / `NEW` type prefix. Inactive by default: `/commit` activates it for the commit flow and it is disabled again after a successful commit, so the agent cannot commit on its own at other times.
9
+ - **`/commit` command.** Waits for queued messages to finish, stages all changes, shows a collapsed summary of the staged diff (a `git diff --stat` line; press `ctrl+o` to expand the full diff), activates the `git_commit` tool, and asks the agent to review the changes and commit via `git_commit` — never via bash. Run `/stop-commit` at any point to abort the flow.
10
+ - **`/stop-commit` command.** Aborts a pending commit flow: deactivates the `git_commit` tool and cancels a `/commit` that is still waiting for queued messages, so no commit is made.
10
11
  - **`/toggle-allow-git` command.** Temporarily allows mutative git commands in bash for the current session. The guard re-arms on the next session.
11
12
 
12
13
  ## Quick start
@@ -17,7 +18,7 @@ Keeps mutative git operations out of the agent's bash and provides a safe, revie
17
18
  /commit
18
19
  ```
19
20
 
20
- 2. The extension stages the working tree and hands the staged diff to the agent with instructions to review it.
21
+ 2. The extension stages the working tree and shows a collapsed summary of the staged diff — press `ctrl+o` to expand it. The full diff is handed to the agent with instructions to review it.
21
22
 
22
23
  3. The agent commits using the `git_commit` tool:
23
24
 
@@ -28,7 +29,9 @@ Keeps mutative git operations out of the agent's bash and provides a safe, revie
28
29
  }
29
30
  ```
30
31
 
31
- 4. If you need to run mutative git yourself, allow it for the session:
32
+ 4. Changed your mind? Run `/stop-commit` to abort the flow before the agent commits.
33
+
34
+ 5. If you need to run mutative git yourself, allow it for the session:
32
35
 
33
36
  ```text
34
37
  /toggle-allow-git
@@ -53,7 +56,7 @@ pi install /path/to/pi-git-commit
53
56
  | `type` | `FIX` (bug fix), `IMPROVE` (improvement), or `NEW` (new feature). |
54
57
  | `message` | Commit message in imperative mood, without the type prefix (it is added automatically). A leading type word matching the chosen type is stripped (with `:`, whitespace, or `-`/`—` separators, any casing, repeats included) so the type is never duplicated. Multi-line allowed for detailed changes. |
55
58
 
56
- The tool runs `git add .` followed by `git commit -m "<TYPE>: <message>"` and reports staging or commit failures as tool errors. A leading type word in the message is stripped whenever it repeats the chosen type — with `:`, whitespace, or `-`/`—` separators, at any casing, repeated prefixes included — so the type never ends up duplicated: `FIX: Fix: ...`, `FIX: Fix ...`, and `fix - ...` all become `FIX: ...`. The tool is inactive by default and only becomes available when you run `/commit`; it is deactivated again after a single use (success or failure), so the agent cannot commit at arbitrary points in the conversation. If a commit fails, run `/commit` again to retry.
59
+ The tool runs `git add .` followed by `git commit -m "<TYPE>: <message>"` and reports staging or commit failures as tool errors. A leading type word in the message is stripped whenever it repeats the chosen type — with `:`, whitespace, or `-`/`—` separators, at any casing, repeated prefixes included — so the type never ends up duplicated: `FIX: Fix: ...`, `FIX: Fix ...`, and `fix - ...` all become `FIX: ...`. The tool is inactive by default and only becomes available when you run `/commit`; it is deactivated again after a successful commit, so the agent cannot commit at arbitrary points in the conversation. If a commit fails, the tool stays active and the agent can retry immediately without re-running `/commit`.
57
60
 
58
61
  ## The bash guard
59
62
 
@@ -70,6 +73,7 @@ Mutative git commands are blocked. Use /toggle-allow-git to allow for this sessi
70
73
  ## Troubleshooting
71
74
 
72
75
  - **The agent refuses to commit.** The guard blocks `git commit` in bash by design. Run `/commit` and let the agent use the `git_commit` tool.
76
+ - **The agent stopped committing.** You ran `/stop-commit`, which aborted the pending flow. Run `/commit` again to start a new one.
73
77
  - **"Nothing to commit (empty diff)."** There are no staged changes — make edits first, then run `/commit` again.
74
78
  - **I need git in bash right now.** Run `/toggle-allow-git`; the guard re-arms automatically on the next session start.
75
79
 
package/index.ts CHANGED
@@ -1,10 +1,13 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
1
+ import { keyHint, renderDiff, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { Box, Text } from "@earendil-works/pi-tui";
2
3
  import { Type } from "typebox";
3
4
 
4
5
  const COMMIT_TYPES = ["FIX", "IMPROVE", "NEW"] as const;
6
+ const DIFF_CUSTOM_TYPE = "git-commit-diff";
5
7
 
6
8
  export default function (pi: ExtensionAPI) {
7
9
  let gitBlocked = true;
10
+ let commitFlowActive = false;
8
11
 
9
12
  const PREFIXES = new Set(["sudo", "env", "command", "nohup", "nice", "time", "exec", "builtin", "doas", "eval", "timeout", "runuser", "pkexec"]);
10
13
  const CONTROL_KEYWORDS = new Set(["if", "then", "else", "elif", "while", "until", "do", "case", "select"]);
@@ -249,6 +252,7 @@ export default function (pi: ExtensionAPI) {
249
252
  }),
250
253
  }),
251
254
  async execute(_toolCallId, params, signal, _onUpdate, _ctx) {
255
+ let committed = false;
252
256
  try {
253
257
  const { type, message } = params;
254
258
  const description = stripLeadingCommitType(type, message);
@@ -265,23 +269,44 @@ export default function (pi: ExtensionAPI) {
265
269
  if (result.code !== 0) {
266
270
  return { content: [{ type: "text", text: `Commit failed: ${result.stderr}` }], details: {}, isError: true };
267
271
  }
268
-
272
+ committed = true;
269
273
  pi.sendMessage(
270
- { customType: "git-commit-deactivated", content: "The git_commit tool is now deactivated and cannot be used again until the user runs /commit to re-enable it. Do not mention this deactivation in your response.", display: false },
274
+ { customType: "git-commit-flow-complete", content: "The commit flow is complete.", display: false },
271
275
  { deliverAs: "steer" },
272
276
  );
273
277
  return { content: [{ type: "text", text: `✓ Committed: ${fullMessage}` }], details: {} };
274
278
  } finally {
275
- deactivateGitCommit();
279
+ if (committed) {
280
+ commitFlowActive = false;
281
+ deactivateGitCommit();
282
+ }
276
283
  }
277
284
  },
278
285
  });
279
286
 
280
287
  pi.on("session_start", () => {
281
288
  gitBlocked = true;
289
+ commitFlowActive = false;
282
290
  deactivateGitCommit();
283
291
  });
284
292
 
293
+ pi.registerMessageRenderer(DIFF_CUSTOM_TYPE, (message, { expanded, outputPad }, theme) => {
294
+ const details = message.details as { diff?: string; stat?: string } | undefined;
295
+ const box = new Box(outputPad, 1, (text) => theme.bg("customMessageBg", text));
296
+ box.addChild(new Text(theme.fg("accent", "Staged changes"), 0, 0));
297
+ if (expanded) {
298
+ if (details?.diff) {
299
+ box.addChild(new Text(renderDiff(details.diff), 0, 0));
300
+ }
301
+ } else {
302
+ if (details?.stat) {
303
+ box.addChild(new Text(details.stat, 0, 0));
304
+ }
305
+ box.addChild(new Text(theme.fg("dim", `(${keyHint("app.tools.expand", "to expand")})`), 0, 0));
306
+ }
307
+ return box;
308
+ });
309
+
285
310
  pi.registerCommand("commit", {
286
311
  description: "Stage files and show diff for commit",
287
312
  handler: async (_args, ctx) => {
@@ -289,15 +314,18 @@ export default function (pi: ExtensionAPI) {
289
314
  ctx.ui.notify("commit requires interactive mode", "error");
290
315
  return;
291
316
  }
317
+ commitFlowActive = true;
292
318
 
293
319
  try {
294
320
  await ctx.ui.setWorkingMessage("Waiting for queued messages to complete...");
295
321
  await ctx.waitForIdle();
322
+ if (!commitFlowActive) return;
296
323
 
297
324
  await ctx.ui.setWorkingMessage("Staging files...");
298
325
  const addResult = await pi.exec("git", ["add", "."]);
299
326
  if (addResult.code !== 0) {
300
327
  ctx.ui.notify(`git add failed: ${addResult.stderr}`, "error");
328
+ commitFlowActive = false;
301
329
  return;
302
330
  }
303
331
 
@@ -305,24 +333,60 @@ export default function (pi: ExtensionAPI) {
305
333
  const diffResult = await pi.exec("git", ["diff", "--staged"]);
306
334
  if (diffResult.code !== 0) {
307
335
  ctx.ui.notify(`git diff failed: ${diffResult.stderr}`, "error");
336
+ commitFlowActive = false;
308
337
  return;
309
338
  }
310
339
 
311
340
  if (!diffResult.stdout.trim()) {
312
341
  ctx.ui.notify("Nothing to commit (empty diff). Stage files first.", "warning");
342
+ commitFlowActive = false;
313
343
  return;
314
344
  }
315
345
 
316
346
  const diff = diffResult.stdout || "(no changes staged)";
317
347
 
348
+ const statResult = await pi.exec("git", ["diff", "--staged", "--stat"]);
349
+ if (!commitFlowActive) return;
350
+ const stat = statResult.code === 0 ? statResult.stdout.trim().split("\n").pop() ?? "" : "";
351
+
318
352
  const prompt = `DO NOT use bash for git. Use ONLY the \`git_commit\` tool.\n\nReview staged changes:\n\`\`\`diff\n${diff}\`\`\`\n\nUse \`git_commit\` tool with:\n- type: FIX, IMPROVE, or NEW\n- message: brief description (imperative mood)`;
353
+ if (!commitFlowActive) return;
319
354
  activateGitCommit();
320
- pi.sendUserMessage(prompt, { deliverAs: "followUp" });
355
+ pi.sendMessage(
356
+ { customType: DIFF_CUSTOM_TYPE, content: prompt, display: true, details: { diff, stat } },
357
+ { deliverAs: "followUp", triggerTurn: true },
358
+ );
321
359
  } finally {
322
360
  ctx.ui.setWorkingMessage();
323
361
  }
324
362
  },
325
363
  });
364
+
365
+ pi.registerCommand("stop-commit", {
366
+ description: "Stop the pending commit flow started by /commit",
367
+ handler: async (_args, ctx) => {
368
+ if (!ctx.hasUI) {
369
+ ctx.ui.notify("stop-commit requires interactive mode", "error");
370
+ return;
371
+ }
372
+ const toolWasActive = pi.getActiveTools().includes("git_commit");
373
+ const flowWasActive = commitFlowActive;
374
+ commitFlowActive = false;
375
+ deactivateGitCommit();
376
+ if (toolWasActive) {
377
+ pi.sendMessage(
378
+ { customType: "git-commit-stopped", content: "The user stopped the commit flow with /stop-commit. Do not attempt to commit.", display: false },
379
+ { deliverAs: "steer" },
380
+ );
381
+ ctx.ui.notify("Commit flow stopped. The git_commit tool is deactivated.", "info");
382
+ } else if (flowWasActive) {
383
+ ctx.ui.notify("Commit flow stopped.", "info");
384
+ } else {
385
+ ctx.ui.notify("No commit flow in progress.", "info");
386
+ }
387
+ },
388
+ });
389
+
326
390
  pi.registerCommand("toggle-allow-git", {
327
391
  description: "Toggle whether mutative git commands are allowed in bash for this session",
328
392
  handler: async (_args, ctx) => {
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "pi-git-commit",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "type": "module",
5
- "description": "Pi extension: block mutative git commands in bash and provide a git_commit tool plus /commit and /toggle-allow-git commands",
5
+ "description": "Pi extension: block mutative git commands in bash and provide a git_commit tool plus /commit, /stop-commit and /toggle-allow-git commands",
6
6
  "main": "index.ts",
7
7
  "repository": {
8
8
  "type": "git",
@@ -31,6 +31,7 @@
31
31
  },
32
32
  "peerDependencies": {
33
33
  "@earendil-works/pi-coding-agent": "*",
34
+ "@earendil-works/pi-tui": "*",
34
35
  "typebox": "*"
35
36
  },
36
37
  "engines": {
@@ -42,6 +43,7 @@
42
43
  },
43
44
  "devDependencies": {
44
45
  "@earendil-works/pi-coding-agent": "^0.84.0",
46
+ "@earendil-works/pi-tui": "^0.84.1",
45
47
  "@types/node": "^24.0.0",
46
48
  "typescript": "~5.9.3",
47
49
  "vitest": "^4.1.9",