pi-anycopy 0.1.4 → 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/README.md +15 -16
- package/extensions/anycopy/config.json +0 -1
- package/extensions/anycopy/enter-navigation.ts +101 -0
- package/extensions/anycopy/index.ts +76 -45
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# anycopy for Pi (`pi-anycopy`)
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
By comparison to Pi's native `/copy` (copies only the last assistant message) and `/md` (bulk-exports the entire branch as a Markdown transcript), `/anycopy` allows you to navigate the full session tree, preview each node's content with syntax highlighting, and copy to the clipboard any node(s) from the tree.
|
|
3
|
+
This extension mirrors all the behaviors of Pi's native `/tree` while adding a live, syntax-highlighting preview of each node's content and the ability to copy any node(s) to the clipboard.
|
|
6
4
|
|
|
7
5
|
<p align="center">
|
|
8
6
|
<img width="450" alt="anycopy demo" src="https://raw.githubusercontent.com/w-winter/dot314/main/assets/anycopy-demo.gif" />
|
|
@@ -42,42 +40,43 @@ Restart Pi after installation.
|
|
|
42
40
|
/anycopy
|
|
43
41
|
```
|
|
44
42
|
|
|
45
|
-
You can also open the overlay via the configurable shortcut in `config.json` without clearing the current editor draft. The default is **ctrl+`**.
|
|
46
|
-
|
|
47
43
|
## Keys
|
|
48
44
|
|
|
49
45
|
Defaults (customizable in `config.json`):
|
|
50
46
|
|
|
51
47
|
| Key | Action |
|
|
52
48
|
|-----|--------|
|
|
53
|
-
| `
|
|
54
|
-
| `
|
|
49
|
+
| `Enter` | Navigate to the focused node (same semantics as `/tree`) |
|
|
50
|
+
| `Space` | Select/unselect focused node for copy |
|
|
51
|
+
| `Shift+C` | Copy selected nodes, or the focused node if nothing is selected |
|
|
55
52
|
| `Shift+X` | Clear selection |
|
|
56
53
|
| `Shift+L` | Label node (native tree behavior) |
|
|
57
54
|
| `Shift+Up` / `Shift+Down` | Scroll node preview by line |
|
|
58
55
|
| `Shift+Left` / `Shift+Right` | Page through node preview |
|
|
59
|
-
| `Esc
|
|
56
|
+
| `Esc` | Close |
|
|
60
57
|
|
|
61
58
|
Notes:
|
|
59
|
+
- `Enter` always navigates the focused node, not the marked set
|
|
60
|
+
- After `Enter`, `/anycopy` offers the same summary choices as `/tree`: `No summary`, `Summarize`, and `Summarize with custom prompt`
|
|
61
|
+
- If `branchSummary.skipPrompt` is `true` in Pi settings, `/anycopy` matches native `/tree` and skips the summary chooser, defaulting to no summary
|
|
62
|
+
- Escaping the summary chooser reopens `/anycopy` with focus restored to the node you tried to select
|
|
63
|
+
- Cancelling the custom summarization editor returns to the summary chooser
|
|
62
64
|
- If no nodes are selected, `Shift+C` copies the focused node
|
|
63
|
-
-
|
|
65
|
+
- Single-node copies use just that node's content; role prefixes like `user:` or `assistant:` are only added when copying 2 or more nodes
|
|
66
|
+
- When copying multiple selected nodes, they are auto-sorted chronologically by position in the session tree, not by selection order
|
|
67
|
+
- Space/`Shift+C` multi-select copy behavior is unchanged by navigation support
|
|
64
68
|
- Label edits are persisted via `pi.setLabel(...)`
|
|
65
|
-
- Despite reoffering node labeling (`/anycopy` is arguably a better UI than `/tree` to also perform this action in), this extension doesn't offer a full reproduction of `/tree`'s other features (e.g., branch switching and summarization are not included)
|
|
66
69
|
|
|
67
70
|
## Configuration
|
|
68
71
|
|
|
69
72
|
Edit `~/.pi/agent/extensions/anycopy/config.json`:
|
|
70
73
|
|
|
71
|
-
- `
|
|
72
|
-
- default: **ctrl+`**
|
|
73
|
-
- set to `null` to disable it, or change it to another Pi key id such as `ctrl+a`
|
|
74
|
-
- `treeFilterMode`: initial tree filter mode when opening `/anycopy` (idea sourced from [lajarre](https://github.com/lajarre)'s [pi-mono/issues/1845](https://github.com/badlogic/pi-mono/issues/1845))
|
|
74
|
+
- `treeFilterMode`: initial tree filter mode when opening `/anycopy`; defaults to `default` to match `/tree`
|
|
75
75
|
- one of: `default` | `no-tools` | `user-only` | `labeled-only` | `all`
|
|
76
|
-
- `keys`: keybindings used inside the `/anycopy` overlay
|
|
76
|
+
- `keys`: keybindings used inside the `/anycopy` overlay for copy/preview actions
|
|
77
77
|
|
|
78
78
|
```json
|
|
79
79
|
{
|
|
80
|
-
"shortcut": "ctrl+`",
|
|
81
80
|
"treeFilterMode": "default",
|
|
82
81
|
"keys": {
|
|
83
82
|
"toggleSelect": "space",
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
export type AnycopyEnterNavigationResult = "reopen" | "closed";
|
|
2
|
+
|
|
3
|
+
export type AnycopySummaryChoice = "No summary" | "Summarize" | "Summarize with custom prompt";
|
|
4
|
+
|
|
5
|
+
export type AnycopyEnterNavigationDeps = {
|
|
6
|
+
entryId: string;
|
|
7
|
+
effectiveLeafIdForNoop: string | null;
|
|
8
|
+
skipSummaryPrompt: boolean;
|
|
9
|
+
close: () => void;
|
|
10
|
+
reopen: (options: { initialSelectedId: string }) => void;
|
|
11
|
+
navigateTree: (
|
|
12
|
+
targetId: string,
|
|
13
|
+
options?: { summarize?: boolean; customInstructions?: string },
|
|
14
|
+
) => Promise<{ cancelled: boolean; aborted?: boolean }>;
|
|
15
|
+
ui: {
|
|
16
|
+
select: (title: string, options: AnycopySummaryChoice[]) => Promise<AnycopySummaryChoice | undefined>;
|
|
17
|
+
editor: (title: string) => Promise<string | undefined>;
|
|
18
|
+
setStatus: (source: string, message: string) => void;
|
|
19
|
+
setWorkingMessage: (message?: string) => void;
|
|
20
|
+
notify: (message: string, level: "error") => void;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export async function runAnycopyEnterNavigation(
|
|
25
|
+
deps: AnycopyEnterNavigationDeps,
|
|
26
|
+
): Promise<AnycopyEnterNavigationResult> {
|
|
27
|
+
const { entryId, effectiveLeafIdForNoop, skipSummaryPrompt, close, reopen, navigateTree, ui } = deps;
|
|
28
|
+
|
|
29
|
+
if (effectiveLeafIdForNoop !== null && entryId === effectiveLeafIdForNoop) {
|
|
30
|
+
close();
|
|
31
|
+
ui.setStatus("anycopy", "Already at this point");
|
|
32
|
+
return "closed";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
close();
|
|
36
|
+
|
|
37
|
+
let wantsSummary = false;
|
|
38
|
+
let customInstructions: string | undefined;
|
|
39
|
+
|
|
40
|
+
if (!skipSummaryPrompt) {
|
|
41
|
+
while (true) {
|
|
42
|
+
const choice = await ui.select("Summarize branch?", [
|
|
43
|
+
"No summary",
|
|
44
|
+
"Summarize",
|
|
45
|
+
"Summarize with custom prompt",
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
if (choice === undefined) {
|
|
49
|
+
reopen({ initialSelectedId: entryId });
|
|
50
|
+
return "reopen";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (choice === "No summary") {
|
|
54
|
+
wantsSummary = false;
|
|
55
|
+
customInstructions = undefined;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (choice === "Summarize") {
|
|
60
|
+
wantsSummary = true;
|
|
61
|
+
customInstructions = undefined;
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
customInstructions = await ui.editor("Custom summarization instructions");
|
|
66
|
+
if (customInstructions === undefined) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
wantsSummary = true;
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
ui.setWorkingMessage("Navigating tree…");
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const result = await navigateTree(entryId, {
|
|
79
|
+
summarize: wantsSummary,
|
|
80
|
+
customInstructions,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
if (result.cancelled) {
|
|
84
|
+
if (wantsSummary) {
|
|
85
|
+
ui.setStatus("anycopy", "Branch summarization cancelled");
|
|
86
|
+
reopen({ initialSelectedId: entryId });
|
|
87
|
+
return "reopen";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
ui.setStatus("anycopy", "Navigation cancelled");
|
|
91
|
+
return "closed";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return "closed";
|
|
95
|
+
} catch (error) {
|
|
96
|
+
ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
97
|
+
return "closed";
|
|
98
|
+
} finally {
|
|
99
|
+
ui.setWorkingMessage();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* Esc - close
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import type { ExtensionAPI, ExtensionCommandContext,
|
|
16
|
+
import type { ExtensionAPI, ExtensionCommandContext, SessionEntry } from "@mariozechner/pi-coding-agent";
|
|
17
17
|
import {
|
|
18
18
|
copyToClipboard,
|
|
19
19
|
getLanguageFromPath,
|
|
@@ -26,9 +26,12 @@ import { Markdown, matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
|
|
|
26
26
|
import type { Focusable } from "@mariozechner/pi-tui";
|
|
27
27
|
|
|
28
28
|
import { existsSync, readFileSync } from "fs";
|
|
29
|
+
import { homedir } from "os";
|
|
29
30
|
import { dirname, join } from "path";
|
|
30
31
|
import { fileURLToPath } from "url";
|
|
31
32
|
|
|
33
|
+
import { runAnycopyEnterNavigation } from "./enter-navigation.ts";
|
|
34
|
+
|
|
32
35
|
type SessionTreeNode = {
|
|
33
36
|
entry: SessionEntry;
|
|
34
37
|
children: SessionTreeNode[];
|
|
@@ -48,17 +51,21 @@ type anycopyKeyConfig = {
|
|
|
48
51
|
type TreeFilterMode = "default" | "no-tools" | "user-only" | "labeled-only" | "all";
|
|
49
52
|
|
|
50
53
|
type anycopyConfig = {
|
|
51
|
-
shortcut?: string | null;
|
|
52
54
|
keys?: Partial<anycopyKeyConfig>;
|
|
53
55
|
treeFilterMode?: TreeFilterMode;
|
|
54
56
|
};
|
|
55
57
|
|
|
56
58
|
type anycopyRuntimeConfig = {
|
|
57
|
-
shortcut: string | null;
|
|
58
59
|
keys: anycopyKeyConfig;
|
|
59
60
|
treeFilterMode: TreeFilterMode;
|
|
60
61
|
};
|
|
61
62
|
|
|
63
|
+
type BranchSummarySettingsFile = {
|
|
64
|
+
branchSummary?: {
|
|
65
|
+
skipPrompt?: boolean;
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
|
|
62
69
|
const DEFAULT_KEYS: anycopyKeyConfig = {
|
|
63
70
|
toggleSelect: "space",
|
|
64
71
|
copy: "shift+c",
|
|
@@ -70,7 +77,6 @@ const DEFAULT_KEYS: anycopyKeyConfig = {
|
|
|
70
77
|
};
|
|
71
78
|
|
|
72
79
|
const DEFAULT_TREE_FILTER_MODE: TreeFilterMode = "default";
|
|
73
|
-
const DEFAULT_SHORTCUT = "ctrl+`";
|
|
74
80
|
|
|
75
81
|
const getExtensionDir = (): string => {
|
|
76
82
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
@@ -78,11 +84,32 @@ const getExtensionDir = (): string => {
|
|
|
78
84
|
return dirname(fileURLToPath(import.meta.url));
|
|
79
85
|
};
|
|
80
86
|
|
|
87
|
+
const getAgentDir = (): string => process.env.PI_CODING_AGENT_DIR || join(homedir(), ".pi", "agent");
|
|
88
|
+
|
|
89
|
+
const readJsonFile = <T>(path: string): T | undefined => {
|
|
90
|
+
if (!existsSync(path)) return undefined;
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
return JSON.parse(readFileSync(path, "utf8")) as T;
|
|
94
|
+
} catch {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const loadBranchSummarySkipPrompt = (cwd: string): boolean => {
|
|
100
|
+
const globalSettings = readJsonFile<BranchSummarySettingsFile>(join(getAgentDir(), "settings.json"));
|
|
101
|
+
const projectSettings = readJsonFile<BranchSummarySettingsFile>(join(cwd, ".pi", "settings.json"));
|
|
102
|
+
const projectSkipPrompt = projectSettings?.branchSummary?.skipPrompt;
|
|
103
|
+
if (typeof projectSkipPrompt === "boolean") return projectSkipPrompt;
|
|
104
|
+
|
|
105
|
+
const globalSkipPrompt = globalSettings?.branchSummary?.skipPrompt;
|
|
106
|
+
return typeof globalSkipPrompt === "boolean" ? globalSkipPrompt : false;
|
|
107
|
+
};
|
|
108
|
+
|
|
81
109
|
const loadConfig = (): anycopyRuntimeConfig => {
|
|
82
110
|
const configPath = join(getExtensionDir(), "config.json");
|
|
83
111
|
if (!existsSync(configPath)) {
|
|
84
112
|
return {
|
|
85
|
-
shortcut: DEFAULT_SHORTCUT,
|
|
86
113
|
keys: { ...DEFAULT_KEYS },
|
|
87
114
|
treeFilterMode: DEFAULT_TREE_FILTER_MODE,
|
|
88
115
|
};
|
|
@@ -92,8 +119,6 @@ const loadConfig = (): anycopyRuntimeConfig => {
|
|
|
92
119
|
const raw = readFileSync(configPath, "utf8");
|
|
93
120
|
const parsed = JSON.parse(raw) as anycopyConfig;
|
|
94
121
|
const keys = parsed.keys ?? {};
|
|
95
|
-
const shortcut =
|
|
96
|
-
parsed.shortcut === null ? null : typeof parsed.shortcut === "string" ? parsed.shortcut : DEFAULT_SHORTCUT;
|
|
97
122
|
const treeFilterModeRaw = parsed.treeFilterMode;
|
|
98
123
|
const validTreeFilterModes: TreeFilterMode[] = ["default", "no-tools", "user-only", "labeled-only", "all"];
|
|
99
124
|
const treeFilterMode =
|
|
@@ -102,7 +127,6 @@ const loadConfig = (): anycopyRuntimeConfig => {
|
|
|
102
127
|
: DEFAULT_TREE_FILTER_MODE;
|
|
103
128
|
|
|
104
129
|
return {
|
|
105
|
-
shortcut,
|
|
106
130
|
keys: {
|
|
107
131
|
toggleSelect: typeof keys.toggleSelect === "string" ? keys.toggleSelect : DEFAULT_KEYS.toggleSelect,
|
|
108
132
|
copy: typeof keys.copy === "string" ? keys.copy : DEFAULT_KEYS.copy,
|
|
@@ -116,7 +140,6 @@ const loadConfig = (): anycopyRuntimeConfig => {
|
|
|
116
140
|
};
|
|
117
141
|
} catch {
|
|
118
142
|
return {
|
|
119
|
-
shortcut: DEFAULT_SHORTCUT,
|
|
120
143
|
keys: { ...DEFAULT_KEYS },
|
|
121
144
|
treeFilterMode: DEFAULT_TREE_FILTER_MODE,
|
|
122
145
|
};
|
|
@@ -345,10 +368,14 @@ const buildNodeOrder = (roots: SessionTreeNode[]): Map<string, number> => {
|
|
|
345
368
|
return order;
|
|
346
369
|
};
|
|
347
370
|
|
|
348
|
-
/** Clipboard text
|
|
371
|
+
/** Clipboard text omits role prefix for a single node and includes it for multi-node copies
|
|
349
372
|
* The preview pane is truncated for performance, while the clipboard copy is not
|
|
350
373
|
*/
|
|
351
374
|
const buildClipboardText = (nodes: SessionTreeNode[]): string => {
|
|
375
|
+
if (nodes.length === 1) {
|
|
376
|
+
return getEntryContent(nodes[0]!.entry);
|
|
377
|
+
}
|
|
378
|
+
|
|
352
379
|
return nodes
|
|
353
380
|
.map((node) => {
|
|
354
381
|
const label = getEntryRoleLabel(node.entry);
|
|
@@ -377,7 +404,6 @@ class anycopyOverlay implements Focusable {
|
|
|
377
404
|
private getTree: () => SessionTreeNode[],
|
|
378
405
|
private nodeById: Map<string, SessionTreeNode>,
|
|
379
406
|
private keys: anycopyKeyConfig,
|
|
380
|
-
private closeShortcut: string | null,
|
|
381
407
|
private onClose: () => void,
|
|
382
408
|
private getTermHeight: () => number,
|
|
383
409
|
private requestRender: () => void,
|
|
@@ -397,10 +423,12 @@ class anycopyOverlay implements Focusable {
|
|
|
397
423
|
}
|
|
398
424
|
|
|
399
425
|
handleInput(data: string): void {
|
|
400
|
-
if (this.
|
|
401
|
-
this.
|
|
426
|
+
if (this.isEditingNodeLabel()) {
|
|
427
|
+
this.selector.handleInput(data);
|
|
428
|
+
this.requestRender();
|
|
402
429
|
return;
|
|
403
430
|
}
|
|
431
|
+
|
|
404
432
|
if (matchesKey(data, this.keys.toggleSelect)) {
|
|
405
433
|
this.toggleSelectedFocusedNode();
|
|
406
434
|
return;
|
|
@@ -441,6 +469,10 @@ class anycopyOverlay implements Focusable {
|
|
|
441
469
|
this.requestRender();
|
|
442
470
|
}
|
|
443
471
|
|
|
472
|
+
private isEditingNodeLabel(): boolean {
|
|
473
|
+
return Boolean((this.selector as { labelInput?: unknown }).labelInput);
|
|
474
|
+
}
|
|
475
|
+
|
|
444
476
|
invalidate(): void {
|
|
445
477
|
// Preview is derived from focused entry + width; invalidate forces recompute
|
|
446
478
|
this.previewCache = null;
|
|
@@ -551,12 +583,12 @@ class anycopyOverlay implements Focusable {
|
|
|
551
583
|
}
|
|
552
584
|
|
|
553
585
|
private renderTreeHeaderHint(width: number): string {
|
|
554
|
-
const closeHint = this.closeShortcut ? `${formatKeyHint(this.closeShortcut)}/Esc: close` : "Esc: close";
|
|
555
586
|
const hint =
|
|
556
|
-
` │
|
|
587
|
+
` │ Enter: navigate` +
|
|
588
|
+
` • ${formatKeyHint(this.keys.toggleSelect)}: select` +
|
|
557
589
|
` • ${formatKeyHint(this.keys.copy)}: copy` +
|
|
558
590
|
` • ${formatKeyHint(this.keys.clear)}: clear` +
|
|
559
|
-
` •
|
|
591
|
+
` • Esc: close`;
|
|
560
592
|
return truncateToWidth(this.theme.fg("dim", hint), width);
|
|
561
593
|
}
|
|
562
594
|
|
|
@@ -636,7 +668,7 @@ class anycopyOverlay implements Focusable {
|
|
|
636
668
|
const selectorLines = this.selector.render(width);
|
|
637
669
|
const headerHint = this.renderTreeHeaderHint(width);
|
|
638
670
|
|
|
639
|
-
// Inject
|
|
671
|
+
// Inject action hints near the tree header (above the list)
|
|
640
672
|
const insertAfter = Math.max(0, selectorLines.findIndex((l) => l.includes("Type to search")));
|
|
641
673
|
if (selectorLines.length > 0) {
|
|
642
674
|
const idx = insertAfter >= 0 ? insertAfter + 1 : 1;
|
|
@@ -670,11 +702,13 @@ class anycopyOverlay implements Focusable {
|
|
|
670
702
|
|
|
671
703
|
export default function anycopyExtension(pi: ExtensionAPI) {
|
|
672
704
|
const config = loadConfig();
|
|
673
|
-
const shortcut = config.shortcut;
|
|
674
705
|
const keys = config.keys;
|
|
675
706
|
const treeFilterMode = config.treeFilterMode;
|
|
676
707
|
|
|
677
|
-
const openAnycopy = async (
|
|
708
|
+
const openAnycopy = async (
|
|
709
|
+
ctx: ExtensionCommandContext,
|
|
710
|
+
opts?: { initialSelectedId?: string },
|
|
711
|
+
) => {
|
|
678
712
|
if (!ctx.hasUI) return;
|
|
679
713
|
|
|
680
714
|
const initialTree = ctx.sessionManager.getTree() as SessionTreeNode[];
|
|
@@ -685,35 +719,50 @@ export default function anycopyExtension(pi: ExtensionAPI) {
|
|
|
685
719
|
|
|
686
720
|
const getTree = () => ctx.sessionManager.getTree() as SessionTreeNode[];
|
|
687
721
|
const currentLeafId = ctx.sessionManager.getLeafId();
|
|
722
|
+
const skipSummaryPrompt = loadBranchSummarySkipPrompt(ctx.cwd);
|
|
688
723
|
|
|
689
724
|
await ctx.ui.custom<void>((tui, theme, _kb, done) => {
|
|
690
725
|
const termRows = tui.terminal?.rows ?? 40;
|
|
691
|
-
// Pass reduced height so tree takes ~35% of terminal (it uses floor(h/2) internally)
|
|
692
726
|
const treeTermHeight = Math.floor(termRows * 0.65);
|
|
693
727
|
|
|
694
728
|
const selector = new TreeSelectorComponent(
|
|
695
729
|
initialTree,
|
|
696
730
|
currentLeafId,
|
|
697
731
|
treeTermHeight,
|
|
698
|
-
() => {
|
|
699
|
-
|
|
700
|
-
|
|
732
|
+
(entryId) => {
|
|
733
|
+
void runAnycopyEnterNavigation({
|
|
734
|
+
entryId,
|
|
735
|
+
effectiveLeafIdForNoop,
|
|
736
|
+
skipSummaryPrompt,
|
|
737
|
+
close: done,
|
|
738
|
+
reopen: (reopenOpts) => {
|
|
739
|
+
void openAnycopy(ctx, reopenOpts);
|
|
740
|
+
},
|
|
741
|
+
navigateTree: async (targetId, options) => ctx.navigateTree(targetId, options),
|
|
742
|
+
ui: {
|
|
743
|
+
select: (title, options) => ctx.ui.select(title, options),
|
|
744
|
+
editor: (title) => ctx.ui.editor(title),
|
|
745
|
+
setStatus: (source, message) => ctx.ui.setStatus(source, message),
|
|
746
|
+
setWorkingMessage: (message) => ctx.ui.setWorkingMessage(message),
|
|
747
|
+
notify: (message, level) => ctx.ui.notify(message, level),
|
|
748
|
+
},
|
|
749
|
+
});
|
|
701
750
|
},
|
|
702
751
|
() => done(),
|
|
703
752
|
(entryId, label) => {
|
|
704
753
|
pi.setLabel(entryId, label);
|
|
705
754
|
},
|
|
755
|
+
opts?.initialSelectedId,
|
|
756
|
+
treeFilterMode,
|
|
706
757
|
);
|
|
758
|
+
const effectiveLeafIdForNoop = selector.getTreeList().getSelectedNode()?.entry.id ?? currentLeafId;
|
|
707
759
|
|
|
708
|
-
// Build a node map once for parent traversal (toolResult → parent assistant toolCall args)
|
|
709
760
|
const nodeById = buildNodeMap(initialTree);
|
|
710
|
-
|
|
711
761
|
const overlay = new anycopyOverlay(
|
|
712
762
|
selector,
|
|
713
763
|
getTree,
|
|
714
764
|
nodeById,
|
|
715
765
|
keys,
|
|
716
|
-
shortcut,
|
|
717
766
|
() => done(),
|
|
718
767
|
() => tui.terminal?.rows ?? 40,
|
|
719
768
|
() => tui.requestRender(),
|
|
@@ -721,15 +770,6 @@ export default function anycopyExtension(pi: ExtensionAPI) {
|
|
|
721
770
|
);
|
|
722
771
|
|
|
723
772
|
const treeList = selector.getTreeList();
|
|
724
|
-
|
|
725
|
-
// Set initial tree filter mode (same semantics as `/tree`)
|
|
726
|
-
const rawTreeList = treeList as any;
|
|
727
|
-
if (rawTreeList && typeof rawTreeList === "object") {
|
|
728
|
-
rawTreeList.filterMode = treeFilterMode;
|
|
729
|
-
if (typeof rawTreeList.applyFilter === "function") rawTreeList.applyFilter();
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
// Monkey-patch render to inject checkbox markers (✓/○) into tree rows
|
|
733
773
|
const originalRender = treeList.render.bind(treeList);
|
|
734
774
|
treeList.render = (width: number) => {
|
|
735
775
|
const innerWidth = Math.max(10, width - 2);
|
|
@@ -766,7 +806,7 @@ export default function anycopyExtension(pi: ExtensionAPI) {
|
|
|
766
806
|
if (typeof nodeId !== "string") return " " + line;
|
|
767
807
|
|
|
768
808
|
const selected = overlay.isSelectedNode(nodeId);
|
|
769
|
-
const marker = selected ? theme.fg("success", "
|
|
809
|
+
const marker = selected ? theme.fg("success", "✓ ") : theme.fg("dim", "○ ");
|
|
770
810
|
return marker + line;
|
|
771
811
|
});
|
|
772
812
|
};
|
|
@@ -782,13 +822,4 @@ export default function anycopyExtension(pi: ExtensionAPI) {
|
|
|
782
822
|
await openAnycopy(ctx);
|
|
783
823
|
},
|
|
784
824
|
});
|
|
785
|
-
|
|
786
|
-
if (shortcut) {
|
|
787
|
-
pi.registerShortcut(shortcut as any, {
|
|
788
|
-
description: "Open anycopy session tree overlay",
|
|
789
|
-
handler: async (ctx) => {
|
|
790
|
-
await openAnycopy(ctx);
|
|
791
|
-
},
|
|
792
|
-
});
|
|
793
|
-
}
|
|
794
825
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-anycopy",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Pi's /tree with a live syntax-highlighted preview and ability to copy any node(s) to the clipboard",
|
|
5
5
|
"keywords": ["pi-package", "pi", "pi-coding-agent"],
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"dot314Prepack": {
|
|
29
29
|
"copy": [
|
|
30
30
|
{ "from": "../../extensions/anycopy/index.ts", "to": "extensions/anycopy/index.ts" },
|
|
31
|
+
{ "from": "../../extensions/anycopy/enter-navigation.ts", "to": "extensions/anycopy/enter-navigation.ts" },
|
|
31
32
|
{ "from": "../../extensions/anycopy/config.json", "to": "extensions/anycopy/config.json" },
|
|
32
33
|
{ "from": "../../LICENSE", "to": "LICENSE" }
|
|
33
34
|
],
|