pi-diff-widget 0.1.3 → 0.2.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/CHANGELOG.md CHANGED
@@ -4,6 +4,20 @@ All notable changes to this project are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
5
5
  adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.2.0] - 2026-10-12
8
+
9
+ ### Changed
10
+
11
+ - `accept` and `reject` moved to `/diff`, where the edits they act on are shown.
12
+ `/diff-widget` now only controls the panel: `on`, `off`, or a bare toggle.
13
+ - `/diff` warns about an unrecognized argument instead of opening the viewer.
14
+ - Both commands complete their arguments on `Tab`.
15
+
16
+ ### Removed
17
+
18
+ - `/diff-widget accept` and `/diff-widget reject`. Use `/diff accept` and
19
+ `/diff reject`.
20
+
7
21
  ## [0.1.3] - 2026-10-12
8
22
 
9
23
  ### Added
package/README.md CHANGED
@@ -40,8 +40,8 @@ Keys:
40
40
  - `g` / `G`: jump to top / bottom
41
41
  - `q` / `Esc`: close
42
42
 
43
- See [Live edits widget](#live-edits-widget) for the `/diff-widget` actions
44
- (`on`, `off`, `accept`, `reject`).
43
+ See [Commands](#commands) for `/diff accept`, `/diff reject`, and the
44
+ `/diff-widget` visibility toggle.
45
45
 
46
46
  ## Live edits widget
47
47
 
@@ -66,27 +66,32 @@ move or delete stops counting as a whole-file deletion.
66
66
 
67
67
  It hides itself when nothing differs.
68
68
 
69
- ### `/diff-widget` actions
69
+ ### Commands
70
+
71
+ Keeping or undoing the agent's edits acts on the tracked files, not on everything
72
+ the uncommitted view lists: `/diff accept` and `/diff reject` work from the
73
+ session baselines, so files the agent never touched are left alone. If you hand
74
+ edit a tracked file after the agent first touched it, `/diff reject` discards
75
+ that edit too.
70
76
 
71
77
  | Command | Effect |
72
78
  |---|---|
79
+ | `/diff accept` | Keep the agent's edits: the current content becomes the baseline and the panel clears. Alias: `keep` |
80
+ | `/diff reject` | Undo the agent's edits: restore every tracked file to its pre-agent content, delete the files the agent created, and recreate the ones it deleted |
73
81
  | `/diff-widget` | Toggle the panel. With no argument it shows a hidden panel and hides a visible one |
74
82
  | `/diff-widget on` | Show it |
75
83
  | `/diff-widget off` | Hide it |
76
- | `/diff-widget accept` | Keep the agent's edits: the current content becomes the baseline and the panel clears. Alias: `keep` |
77
- | `/diff-widget reject` | Undo the agent's edits: restore every tracked file to its pre-agent content, delete the files the agent created, and recreate the ones it deleted |
78
84
 
79
85
  `accept` is Zed's Keep All. `reject` asks for confirmation first, naming up to
80
86
  five affected paths, and cannot be undone. Whole files only: there is no
81
87
  hunk-level accept or reject. With nothing to reject it reports `Nothing to
82
- reject`, and any unrecognized argument warns instead of toggling the panel, so a
83
- typo never hides the widget by accident.
88
+ reject`. Any unrecognized argument warns instead of acting, so a typo never hides
89
+ the panel or reverts a file. Both commands complete their arguments on `Tab`.
84
90
 
85
91
  ```
86
- /diff-widget
87
- /diff-widget on
88
- /diff-widget accept
89
- /diff-widget reject
92
+ /diff accept
93
+ /diff reject
94
+ /diff-widget off
90
95
  ```
91
96
 
92
97
  ## Status
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-diff-widget",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "Interactive diff viewer for pi: uncommitted changes and per-turn diffs",
5
5
  "keywords": [
6
6
  "pi-package",
package/src/actions.ts ADDED
@@ -0,0 +1,74 @@
1
+ import type { AutocompleteItem } from '@earendil-works/pi-tui';
2
+ import type {
3
+ ExtensionAPI,
4
+ ExtensionCommandContext,
5
+ } from '@earendil-works/pi-coding-agent';
6
+ import type { BaselineTracker } from './baseline.ts';
7
+ import { collectRevertTargets, revertTargets } from './summary.ts';
8
+
9
+ export type DiffAction = 'view' | 'accept' | 'reject' | 'unknown';
10
+
11
+ /** Maps `/diff` arguments onto the action they select. Bare means the viewer. */
12
+ export function parseDiffAction(args: string): DiffAction {
13
+ const action = args.trim().toLowerCase();
14
+ if (action === '') return 'view';
15
+ if (action === 'accept' || action === 'keep') return 'accept';
16
+ if (action === 'reject') return 'reject';
17
+ return 'unknown';
18
+ }
19
+
20
+ /** Completes a first argument from `items`, and stops once a second word starts. */
21
+ export function argumentCompletions(
22
+ items: string[],
23
+ ): (prefix: string) => AutocompleteItem[] | null {
24
+ return (prefix) => {
25
+ const [first = '', ...rest] = prefix.trimStart().split(/\s+/);
26
+ if (rest.length > 0) return null;
27
+ return items
28
+ .filter((item) => item.startsWith(first))
29
+ .map((item) => ({ value: `${item} `, label: item }));
30
+ };
31
+ }
32
+
33
+ /** Repaint the widget after a baseline change, so the panel is never stale. */
34
+ export type Refresh = (ctx: ExtensionCommandContext) => Promise<void>;
35
+
36
+ /** Keep the agent's edits: the current content becomes the new baseline. */
37
+ export async function acceptEdits(
38
+ pi: ExtensionAPI,
39
+ baselines: BaselineTracker,
40
+ refresh: Refresh,
41
+ ctx: ExtensionCommandContext,
42
+ ): Promise<void> {
43
+ await baselines.rebaseline(pi);
44
+ await refresh(ctx);
45
+ ctx.ui.notify('Edits accepted', 'info');
46
+ }
47
+
48
+ /** Undo the agent's edits, after confirming. Restores pre-agent content. */
49
+ export async function rejectEdits(
50
+ baselines: BaselineTracker,
51
+ refresh: Refresh,
52
+ ctx: ExtensionCommandContext,
53
+ ): Promise<void> {
54
+ await ctx.waitForIdle();
55
+ const targets = await collectRevertTargets(baselines, ctx.cwd);
56
+ if (targets.length === 0) {
57
+ ctx.ui.notify('Nothing to reject', 'info');
58
+ return;
59
+ }
60
+ const names = targets
61
+ .slice(0, 5)
62
+ .map((target) => target.relativePath)
63
+ .join(', ');
64
+ const rest = targets.length > 5 ? ` and ${targets.length - 5} more` : '';
65
+ const confirmed = await ctx.ui.confirm(
66
+ 'Reject agent edits?',
67
+ `Restore ${targets.length} file(s) to their pre-agent content: ${names}${rest}. This cannot be undone.`,
68
+ );
69
+ if (!confirmed) return;
70
+
71
+ await revertTargets(targets);
72
+ await refresh(ctx);
73
+ ctx.ui.notify(`Rejected edits to ${targets.length} file(s)`, 'info');
74
+ }
package/src/index.ts CHANGED
@@ -1,6 +1,12 @@
1
1
  import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
2
2
  import { collectUncommitted, type DiffFile } from './git.ts';
3
3
  import type { ExecFn } from './exec.ts';
4
+ import {
5
+ acceptEdits,
6
+ argumentCompletions,
7
+ parseDiffAction,
8
+ rejectEdits,
9
+ } from './actions.ts';
4
10
  import { createBaselineTracker } from './baseline.ts';
5
11
  import { collectTurns, createWriteDiffTracker } from './turns.ts';
6
12
  import { DiffViewer } from './ui.ts';
@@ -13,12 +19,30 @@ export default function (pi: ExtensionAPI) {
13
19
  const exec: ExecFn = (command, args, options) =>
14
20
  pi.exec(command, args, options);
15
21
 
16
- registerDiffWidget(pi, baselines, exec);
22
+ const refreshWidget = registerDiffWidget(pi, baselines, exec);
17
23
 
18
24
  pi.registerCommand('diff', {
19
25
  description:
20
- 'Show uncommitted changes and per-turn edits in a scrollable viewer',
21
- handler: async (_args, ctx) => {
26
+ 'Show uncommitted changes and per-turn edits, or accept and reject the edits the agent made',
27
+ getArgumentCompletions: argumentCompletions(['accept', 'reject']),
28
+ handler: async (args, ctx) => {
29
+ const action = parseDiffAction(args);
30
+ if (action === 'accept') {
31
+ await acceptEdits(pi, baselines, refreshWidget, ctx);
32
+ return;
33
+ }
34
+ if (action === 'reject') {
35
+ await rejectEdits(baselines, refreshWidget, ctx);
36
+ return;
37
+ }
38
+ if (action === 'unknown') {
39
+ ctx.ui.notify(
40
+ `Unknown action "${args.trim()}". Use /diff, /diff accept (keep), or /diff reject.`,
41
+ 'warning',
42
+ );
43
+ return;
44
+ }
45
+
22
46
  if (ctx.mode !== 'tui') {
23
47
  ctx.ui.notify('/diff needs the interactive TUI', 'warning');
24
48
  return;
package/src/widget.ts CHANGED
@@ -5,15 +5,12 @@ import type {
5
5
  ExtensionContext,
6
6
  } from '@earendil-works/pi-coding-agent';
7
7
  import type { Theme } from '@earendil-works/pi-coding-agent';
8
+ import type { Refresh } from './actions.ts';
9
+ import { argumentCompletions } from './actions.ts';
8
10
  import type { BaselineTracker } from './baseline.ts';
9
11
  import type { DocumentTheme } from './document.ts';
10
12
  import type { ExecFn } from './exec.ts';
11
- import {
12
- collectRevertTargets,
13
- computeNetSummary,
14
- pruneCleanBaselines,
15
- revertTargets,
16
- } from './summary.ts';
13
+ import { computeNetSummary, pruneCleanBaselines } from './summary.ts';
17
14
  import type { EditSummary } from './view.ts';
18
15
 
19
16
  export const WIDGET_KEY = 'pi-diff';
@@ -76,7 +73,7 @@ export function registerDiffWidget(
76
73
  pi: ExtensionAPI,
77
74
  baselines: BaselineTracker,
78
75
  exec: ExecFn,
79
- ): void {
76
+ ): Refresh {
80
77
  let visible = true;
81
78
 
82
79
  const update = async (ctx: ExtensionContext) => {
@@ -108,52 +105,17 @@ export function registerDiffWidget(
108
105
  });
109
106
 
110
107
  pi.registerCommand('diff-widget', {
111
- description:
112
- 'Live edits widget: on, off, accept (keep), or reject (undo)',
108
+ description: 'Live edits widget: on, off, or toggle',
109
+ getArgumentCompletions: argumentCompletions(['on', 'off']),
113
110
  handler: async (args, ctx) => {
114
111
  const action = args.trim().toLowerCase();
115
112
 
116
- if (action === 'reject') {
117
- await ctx.waitForIdle();
118
- const targets = await collectRevertTargets(baselines, ctx.cwd);
119
- if (targets.length === 0) {
120
- ctx.ui.notify('Nothing to reject', 'info');
121
- return;
122
- }
123
- const names = targets
124
- .slice(0, 5)
125
- .map((target) => target.relativePath)
126
- .join(', ');
127
- const rest =
128
- targets.length > 5 ? ` and ${targets.length - 5} more` : '';
129
- const confirmed = await ctx.ui.confirm(
130
- 'Reject agent edits?',
131
- `Restore ${targets.length} file(s) to their pre-agent content: ${names}${rest}. This cannot be undone.`,
132
- );
133
- if (!confirmed) return;
134
-
135
- await revertTargets(targets);
136
- await update(ctx);
137
- ctx.ui.notify(
138
- `Rejected edits to ${targets.length} file(s)`,
139
- 'info',
140
- );
141
- return;
142
- }
143
-
144
- if (action === 'accept' || action === 'keep') {
145
- await baselines.rebaseline(pi);
146
- await update(ctx);
147
- ctx.ui.notify('Edits accepted', 'info');
148
- return;
149
- }
150
-
151
113
  if (action === 'on') visible = true;
152
114
  else if (action === 'off') visible = false;
153
115
  else if (action === '') visible = !visible;
154
116
  else {
155
117
  ctx.ui.notify(
156
- `Unknown action "${action}". Use on, off, accept, or reject.`,
118
+ `Unknown action "${action}". Use on or off. To keep or undo the edits, use /diff accept or /diff reject.`,
157
119
  'warning',
158
120
  );
159
121
  return;
@@ -163,4 +125,6 @@ export function registerDiffWidget(
163
125
  ctx.ui.notify(`Edits widget ${visible ? 'on' : 'off'}`, 'info');
164
126
  },
165
127
  });
128
+
129
+ return update;
166
130
  }