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 +14 -0
- package/README.md +16 -11
- package/package.json +1 -1
- package/src/actions.ts +74 -0
- package/src/index.ts +27 -3
- package/src/widget.ts +9 -45
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 [
|
|
44
|
-
|
|
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
|
-
###
|
|
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
|
|
83
|
-
|
|
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
|
|
87
|
-
/diff
|
|
88
|
-
/diff-widget
|
|
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
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
|
|
21
|
-
|
|
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
|
-
):
|
|
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
|
-
|
|
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
|
|
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
|
}
|