pi-anycopy 0.2.3 → 0.2.4
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 +3 -3
- package/extensions/anycopy/config.json +2 -2
- package/extensions/anycopy/index.ts +38 -115
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ Defaults (customizable in `config.json`):
|
|
|
53
53
|
| `Shift+L` | Label node (native tree behavior) |
|
|
54
54
|
| `Shift+T` | Toggle label timestamps for labeled nodes |
|
|
55
55
|
| `Shift+Up` / `Shift+Down` | Scroll node preview by line |
|
|
56
|
-
| `Shift+
|
|
56
|
+
| `Shift+PageUp` / `Shift+PageDown` | Page through node preview |
|
|
57
57
|
| `Esc` | Close |
|
|
58
58
|
|
|
59
59
|
Notes:
|
|
@@ -93,8 +93,8 @@ Edit `~/.pi/agent/extensions/anycopy/config.json`:
|
|
|
93
93
|
"toggleLabelTimestamps": "shift+t",
|
|
94
94
|
"scrollUp": "shift+up",
|
|
95
95
|
"scrollDown": "shift+down",
|
|
96
|
-
"pageUp": "shift+
|
|
97
|
-
"pageDown": "shift+
|
|
96
|
+
"pageUp": "shift+pageup",
|
|
97
|
+
"pageDown": "shift+pagedown"
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
```
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* Shift+L - label node
|
|
11
11
|
* Shift+T - toggle label timestamps for labeled nodes
|
|
12
12
|
* Shift+↑/↓ - scroll preview
|
|
13
|
-
* Shift
|
|
13
|
+
* Shift+PageUp/PageDown - page preview
|
|
14
14
|
* Esc - close
|
|
15
15
|
*/
|
|
16
16
|
|
|
@@ -47,7 +47,15 @@ type SessionTreeNode = {
|
|
|
47
47
|
entry: SessionEntry;
|
|
48
48
|
children: SessionTreeNode[];
|
|
49
49
|
label?: string;
|
|
50
|
-
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
type anycopyTreeList = ReturnType<TreeSelectorComponent["getTreeList"]>;
|
|
53
|
+
|
|
54
|
+
type anycopyTreeListInternals = anycopyTreeList & {
|
|
55
|
+
filteredNodes: Array<{ node: SessionTreeNode }>;
|
|
56
|
+
selectedIndex: number;
|
|
57
|
+
maxVisibleLines: number;
|
|
58
|
+
showLabelTimestamps: boolean;
|
|
51
59
|
};
|
|
52
60
|
|
|
53
61
|
type anycopyKeyConfig = {
|
|
@@ -88,8 +96,8 @@ const DEFAULT_KEYS: anycopyKeyConfig = {
|
|
|
88
96
|
toggleLabelTimestamps: "shift+t",
|
|
89
97
|
scrollDown: "shift+down",
|
|
90
98
|
scrollUp: "shift+up",
|
|
91
|
-
pageDown: "shift+
|
|
92
|
-
pageUp: "shift+
|
|
99
|
+
pageDown: "shift+pagedown",
|
|
100
|
+
pageUp: "shift+pageup",
|
|
93
101
|
};
|
|
94
102
|
|
|
95
103
|
const DEFAULT_TREE_FILTER_MODE: TreeFilterMode = "default";
|
|
@@ -394,64 +402,8 @@ const buildNodeOrder = (roots: SessionTreeNode[]): Map<string, number> => {
|
|
|
394
402
|
return order;
|
|
395
403
|
};
|
|
396
404
|
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
for (const entry of entries) {
|
|
400
|
-
if (entry.type !== "label") continue;
|
|
401
|
-
if (entry.label) {
|
|
402
|
-
timestampsById.set(entry.targetId, entry.timestamp);
|
|
403
|
-
} else {
|
|
404
|
-
timestampsById.delete(entry.targetId);
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
return timestampsById;
|
|
408
|
-
};
|
|
409
|
-
|
|
410
|
-
const applyLabelTimestampsToTree = (roots: SessionTreeNode[], timestampsById: Map<string, string>): void => {
|
|
411
|
-
const stack = [...roots];
|
|
412
|
-
while (stack.length > 0) {
|
|
413
|
-
const node = stack.pop()!;
|
|
414
|
-
node.labelTimestamp = timestampsById.get(node.entry.id);
|
|
415
|
-
for (const child of node.children) stack.push(child);
|
|
416
|
-
}
|
|
417
|
-
};
|
|
418
|
-
|
|
419
|
-
const formatLabelTimestamp = (timestamp: string): string => {
|
|
420
|
-
const date = new Date(timestamp);
|
|
421
|
-
if (Number.isNaN(date.getTime())) return timestamp;
|
|
422
|
-
|
|
423
|
-
const now = new Date();
|
|
424
|
-
const hours = date.getHours().toString().padStart(2, "0");
|
|
425
|
-
const minutes = date.getMinutes().toString().padStart(2, "0");
|
|
426
|
-
const time = `${hours}:${minutes}`;
|
|
427
|
-
|
|
428
|
-
if (
|
|
429
|
-
date.getFullYear() === now.getFullYear() &&
|
|
430
|
-
date.getMonth() === now.getMonth() &&
|
|
431
|
-
date.getDate() === now.getDate()
|
|
432
|
-
) {
|
|
433
|
-
return time;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
const month = date.getMonth() + 1;
|
|
437
|
-
const day = date.getDate();
|
|
438
|
-
if (date.getFullYear() === now.getFullYear()) {
|
|
439
|
-
return `${month}/${day} ${time}`;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
const year = date.getFullYear().toString().slice(-2);
|
|
443
|
-
return `${year}/${month}/${day} ${time}`;
|
|
444
|
-
};
|
|
445
|
-
|
|
446
|
-
const insertLabelTimestampIntoLine = (line: string, node: SessionTreeNode, theme: any): string => {
|
|
447
|
-
if (!node.label || !node.labelTimestamp) return line;
|
|
448
|
-
|
|
449
|
-
const labelToken = `[${node.label}] `;
|
|
450
|
-
const labelIndex = line.indexOf(labelToken);
|
|
451
|
-
if (labelIndex === -1) return line;
|
|
452
|
-
|
|
453
|
-
const insertAt = labelIndex + labelToken.length;
|
|
454
|
-
return `${line.slice(0, insertAt)}${theme.fg("muted", `${formatLabelTimestamp(node.labelTimestamp)} `)}${line.slice(insertAt)}`;
|
|
405
|
+
const getTreeListInternals = (treeList: anycopyTreeList): anycopyTreeListInternals => {
|
|
406
|
+
return treeList as anycopyTreeListInternals;
|
|
455
407
|
};
|
|
456
408
|
|
|
457
409
|
/** Clipboard text omits role prefix for a single node and includes it for multi-node copies
|
|
@@ -478,7 +430,6 @@ class anycopyOverlay implements Focusable {
|
|
|
478
430
|
private _focused = false;
|
|
479
431
|
private previewScrollOffset = 0;
|
|
480
432
|
private lastPreviewHeight = 0;
|
|
481
|
-
private showLabelTimestamps = false;
|
|
482
433
|
private previewCache: {
|
|
483
434
|
entryId: string;
|
|
484
435
|
width: number;
|
|
@@ -495,7 +446,6 @@ class anycopyOverlay implements Focusable {
|
|
|
495
446
|
beforeTransientFoldedNodeIds: string[],
|
|
496
447
|
afterTransientFoldedNodeIds: string[],
|
|
497
448
|
) => void) | null,
|
|
498
|
-
private onClose: () => void,
|
|
499
449
|
private getTermHeight: () => number,
|
|
500
450
|
private requestRender: () => void,
|
|
501
451
|
private theme: any,
|
|
@@ -509,10 +459,14 @@ class anycopyOverlay implements Focusable {
|
|
|
509
459
|
this.selector.focused = value;
|
|
510
460
|
}
|
|
511
461
|
|
|
512
|
-
getTreeList() {
|
|
462
|
+
getTreeList(): anycopyTreeList {
|
|
513
463
|
return this.selector.getTreeList();
|
|
514
464
|
}
|
|
515
465
|
|
|
466
|
+
private getTreeListInternals(): anycopyTreeListInternals {
|
|
467
|
+
return getTreeListInternals(this.getTreeList());
|
|
468
|
+
}
|
|
469
|
+
|
|
516
470
|
handleInput(data: string): void {
|
|
517
471
|
if (this.isEditingNodeLabel()) {
|
|
518
472
|
this.selector.handleInput(data);
|
|
@@ -533,11 +487,17 @@ class anycopyOverlay implements Focusable {
|
|
|
533
487
|
return;
|
|
534
488
|
}
|
|
535
489
|
if (matchesKey(data, this.keys.toggleLabelTimestamps)) {
|
|
536
|
-
|
|
490
|
+
const treeList = this.getTreeListInternals();
|
|
491
|
+
treeList.showLabelTimestamps = !treeList.showLabelTimestamps;
|
|
537
492
|
this.requestRender();
|
|
538
493
|
return;
|
|
539
494
|
}
|
|
540
495
|
|
|
496
|
+
const keybindings = getKeybindings();
|
|
497
|
+
if (keybindings.matches(data, "app.tree.toggleLabelTimestamp")) {
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
541
501
|
if (matchesKey(data, this.keys.scrollDown)) {
|
|
542
502
|
this.previewScrollOffset += 1;
|
|
543
503
|
this.requestRender();
|
|
@@ -561,7 +521,6 @@ class anycopyOverlay implements Focusable {
|
|
|
561
521
|
return;
|
|
562
522
|
}
|
|
563
523
|
|
|
564
|
-
const keybindings = getKeybindings();
|
|
565
524
|
const shouldTrackExplicitFoldMutation =
|
|
566
525
|
this.onExplicitFoldMutation !== null &&
|
|
567
526
|
(keybindings.matches(data, "app.tree.foldOrUp") || keybindings.matches(data, "app.tree.unfoldOrDown"));
|
|
@@ -629,10 +588,6 @@ class anycopyOverlay implements Focusable {
|
|
|
629
588
|
return this.selectedNodeIds.has(id);
|
|
630
589
|
}
|
|
631
590
|
|
|
632
|
-
isShowingLabelTimestamps(): boolean {
|
|
633
|
-
return this.showLabelTimestamps;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
591
|
copySelectedOrFocusedNode(): void {
|
|
637
592
|
const focused = this.getFocusedNode();
|
|
638
593
|
const ids =
|
|
@@ -824,22 +779,13 @@ export default function anycopyExtension(pi: ExtensionAPI) {
|
|
|
824
779
|
) => {
|
|
825
780
|
if (!ctx.hasUI) return;
|
|
826
781
|
|
|
827
|
-
const
|
|
828
|
-
const tree = ctx.sessionManager.getTree() as SessionTreeNode[];
|
|
829
|
-
applyLabelTimestampsToTree(tree, buildLatestLabelTimestamps(ctx.sessionManager.getEntries() as SessionEntry[]));
|
|
830
|
-
return tree;
|
|
831
|
-
};
|
|
832
|
-
|
|
833
|
-
const initialTree = buildAnnotatedTree();
|
|
782
|
+
const initialTree = ctx.sessionManager.getTree() as SessionTreeNode[];
|
|
834
783
|
if (initialTree.length === 0) {
|
|
835
784
|
ctx.ui.notify("No entries in session", "warning");
|
|
836
785
|
return;
|
|
837
786
|
}
|
|
838
787
|
|
|
839
|
-
const getTree = () =>
|
|
840
|
-
const refreshInitialTreeLabelTimestamps = () => {
|
|
841
|
-
applyLabelTimestampsToTree(initialTree, buildLatestLabelTimestamps(ctx.sessionManager.getEntries() as SessionEntry[]));
|
|
842
|
-
};
|
|
788
|
+
const getTree = () => ctx.sessionManager.getTree() as SessionTreeNode[];
|
|
843
789
|
const currentLeafId = ctx.sessionManager.getLeafId();
|
|
844
790
|
const skipSummaryPrompt = loadBranchSummarySkipPrompt(ctx.cwd);
|
|
845
791
|
|
|
@@ -880,7 +826,6 @@ export default function anycopyExtension(pi: ExtensionAPI) {
|
|
|
880
826
|
() => done(),
|
|
881
827
|
(entryId, label) => {
|
|
882
828
|
pi.setLabel(entryId, label);
|
|
883
|
-
refreshInitialTreeLabelTimestamps();
|
|
884
829
|
},
|
|
885
830
|
opts?.initialSelectedId,
|
|
886
831
|
treeFilterMode,
|
|
@@ -939,60 +884,38 @@ export default function anycopyExtension(pi: ExtensionAPI) {
|
|
|
939
884
|
nodeById,
|
|
940
885
|
keys,
|
|
941
886
|
persistFoldState ? handleExplicitFoldMutation : null,
|
|
942
|
-
() => done(),
|
|
943
887
|
() => tui.terminal?.rows ?? 40,
|
|
944
888
|
() => tui.requestRender(),
|
|
945
889
|
theme,
|
|
946
890
|
);
|
|
947
891
|
|
|
948
892
|
const treeList = selector.getTreeList();
|
|
893
|
+
const treeListInternals = getTreeListInternals(treeList);
|
|
949
894
|
const originalRender = treeList.render.bind(treeList);
|
|
950
895
|
treeList.render = (width: number) => {
|
|
951
896
|
const innerWidth = Math.max(10, width - 2);
|
|
952
897
|
const lines = originalRender(innerWidth);
|
|
898
|
+
const filtered = treeListInternals.filteredNodes;
|
|
953
899
|
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
const decorateStatusLine = (line: string): string => {
|
|
957
|
-
const status = overlay.isShowingLabelTimestamps() ? `${line}${theme.fg("muted", " [+label time]")}` : line;
|
|
958
|
-
return truncateToWidth(` ${status}`, width);
|
|
959
|
-
};
|
|
960
|
-
|
|
961
|
-
if (!Array.isArray(filteredRaw) || filteredRaw.length === 0) {
|
|
962
|
-
return lines.map((line: string, i: number) =>
|
|
963
|
-
i === lines.length - 1 ? decorateStatusLine(line) : truncateToWidth(` ${line}`, width),
|
|
964
|
-
);
|
|
900
|
+
if (!Array.isArray(filtered) || filtered.length === 0) {
|
|
901
|
+
return lines.map((line: string) => truncateToWidth(` ${line}`, width));
|
|
965
902
|
}
|
|
966
|
-
const filtered = filteredRaw as { node: SessionTreeNode }[];
|
|
967
|
-
|
|
968
|
-
const selectedIdxRaw = tl.selectedIndex;
|
|
969
|
-
const maxVisibleRaw = tl.maxVisibleLines;
|
|
970
|
-
const selectedIdx =
|
|
971
|
-
typeof selectedIdxRaw === "number" && Number.isFinite(selectedIdxRaw) ? selectedIdxRaw : 0;
|
|
972
|
-
const maxVisible =
|
|
973
|
-
typeof maxVisibleRaw === "number" && Number.isFinite(maxVisibleRaw) && maxVisibleRaw > 0
|
|
974
|
-
? maxVisibleRaw
|
|
975
|
-
: filtered.length;
|
|
976
903
|
|
|
904
|
+
const maxVisible = Math.max(1, treeListInternals.maxVisibleLines);
|
|
977
905
|
const startIdx = Math.max(
|
|
978
906
|
0,
|
|
979
|
-
Math.min(
|
|
907
|
+
Math.min(treeListInternals.selectedIndex - Math.floor(maxVisible / 2), filtered.length - maxVisible),
|
|
980
908
|
);
|
|
981
909
|
const treeRowCount = Math.max(0, lines.length - 1);
|
|
982
910
|
|
|
983
911
|
return lines.map((line: string, i: number) => {
|
|
984
|
-
if (i >= treeRowCount) return
|
|
912
|
+
if (i >= treeRowCount) return truncateToWidth(` ${line}`, width);
|
|
985
913
|
|
|
986
|
-
const
|
|
987
|
-
const node = filtered[nodeIdx]?.node as SessionTreeNode | undefined;
|
|
988
|
-
const nodeId = node?.entry?.id;
|
|
914
|
+
const nodeId = filtered[startIdx + i]?.node.entry.id;
|
|
989
915
|
if (typeof nodeId !== "string") return truncateToWidth(` ${line}`, width);
|
|
990
916
|
|
|
991
|
-
const
|
|
992
|
-
|
|
993
|
-
const lineWithTimestamp =
|
|
994
|
-
overlay.isShowingLabelTimestamps() && node ? insertLabelTimestampIntoLine(line, node, theme) : line;
|
|
995
|
-
return truncateToWidth(marker + lineWithTimestamp, width);
|
|
917
|
+
const marker = overlay.isSelectedNode(nodeId) ? theme.fg("success", "✓ ") : theme.fg("dim", "○ ");
|
|
918
|
+
return truncateToWidth(marker + line, width);
|
|
996
919
|
});
|
|
997
920
|
};
|
|
998
921
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-anycopy",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
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",
|