pi-anycopy 0.2.1 → 0.2.2
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 +7 -3
- package/extensions/anycopy/config.json +2 -1
- package/extensions/anycopy/index.ts +92 -5
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ Defaults (customizable in `config.json`):
|
|
|
47
47
|
| Key | Action |
|
|
48
48
|
|-----|--------|
|
|
49
49
|
| `Enter` | Navigate to the focused node (same semantics as `/tree`) |
|
|
50
|
-
| `
|
|
50
|
+
| `Shift+A` | Select/unselect focused node for copy |
|
|
51
51
|
| `Shift+C` | Copy selected nodes, or the focused node if nothing is selected |
|
|
52
52
|
| `Shift+X` | Clear selection |
|
|
53
53
|
| `Shift+L` | Label node (native tree behavior) |
|
|
@@ -65,11 +65,13 @@ Notes:
|
|
|
65
65
|
- If no nodes are selected, `Shift+C` copies the focused node
|
|
66
66
|
- Single-node copies use just that node's content; role prefixes like `user:` or `assistant:` are only added when copying 2 or more nodes
|
|
67
67
|
- When copying multiple selected nodes, they are auto-sorted chronologically by position in the session tree, not by selection order
|
|
68
|
-
-
|
|
68
|
+
- `Shift+A`/`Shift+C` multi-select copy behavior is unchanged by navigation support, while plain space remains available for search queries
|
|
69
69
|
- `Shift+T` is configurable via `keys.toggleLabelTimestamps` in `config.json`
|
|
70
70
|
- `Shift+T` shows timestamps for labeled nodes only, using the latest label-change time for each label
|
|
71
71
|
- Same-day labels show `HH:MM`; older labels show `M/D HH:MM`; cross-year labels show `YY/M/D HH:MM`
|
|
72
72
|
- Label edits are persisted via `pi.setLabel(...)`
|
|
73
|
+
- [Folded](https://github.com/badlogic/pi-mono/blob/09e9de5749193beab234f30ed220a77f3d91cfad/packages/coding-agent/docs/tree.md#controls) branches are persisted by default in hidden `/anycopy` session entries, so closing/reopening `/anycopy`, switching to a sibling branch, or revisiting the session later restores the same folded view until you explicitly unfold it again
|
|
74
|
+
- Search and filter changes still reset the live overlay's fold state temporarily; reopening `/anycopy` restores the persisted folded branches
|
|
73
75
|
|
|
74
76
|
## Configuration
|
|
75
77
|
|
|
@@ -77,13 +79,15 @@ Edit `~/.pi/agent/extensions/anycopy/config.json`:
|
|
|
77
79
|
|
|
78
80
|
- `treeFilterMode`: initial tree filter mode when opening `/anycopy`; defaults to `default` to match `/tree`
|
|
79
81
|
- one of: `default` | `no-tools` | `user-only` | `labeled-only` | `all`
|
|
82
|
+
- `persistFoldState`: whether `/anycopy` persists folded branches across reopenings and later sessions; defaults to `true`; when disabled, `/anycopy` does not read or write hidden fold-state session entries
|
|
80
83
|
- `keys`: keybindings used inside the `/anycopy` overlay for copy/preview actions, including the label timestamp toggle
|
|
81
84
|
|
|
82
85
|
```json
|
|
83
86
|
{
|
|
84
87
|
"treeFilterMode": "default",
|
|
88
|
+
"persistFoldState": true,
|
|
85
89
|
"keys": {
|
|
86
|
-
"toggleSelect": "
|
|
90
|
+
"toggleSelect": "shift+a",
|
|
87
91
|
"copy": "shift+c",
|
|
88
92
|
"clear": "shift+x",
|
|
89
93
|
"toggleLabelTimestamps": "shift+t",
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Layout: native TreeSelectorComponent at top, status bar, preview below
|
|
5
5
|
*
|
|
6
6
|
* Default keys (customizable via ./config.json):
|
|
7
|
-
*
|
|
7
|
+
* Shift+A - select/unselect focused node for copy
|
|
8
8
|
* Shift+C - copy selected nodes (or focused node if none selected)
|
|
9
9
|
* Shift+X - clear selection
|
|
10
10
|
* Shift+L - label node
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
TreeSelectorComponent,
|
|
24
24
|
} from "@mariozechner/pi-coding-agent";
|
|
25
25
|
|
|
26
|
-
import { Markdown, matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
|
|
26
|
+
import { getKeybindings, Markdown, matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
|
|
27
27
|
import type { Focusable } from "@mariozechner/pi-tui";
|
|
28
28
|
|
|
29
29
|
import { existsSync, readFileSync } from "fs";
|
|
@@ -32,6 +32,16 @@ import { dirname, join } from "path";
|
|
|
32
32
|
import { fileURLToPath } from "url";
|
|
33
33
|
|
|
34
34
|
import { runAnycopyEnterNavigation } from "./enter-navigation.ts";
|
|
35
|
+
import {
|
|
36
|
+
ANYCOPY_FOLD_STATE_CUSTOM_TYPE,
|
|
37
|
+
createFoldStateEntryData,
|
|
38
|
+
foldStateNodeIdListsEqual,
|
|
39
|
+
getSelectorFoldedNodeIds,
|
|
40
|
+
loadLatestFoldStateFromEntries,
|
|
41
|
+
mergeExplicitFoldMutation,
|
|
42
|
+
normalizeFoldedNodeIds,
|
|
43
|
+
setSelectorFoldedNodeIds,
|
|
44
|
+
} from "./fold-state.ts";
|
|
35
45
|
|
|
36
46
|
type SessionTreeNode = {
|
|
37
47
|
entry: SessionEntry;
|
|
@@ -56,11 +66,13 @@ type TreeFilterMode = "default" | "no-tools" | "user-only" | "labeled-only" | "a
|
|
|
56
66
|
type anycopyConfig = {
|
|
57
67
|
keys?: Partial<anycopyKeyConfig>;
|
|
58
68
|
treeFilterMode?: TreeFilterMode;
|
|
69
|
+
persistFoldState?: boolean;
|
|
59
70
|
};
|
|
60
71
|
|
|
61
72
|
type anycopyRuntimeConfig = {
|
|
62
73
|
keys: anycopyKeyConfig;
|
|
63
74
|
treeFilterMode: TreeFilterMode;
|
|
75
|
+
persistFoldState: boolean;
|
|
64
76
|
};
|
|
65
77
|
|
|
66
78
|
type BranchSummarySettingsFile = {
|
|
@@ -70,7 +82,7 @@ type BranchSummarySettingsFile = {
|
|
|
70
82
|
};
|
|
71
83
|
|
|
72
84
|
const DEFAULT_KEYS: anycopyKeyConfig = {
|
|
73
|
-
toggleSelect: "
|
|
85
|
+
toggleSelect: "shift+a",
|
|
74
86
|
copy: "shift+c",
|
|
75
87
|
clear: "shift+x",
|
|
76
88
|
toggleLabelTimestamps: "shift+t",
|
|
@@ -81,6 +93,7 @@ const DEFAULT_KEYS: anycopyKeyConfig = {
|
|
|
81
93
|
};
|
|
82
94
|
|
|
83
95
|
const DEFAULT_TREE_FILTER_MODE: TreeFilterMode = "default";
|
|
96
|
+
const DEFAULT_PERSIST_FOLD_STATE = true;
|
|
84
97
|
|
|
85
98
|
const getExtensionDir = (): string => {
|
|
86
99
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
@@ -116,6 +129,7 @@ const loadConfig = (): anycopyRuntimeConfig => {
|
|
|
116
129
|
return {
|
|
117
130
|
keys: { ...DEFAULT_KEYS },
|
|
118
131
|
treeFilterMode: DEFAULT_TREE_FILTER_MODE,
|
|
132
|
+
persistFoldState: DEFAULT_PERSIST_FOLD_STATE,
|
|
119
133
|
};
|
|
120
134
|
}
|
|
121
135
|
|
|
@@ -129,6 +143,8 @@ const loadConfig = (): anycopyRuntimeConfig => {
|
|
|
129
143
|
typeof treeFilterModeRaw === "string" && validTreeFilterModes.includes(treeFilterModeRaw as TreeFilterMode)
|
|
130
144
|
? (treeFilterModeRaw as TreeFilterMode)
|
|
131
145
|
: DEFAULT_TREE_FILTER_MODE;
|
|
146
|
+
const persistFoldState =
|
|
147
|
+
typeof parsed.persistFoldState === "boolean" ? parsed.persistFoldState : DEFAULT_PERSIST_FOLD_STATE;
|
|
132
148
|
|
|
133
149
|
return {
|
|
134
150
|
keys: {
|
|
@@ -145,11 +161,13 @@ const loadConfig = (): anycopyRuntimeConfig => {
|
|
|
145
161
|
pageUp: typeof keys.pageUp === "string" ? keys.pageUp : DEFAULT_KEYS.pageUp,
|
|
146
162
|
},
|
|
147
163
|
treeFilterMode,
|
|
164
|
+
persistFoldState,
|
|
148
165
|
};
|
|
149
166
|
} catch {
|
|
150
167
|
return {
|
|
151
168
|
keys: { ...DEFAULT_KEYS },
|
|
152
169
|
treeFilterMode: DEFAULT_TREE_FILTER_MODE,
|
|
170
|
+
persistFoldState: DEFAULT_PERSIST_FOLD_STATE,
|
|
153
171
|
};
|
|
154
172
|
}
|
|
155
173
|
};
|
|
@@ -473,6 +491,10 @@ class anycopyOverlay implements Focusable {
|
|
|
473
491
|
private getTree: () => SessionTreeNode[],
|
|
474
492
|
private nodeById: Map<string, SessionTreeNode>,
|
|
475
493
|
private keys: anycopyKeyConfig,
|
|
494
|
+
private onExplicitFoldMutation: ((
|
|
495
|
+
beforeTransientFoldedNodeIds: string[],
|
|
496
|
+
afterTransientFoldedNodeIds: string[],
|
|
497
|
+
) => void) | null,
|
|
476
498
|
private onClose: () => void,
|
|
477
499
|
private getTermHeight: () => number,
|
|
478
500
|
private requestRender: () => void,
|
|
@@ -539,7 +561,18 @@ class anycopyOverlay implements Focusable {
|
|
|
539
561
|
return;
|
|
540
562
|
}
|
|
541
563
|
|
|
564
|
+
const keybindings = getKeybindings();
|
|
565
|
+
const shouldTrackExplicitFoldMutation =
|
|
566
|
+
this.onExplicitFoldMutation !== null &&
|
|
567
|
+
(keybindings.matches(data, "app.tree.foldOrUp") || keybindings.matches(data, "app.tree.unfoldOrDown"));
|
|
568
|
+
const beforeTransientFoldedNodeIds = shouldTrackExplicitFoldMutation ? getSelectorFoldedNodeIds(this.selector) : null;
|
|
569
|
+
|
|
542
570
|
this.selector.handleInput(data);
|
|
571
|
+
|
|
572
|
+
if (beforeTransientFoldedNodeIds) {
|
|
573
|
+
this.onExplicitFoldMutation?.(beforeTransientFoldedNodeIds, getSelectorFoldedNodeIds(this.selector));
|
|
574
|
+
}
|
|
575
|
+
|
|
543
576
|
this.requestRender();
|
|
544
577
|
}
|
|
545
578
|
|
|
@@ -783,6 +816,7 @@ export default function anycopyExtension(pi: ExtensionAPI) {
|
|
|
783
816
|
const config = loadConfig();
|
|
784
817
|
const keys = config.keys;
|
|
785
818
|
const treeFilterMode = config.treeFilterMode;
|
|
819
|
+
const persistFoldState = config.persistFoldState;
|
|
786
820
|
|
|
787
821
|
const openAnycopy = async (
|
|
788
822
|
ctx: ExtensionCommandContext,
|
|
@@ -812,6 +846,13 @@ export default function anycopyExtension(pi: ExtensionAPI) {
|
|
|
812
846
|
await ctx.ui.custom<void>((tui, theme, _kb, done) => {
|
|
813
847
|
const termRows = tui.terminal?.rows ?? 40;
|
|
814
848
|
const treeTermHeight = Math.floor(termRows * 0.65);
|
|
849
|
+
const nodeById = buildNodeMap(initialTree);
|
|
850
|
+
const validNodeIds = new Set(nodeById.keys());
|
|
851
|
+
const restoredFoldState = persistFoldState
|
|
852
|
+
? loadLatestFoldStateFromEntries(ctx.sessionManager.getEntries() as SessionEntry[], validNodeIds)
|
|
853
|
+
: null;
|
|
854
|
+
let durableFoldedNodeIds = restoredFoldState?.foldedNodeIds ?? [];
|
|
855
|
+
let lastPersistedFoldedNodeIds = durableFoldedNodeIds;
|
|
815
856
|
|
|
816
857
|
const selector = new TreeSelectorComponent(
|
|
817
858
|
initialTree,
|
|
@@ -844,14 +885,60 @@ export default function anycopyExtension(pi: ExtensionAPI) {
|
|
|
844
885
|
opts?.initialSelectedId,
|
|
845
886
|
treeFilterMode,
|
|
846
887
|
);
|
|
847
|
-
const effectiveLeafIdForNoop = selector.getTreeList().getSelectedNode()?.entry.id ?? currentLeafId;
|
|
848
888
|
|
|
849
|
-
|
|
889
|
+
if (persistFoldState) {
|
|
890
|
+
const restoredFoldedNodeIds = normalizeFoldedNodeIds(
|
|
891
|
+
setSelectorFoldedNodeIds(selector, durableFoldedNodeIds),
|
|
892
|
+
validNodeIds,
|
|
893
|
+
);
|
|
894
|
+
durableFoldedNodeIds = restoredFoldedNodeIds;
|
|
895
|
+
lastPersistedFoldedNodeIds = restoredFoldedNodeIds;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
const persistDurableFoldState = (nextDurableFoldedNodeIds: string[]): void => {
|
|
899
|
+
if (!persistFoldState || foldStateNodeIdListsEqual(nextDurableFoldedNodeIds, lastPersistedFoldedNodeIds)) {
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
try {
|
|
904
|
+
pi.appendEntry(
|
|
905
|
+
ANYCOPY_FOLD_STATE_CUSTOM_TYPE,
|
|
906
|
+
createFoldStateEntryData(nextDurableFoldedNodeIds, validNodeIds),
|
|
907
|
+
);
|
|
908
|
+
lastPersistedFoldedNodeIds = nextDurableFoldedNodeIds;
|
|
909
|
+
} catch (error) {
|
|
910
|
+
ctx.ui.notify(
|
|
911
|
+
error instanceof Error ? error.message : "Failed to persist /anycopy fold state",
|
|
912
|
+
"error",
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
|
|
917
|
+
const handleExplicitFoldMutation = (
|
|
918
|
+
beforeTransientFoldedNodeIds: string[],
|
|
919
|
+
afterTransientFoldedNodeIds: string[],
|
|
920
|
+
): void => {
|
|
921
|
+
const nextDurableFoldedNodeIds = mergeExplicitFoldMutation({
|
|
922
|
+
durableFoldedNodeIds,
|
|
923
|
+
beforeTransientFoldedNodeIds,
|
|
924
|
+
afterTransientFoldedNodeIds,
|
|
925
|
+
validNodeIds,
|
|
926
|
+
});
|
|
927
|
+
if (foldStateNodeIdListsEqual(nextDurableFoldedNodeIds, durableFoldedNodeIds)) {
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
durableFoldedNodeIds = nextDurableFoldedNodeIds;
|
|
932
|
+
persistDurableFoldState(nextDurableFoldedNodeIds);
|
|
933
|
+
};
|
|
934
|
+
|
|
935
|
+
const effectiveLeafIdForNoop = selector.getTreeList().getSelectedNode()?.entry.id ?? currentLeafId;
|
|
850
936
|
const overlay = new anycopyOverlay(
|
|
851
937
|
selector,
|
|
852
938
|
getTree,
|
|
853
939
|
nodeById,
|
|
854
940
|
keys,
|
|
941
|
+
persistFoldState ? handleExplicitFoldMutation : null,
|
|
855
942
|
() => done(),
|
|
856
943
|
() => tui.terminal?.rows ?? 40,
|
|
857
944
|
() => tui.requestRender(),
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-anycopy",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Pi's /tree with a live syntax-highlighted preview
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"description": "Pi's /tree with a live syntax-highlighted preview, ability to copy any node(s) to the clipboard, and persistence of folded branches",
|
|
5
5
|
"keywords": ["pi-package", "pi", "pi-coding-agent"],
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|