pi-anycopy 0.2.3 → 0.2.5

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 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+Left` / `Shift+Right` | Page through node preview |
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+left",
97
- "pageDown": "shift+right"
96
+ "pageUp": "shift+pageup",
97
+ "pageDown": "shift+pagedown"
98
98
  }
99
99
  }
100
100
  ```
@@ -8,7 +8,7 @@
8
8
  "toggleLabelTimestamps": "shift+t",
9
9
  "scrollUp": "shift+up",
10
10
  "scrollDown": "shift+down",
11
- "pageUp": "shift+left",
12
- "pageDown": "shift+right"
11
+ "pageUp": "shift+pageup",
12
+ "pageDown": "shift+pagedown"
13
13
  }
14
14
  }
@@ -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+←/→ - page preview
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
- labelTimestamp?: string;
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+right",
92
- pageUp: "shift+left",
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 buildLatestLabelTimestamps = (entries: SessionEntry[]): Map<string, string> => {
398
- const timestampsById = new Map<string, string>();
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
- this.showLabelTimestamps = !this.showLabelTimestamps;
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 buildAnnotatedTree = (): SessionTreeNode[] => {
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 = () => buildAnnotatedTree();
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
- const tl = treeList as any;
955
- const filteredRaw = tl.filteredNodes;
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(selectedIdx - Math.floor(maxVisible / 2), filtered.length - maxVisible),
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 decorateStatusLine(line);
912
+ if (i >= treeRowCount) return truncateToWidth(` ${line}`, width);
985
913
 
986
- const nodeIdx = startIdx + i;
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 selected = overlay.isSelectedNode(nodeId);
992
- const marker = selected ? theme.fg("success", "✓ ") : theme.fg("dim", "○ ");
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",
3
+ "version": "0.2.5",
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",
@@ -18,7 +18,7 @@
18
18
  "image": "https://raw.githubusercontent.com/w-winter/dot314/main/assets/anycopy-demo.gif"
19
19
  },
20
20
  "peerDependencies": {
21
- "@mariozechner/pi-coding-agent": "*",
21
+ "@mariozechner/pi-coding-agent": "^0.65.0",
22
22
  "@mariozechner/pi-tui": "*"
23
23
  },
24
24
  "scripts": {