pi-edit-session-in-place 0.1.13 → 0.1.15
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 +22 -0
- package/README.md +7 -6
- package/extensions/edit-session-in-place.ts +174 -16
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,28 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## [0.1.15] - 2026-06-04
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- updated the local pi development baseline to `@earendil-works/pi-coding-agent` / `@earendil-works/pi-tui` `0.78.1` and regenerated the npm lockfile
|
|
11
|
+
- refreshed README compatibility notes and the fleet-tested pi marker for `0.78.1` without pinning pi `0.78.1` as a hard runtime requirement
|
|
12
|
+
- changed the main-editor integration to wrap any previously configured custom editor instead of replacing it
|
|
13
|
+
|
|
14
|
+
### Compatibility
|
|
15
|
+
- reviewed the pi `0.78.1` changelog and current extension, TUI, custom editor, mode, session, and package guidance; the extension now guards custom TUI behavior with `ctx.mode === "tui"` and keeps existing custom editors composed
|
|
16
|
+
- confirmed the new `ctx.getSystemPromptOptions()` command API, provider additions, package-install hardening, and security fixes do not require additional runtime changes for this package
|
|
17
|
+
|
|
18
|
+
## [0.1.14] - 2026-05-29
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- updated the local pi development baseline to `@earendil-works/pi-coding-agent` / `@earendil-works/pi-tui` `0.78.0` and regenerated the npm lockfile
|
|
22
|
+
- aligned package tooling metadata with the pi fleet policy: `packageManager` now records the npm 11 major line and `engines.node` is `>=22 <25`
|
|
23
|
+
- refreshed README compatibility notes and the fleet-tested pi marker for `0.78.0`
|
|
24
|
+
|
|
25
|
+
### Compatibility
|
|
26
|
+
- reviewed the pi `0.78.0` changelog and current extension, TUI, session, and package guidance; the extension continues to use supported command, shortcut, custom editor, and session tree APIs
|
|
27
|
+
- confirmed the new startup session naming, file-link rendering, provider, and argument parsing changes do not require runtime changes for this package
|
|
28
|
+
|
|
7
29
|
## [0.1.13] - 2026-05-28
|
|
8
30
|
|
|
9
31
|
### Changed
|
package/README.md
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# pi edit-session-in-place
|
|
2
2
|
|
|
3
|
-
A [pi](https://github.com/
|
|
3
|
+
A [pi](https://github.com/earendil-works/pi-mono) extension that lets you rewind to an earlier user message in the current branch, then either **edit it in place** or **delete it and continue from there**.
|
|
4
4
|
|
|
5
5
|
## Compatibility
|
|
6
6
|
|
|
7
7
|
Tested with:
|
|
8
8
|
|
|
9
|
-
- `@earendil-works/pi-coding-agent` `0.
|
|
10
|
-
- `@earendil-works/pi-tui` `0.
|
|
11
|
-
- Node.js `>=22
|
|
9
|
+
- `@earendil-works/pi-coding-agent` `0.78.1`
|
|
10
|
+
- `@earendil-works/pi-tui` `0.78.1`
|
|
11
|
+
- Node.js `>=22 <25`
|
|
12
12
|
|
|
13
|
-
Local development and verification in this repo target pi `0.
|
|
13
|
+
Local development and verification in this repo target pi `0.78.1` as the suggested minimum tested baseline. `@earendil-works/pi-coding-agent` and `@earendil-works/pi-tui` stay in `devDependencies` for local typechecking and tests, while pi core packages are declared as optional wildcard peers and the extension relies on pi's bundled runtime packages at execution time. That keeps installs forward-open for future pi releases: npm peer ranges should not block users from trying a newer pi, though runtime behavior is only verified against the tested baseline until a follow-up package release confirms it.
|
|
14
14
|
|
|
15
15
|
## What it does
|
|
16
16
|
|
|
@@ -76,11 +76,12 @@ If you clear the message and submit an empty value, the selected message is effe
|
|
|
76
76
|
|
|
77
77
|
## Behavior notes
|
|
78
78
|
|
|
79
|
-
- Works in interactive mode; non-interactive modes do not show the picker/editor UI
|
|
79
|
+
- Works in interactive TUI mode; non-interactive and RPC modes do not show the picker/editor UI
|
|
80
80
|
- Later messages on the abandoned branch are not deleted from the session file; they remain reachable through `/tree`
|
|
81
81
|
- If the selected message contains images, the extension warns that re-editing or deleting it will drop the images and keep only text behavior
|
|
82
82
|
- The extension only offers text-bearing user messages for editing; image-only or whitespace-only user messages are skipped
|
|
83
83
|
- Queued messages must be cleared before using the command
|
|
84
|
+
- If another extension has already customized the main editor, this extension wraps it instead of replacing it
|
|
84
85
|
|
|
85
86
|
## Development
|
|
86
87
|
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
DynamicBorder,
|
|
16
16
|
keyHint,
|
|
17
17
|
rawKeyHint,
|
|
18
|
+
type AppKeybinding,
|
|
18
19
|
type ExtensionAPI,
|
|
19
20
|
type ExtensionCommandContext,
|
|
20
21
|
type KeybindingsManager,
|
|
@@ -29,6 +30,8 @@ import {
|
|
|
29
30
|
Spacer,
|
|
30
31
|
Text,
|
|
31
32
|
matchesKey,
|
|
33
|
+
type AutocompleteProvider,
|
|
34
|
+
type EditorComponent,
|
|
32
35
|
type EditorTheme,
|
|
33
36
|
type Focusable,
|
|
34
37
|
type TUI,
|
|
@@ -288,7 +291,7 @@ class EditableMessageSelector extends Container {
|
|
|
288
291
|
this.onCancel = onCancel;
|
|
289
292
|
this.selectedIndex = Math.max(0, messages.length - 1);
|
|
290
293
|
|
|
291
|
-
this.addChild(new DynamicBorder((text) => theme.fg("accent", text)));
|
|
294
|
+
this.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
|
|
292
295
|
this.addChild(new Spacer(1));
|
|
293
296
|
this.addChild(new Text(theme.fg("accent", title), 1, 0));
|
|
294
297
|
this.addChild(new Spacer(1));
|
|
@@ -317,7 +320,7 @@ class EditableMessageSelector extends Container {
|
|
|
317
320
|
),
|
|
318
321
|
);
|
|
319
322
|
this.addChild(new Spacer(1));
|
|
320
|
-
this.addChild(new DynamicBorder((text) => theme.fg("accent", text)));
|
|
323
|
+
this.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
|
|
321
324
|
}
|
|
322
325
|
|
|
323
326
|
private setSelectedIndex(index: number) {
|
|
@@ -391,7 +394,7 @@ class ReeditMessageEditor extends Container implements Focusable {
|
|
|
391
394
|
this.keybindings = keybindings;
|
|
392
395
|
this.onCancel = onCancel;
|
|
393
396
|
|
|
394
|
-
this.addChild(new DynamicBorder((text) => theme.fg("accent", text)));
|
|
397
|
+
this.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
|
|
395
398
|
this.addChild(new Spacer(1));
|
|
396
399
|
this.addChild(new Text(theme.fg("accent", title), 1, 0));
|
|
397
400
|
this.addChild(new Spacer(1));
|
|
@@ -413,7 +416,7 @@ class ReeditMessageEditor extends Container implements Focusable {
|
|
|
413
416
|
].join(" ");
|
|
414
417
|
this.addChild(new Text(hint, 1, 0));
|
|
415
418
|
this.addChild(new Spacer(1));
|
|
416
|
-
this.addChild(new DynamicBorder((text) => theme.fg("accent", text)));
|
|
419
|
+
this.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
|
|
417
420
|
}
|
|
418
421
|
|
|
419
422
|
handleInput(data: string): void {
|
|
@@ -503,7 +506,10 @@ const clearSavedDraft = () => {
|
|
|
503
506
|
};
|
|
504
507
|
|
|
505
508
|
const handleEditTurn = async (ctx: ExtensionCommandContext) => {
|
|
506
|
-
if (
|
|
509
|
+
if (ctx.mode !== "tui") {
|
|
510
|
+
if (ctx.hasUI) {
|
|
511
|
+
ctx.ui.notify("/edit-turn requires interactive TUI mode.", "warning");
|
|
512
|
+
}
|
|
507
513
|
clearSavedDraft();
|
|
508
514
|
return;
|
|
509
515
|
}
|
|
@@ -566,12 +572,16 @@ const handleEditTurn = async (ctx: ExtensionCommandContext) => {
|
|
|
566
572
|
);
|
|
567
573
|
};
|
|
568
574
|
|
|
575
|
+
const triggerEditTurnCommand = (editor: Pick<EditorComponent, "getText" | "setText" | "handleInput">) => {
|
|
576
|
+
draftBeforeHotkey = editor.getText();
|
|
577
|
+
editor.setText(COMMAND_TEXT);
|
|
578
|
+
editor.handleInput("\r");
|
|
579
|
+
};
|
|
580
|
+
|
|
569
581
|
class EditSessionInPlaceEditor extends CustomEditor {
|
|
570
582
|
handleInput(data: string): void {
|
|
571
583
|
if (matchesKey(data, HOTKEY)) {
|
|
572
|
-
|
|
573
|
-
this.setText(COMMAND_TEXT);
|
|
574
|
-
super.handleInput("\r");
|
|
584
|
+
triggerEditTurnCommand(this);
|
|
575
585
|
return;
|
|
576
586
|
}
|
|
577
587
|
|
|
@@ -579,6 +589,152 @@ class EditSessionInPlaceEditor extends CustomEditor {
|
|
|
579
589
|
}
|
|
580
590
|
}
|
|
581
591
|
|
|
592
|
+
type AutocompleteAwareEditor = EditorComponent & {
|
|
593
|
+
isShowingAutocomplete?: () => boolean;
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
class EditSessionInPlaceEditorWrapper implements EditorComponent, Focusable {
|
|
597
|
+
public actionHandlers: Map<AppKeybinding, () => void> = new Map();
|
|
598
|
+
public onEscape?: () => void;
|
|
599
|
+
public onCtrlD?: () => void;
|
|
600
|
+
public onPasteImage?: () => void;
|
|
601
|
+
public onExtensionShortcut?: (data: string) => boolean | undefined;
|
|
602
|
+
|
|
603
|
+
constructor(
|
|
604
|
+
private readonly base: EditorComponent,
|
|
605
|
+
private readonly keybindings: KeybindingsManager,
|
|
606
|
+
) {}
|
|
607
|
+
|
|
608
|
+
get focused(): boolean {
|
|
609
|
+
return "focused" in this.base && typeof this.base.focused === "boolean" ? this.base.focused : false;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
set focused(value: boolean) {
|
|
613
|
+
if ("focused" in this.base) {
|
|
614
|
+
(this.base as EditorComponent & Focusable).focused = value;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
get wantsKeyRelease(): boolean | undefined {
|
|
619
|
+
return this.base.wantsKeyRelease;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
set wantsKeyRelease(value: boolean | undefined) {
|
|
623
|
+
this.base.wantsKeyRelease = value;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
get onSubmit(): ((text: string) => void) | undefined {
|
|
627
|
+
return this.base.onSubmit;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
set onSubmit(value: ((text: string) => void) | undefined) {
|
|
631
|
+
this.base.onSubmit = value;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
get onChange(): ((text: string) => void) | undefined {
|
|
635
|
+
return this.base.onChange;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
set onChange(value: ((text: string) => void) | undefined) {
|
|
639
|
+
this.base.onChange = value;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
get borderColor(): ((str: string) => string) | undefined {
|
|
643
|
+
return this.base.borderColor;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
set borderColor(value: ((str: string) => string) | undefined) {
|
|
647
|
+
this.base.borderColor = value;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
render(width: number): string[] {
|
|
651
|
+
return this.base.render(width);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
invalidate(): void {
|
|
655
|
+
this.base.invalidate();
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
getText(): string {
|
|
659
|
+
return this.base.getText();
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
setText(text: string): void {
|
|
663
|
+
this.base.setText(text);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
handleInput(data: string): void {
|
|
667
|
+
if (matchesKey(data, HOTKEY)) {
|
|
668
|
+
triggerEditTurnCommand(this.base);
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (this.onExtensionShortcut?.(data)) {
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
if (this.keybindings.matches(data, "app.clipboard.pasteImage")) {
|
|
677
|
+
this.onPasteImage?.();
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (this.keybindings.matches(data, "app.interrupt")) {
|
|
682
|
+
const isShowingAutocomplete = (this.base as AutocompleteAwareEditor).isShowingAutocomplete?.() ?? false;
|
|
683
|
+
if (!isShowingAutocomplete) {
|
|
684
|
+
const handler = this.onEscape ?? this.actionHandlers.get("app.interrupt");
|
|
685
|
+
if (handler) {
|
|
686
|
+
handler();
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
this.base.handleInput(data);
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (this.keybindings.matches(data, "app.exit") && this.base.getText().length === 0) {
|
|
696
|
+
const handler = this.onCtrlD ?? this.actionHandlers.get("app.exit");
|
|
697
|
+
if (handler) {
|
|
698
|
+
handler();
|
|
699
|
+
}
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
for (const [action, handler] of this.actionHandlers) {
|
|
704
|
+
if (action !== "app.interrupt" && action !== "app.exit" && this.keybindings.matches(data, action)) {
|
|
705
|
+
handler();
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
this.base.handleInput(data);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
addToHistory(text: string): void {
|
|
714
|
+
this.base.addToHistory?.(text);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
insertTextAtCursor(text: string): void {
|
|
718
|
+
this.base.insertTextAtCursor?.(text);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
getExpandedText(): string {
|
|
722
|
+
return this.base.getExpandedText?.() ?? this.base.getText();
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
setAutocompleteProvider(provider: AutocompleteProvider): void {
|
|
726
|
+
this.base.setAutocompleteProvider?.(provider);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
setPaddingX(padding: number): void {
|
|
730
|
+
this.base.setPaddingX?.(padding);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
setAutocompleteMaxVisible(maxVisible: number): void {
|
|
734
|
+
this.base.setAutocompleteMaxVisible?.(maxVisible);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
582
738
|
export default function editSessionInPlace(pi: ExtensionAPI) {
|
|
583
739
|
pi.registerCommand(COMMAND_NAME, {
|
|
584
740
|
description: `Select and re-edit a previous user message on the current branch (${HOTKEY_LABEL})`,
|
|
@@ -587,9 +743,8 @@ export default function editSessionInPlace(pi: ExtensionAPI) {
|
|
|
587
743
|
},
|
|
588
744
|
});
|
|
589
745
|
|
|
590
|
-
//
|
|
591
|
-
//
|
|
592
|
-
// register the shortcut here so it appears in /hotkeys and other shortcut diagnostics.
|
|
746
|
+
// Shortcut handlers receive ExtensionContext, so the custom editor hotkey path performs
|
|
747
|
+
// the command execution while this registration keeps the hotkey discoverable.
|
|
593
748
|
pi.registerShortcut(HOTKEY, {
|
|
594
749
|
description: `Edit a previous user message (${HOTKEY_LABEL})`,
|
|
595
750
|
handler: (ctx) => {
|
|
@@ -601,15 +756,18 @@ export default function editSessionInPlace(pi: ExtensionAPI) {
|
|
|
601
756
|
},
|
|
602
757
|
});
|
|
603
758
|
|
|
604
|
-
pi.on("session_start",
|
|
759
|
+
pi.on("session_start", (_event, ctx) => {
|
|
605
760
|
clearSavedDraft();
|
|
761
|
+
if (ctx.mode === "tui") {
|
|
762
|
+
const previousEditor = ctx.ui.getEditorComponent();
|
|
763
|
+
ctx.ui.setEditorComponent((tui, theme, keybindings) => {
|
|
764
|
+
const baseEditor = previousEditor?.(tui, theme, keybindings);
|
|
765
|
+
return baseEditor ? new EditSessionInPlaceEditorWrapper(baseEditor, keybindings) : new EditSessionInPlaceEditor(tui, theme, keybindings);
|
|
766
|
+
});
|
|
767
|
+
}
|
|
606
768
|
});
|
|
607
769
|
|
|
608
770
|
pi.on("session_shutdown", async () => {
|
|
609
771
|
clearSavedDraft();
|
|
610
772
|
});
|
|
611
|
-
|
|
612
|
-
pi.on("session_start", (_event, ctx) => {
|
|
613
|
-
ctx.ui.setEditorComponent((tui, theme, keybindings) => new EditSessionInPlaceEditor(tui, theme, keybindings));
|
|
614
|
-
});
|
|
615
773
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-edit-session-in-place",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "pi extension that lets you re-edit or delete an earlier user message in the current session branch",
|
|
5
5
|
"author": "Mitch Fultz (https://github.com/fitchmultz)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,11 +42,11 @@
|
|
|
42
42
|
"@earendil-works/pi-tui": "*"
|
|
43
43
|
},
|
|
44
44
|
"engines": {
|
|
45
|
-
"node": ">=22
|
|
45
|
+
"node": ">=22 <25"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
49
|
-
"@earendil-works/pi-tui": "^0.
|
|
48
|
+
"@earendil-works/pi-coding-agent": "^0.78.1",
|
|
49
|
+
"@earendil-works/pi-tui": "^0.78.1",
|
|
50
50
|
"@types/node": "^25.9.1",
|
|
51
51
|
"typescript": "^6.0.3"
|
|
52
52
|
},
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"./extensions"
|
|
59
59
|
]
|
|
60
60
|
},
|
|
61
|
-
"packageManager": "npm@11.
|
|
61
|
+
"packageManager": "npm@11.0.0",
|
|
62
62
|
"peerDependenciesMeta": {
|
|
63
63
|
"@earendil-works/pi-coding-agent": {
|
|
64
64
|
"optional": true
|