pi-copy-message 1.0.1 → 1.0.3
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 +8 -0
- package/README.md +8 -1
- package/extensions/copy-message.ts +69 -12
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.3 - 2026-06-07
|
|
4
|
+
|
|
5
|
+
- Register `/copy-message` before `/copy-user` so package command autocomplete prefers the picker over the shortcut.
|
|
6
|
+
|
|
7
|
+
## 1.0.2 - 2026-06-07
|
|
8
|
+
|
|
9
|
+
- Add `/copy-user` shortcut for copying the most recent user message directly.
|
|
10
|
+
|
|
3
11
|
## 1.0.1 - 2026-06-07
|
|
4
12
|
|
|
5
13
|
- Improve npm package description and keywords.
|
package/README.md
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
# pi copy-message extension
|
|
2
2
|
|
|
3
|
-
A [pi](https://github.com/earendil-works/pi-mono) extension that adds `/copy-message`: a keyboard-first picker for copying raw session message text without terminal wrapping, padding, or rendered TUI artifacts.
|
|
3
|
+
A [pi](https://github.com/earendil-works/pi-mono) extension that adds `/copy-message`: a keyboard-first picker for copying raw session message text without terminal wrapping, padding, or rendered TUI artifacts. It also adds `/copy-user` as a direct shortcut for the most recent user message.
|
|
4
4
|
|
|
5
5
|
`pi-copy-message` supersedes [`pi-copy-user-message`](https://github.com/fitchmultz/pi-copy-user-message), which only copied the most recent user message.
|
|
6
6
|
|
|
7
7
|
## What it does
|
|
8
8
|
|
|
9
9
|
- Adds `/copy-message`
|
|
10
|
+
- Adds `/copy-user` for copying the most recent user message directly
|
|
10
11
|
- Copies raw stored session message text, not rendered terminal lines
|
|
11
12
|
- Shows messages in chat order: oldest at top, newest at bottom
|
|
12
13
|
- Selects the newest visible message by default
|
|
@@ -50,6 +51,12 @@ Open the picker:
|
|
|
50
51
|
/copy-message
|
|
51
52
|
```
|
|
52
53
|
|
|
54
|
+
Copy the most recent user message directly:
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
/copy-user
|
|
58
|
+
```
|
|
59
|
+
|
|
53
60
|
Copy the latest visible default message directly:
|
|
54
61
|
|
|
55
62
|
```text
|
|
@@ -86,29 +86,65 @@ function formatTime(timestamp: unknown): string {
|
|
|
86
86
|
return date.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" });
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
function entryToCopyableMessage(entry: unknown): CopyableMessage | undefined {
|
|
90
|
+
if (entry === null || typeof entry !== "object") return undefined;
|
|
91
|
+
const record = entry as Record<string, unknown>;
|
|
92
|
+
if (record.type !== "message") return undefined;
|
|
93
|
+
if (record.message === null || typeof record.message !== "object") return undefined;
|
|
94
|
+
|
|
95
|
+
const message = record.message as Record<string, unknown>;
|
|
96
|
+
const role = typeof message.role === "string" ? message.role : "message";
|
|
97
|
+
const text = textFromMessage(message);
|
|
98
|
+
if (!text.trim()) return undefined;
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
id: typeof record.id === "string" ? record.id : "unknown",
|
|
102
|
+
role,
|
|
103
|
+
timestamp: typeof record.timestamp === "string" ? record.timestamp : undefined,
|
|
104
|
+
text,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
89
108
|
export function collectCopyableMessages(ctx: { sessionManager: { getBranch(): unknown[] } }): CopyableMessage[] {
|
|
90
|
-
|
|
109
|
+
return ctx.sessionManager.getBranch().flatMap((entry) => {
|
|
110
|
+
const message = entryToCopyableMessage(entry);
|
|
111
|
+
return message ? [message] : [];
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export type MostRecentUserMessageResult =
|
|
116
|
+
| { kind: "message"; message: CopyableMessage }
|
|
117
|
+
| { kind: "no-user-message" }
|
|
118
|
+
| { kind: "no-text" };
|
|
91
119
|
|
|
92
|
-
|
|
120
|
+
export function getMostRecentUserMessage(ctx: { sessionManager: { getBranch(): unknown[] } }): MostRecentUserMessageResult {
|
|
121
|
+
const branch = ctx.sessionManager.getBranch();
|
|
122
|
+
|
|
123
|
+
for (let i = branch.length - 1; i >= 0; i--) {
|
|
124
|
+
const entry = branch[i];
|
|
93
125
|
if (entry === null || typeof entry !== "object") continue;
|
|
94
126
|
const record = entry as Record<string, unknown>;
|
|
95
127
|
if (record.type !== "message") continue;
|
|
96
128
|
if (record.message === null || typeof record.message !== "object") continue;
|
|
97
129
|
|
|
98
130
|
const message = record.message as Record<string, unknown>;
|
|
99
|
-
|
|
131
|
+
if (message.role !== "user") continue;
|
|
132
|
+
|
|
100
133
|
const text = textFromMessage(message);
|
|
101
|
-
if (!text.trim())
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
134
|
+
if (!text.trim()) return { kind: "no-text" };
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
kind: "message",
|
|
138
|
+
message: {
|
|
139
|
+
id: typeof record.id === "string" ? record.id : "unknown",
|
|
140
|
+
role: "user",
|
|
141
|
+
timestamp: typeof record.timestamp === "string" ? record.timestamp : undefined,
|
|
142
|
+
text,
|
|
143
|
+
},
|
|
144
|
+
};
|
|
109
145
|
}
|
|
110
146
|
|
|
111
|
-
return
|
|
147
|
+
return { kind: "no-user-message" };
|
|
112
148
|
}
|
|
113
149
|
|
|
114
150
|
function commandExists(command: string): boolean {
|
|
@@ -419,6 +455,20 @@ function copySelectedMessage(ctx: Pick<ExtensionCommandContext, "ui">, selected:
|
|
|
419
455
|
ctx.ui.notify(`Copied ${roleLabel(selected.role)} message`, "info");
|
|
420
456
|
}
|
|
421
457
|
|
|
458
|
+
function copyMostRecentUserMessage(ctx: Pick<ExtensionCommandContext, "sessionManager" | "ui">) {
|
|
459
|
+
const result = getMostRecentUserMessage(ctx);
|
|
460
|
+
if (result.kind === "no-user-message") {
|
|
461
|
+
ctx.ui.notify("No user messages found", "warning");
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
if (result.kind === "no-text") {
|
|
465
|
+
ctx.ui.notify("The most recent user message has no text to copy", "warning");
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
copySelectedMessage(ctx, result.message);
|
|
470
|
+
}
|
|
471
|
+
|
|
422
472
|
export default function copyMessageExtension(pi: Pick<ExtensionAPI, "registerCommand">) {
|
|
423
473
|
pi.registerCommand("copy-message", {
|
|
424
474
|
description: "Select a session message and copy its raw text to the clipboard",
|
|
@@ -447,4 +497,11 @@ export default function copyMessageExtension(pi: Pick<ExtensionAPI, "registerCom
|
|
|
447
497
|
copySelectedMessage(ctx, selected);
|
|
448
498
|
},
|
|
449
499
|
});
|
|
500
|
+
|
|
501
|
+
pi.registerCommand("copy-user", {
|
|
502
|
+
description: "Copy the most recent user message to the clipboard",
|
|
503
|
+
handler: async (_args, ctx) => {
|
|
504
|
+
copyMostRecentUserMessage(ctx);
|
|
505
|
+
},
|
|
506
|
+
});
|
|
450
507
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-copy-message",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Pi extension with a searchable TUI picker for copying raw session messages",
|
|
5
5
|
"author": "Mitch Fultz (https://github.com/fitchmultz)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"extension",
|
|
16
16
|
"clipboard",
|
|
17
17
|
"copy-message",
|
|
18
|
+
"copy-user",
|
|
18
19
|
"copy",
|
|
19
20
|
"session-messages",
|
|
20
21
|
"slash-command",
|