pi-copy-message 1.0.11 → 2.1.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/CHANGELOG.md +12 -0
- package/README.md +11 -8
- package/extensions/copy-message.ts +68 -16
- package/package.json +11 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.1.0 - 2026-08-17
|
|
4
|
+
|
|
5
|
+
- Add an `Alt+C` picker filter and `MessageVisibility.showCustom` option for showing or hiding custom messages.
|
|
6
|
+
- Hide picker shortcut hints that collide with configured selection bindings.
|
|
7
|
+
|
|
8
|
+
## 2.0.0 - 2026-08-06
|
|
9
|
+
|
|
10
|
+
- Set pi 0.84.0 as the minimum supported version and refresh the local coding-agent and TUI development baseline.
|
|
11
|
+
- Honor configured `tui.select.pageUp` and `tui.select.pageDown` bindings, with Ctrl-modified page and jump controls that remain available in fullscreen mode.
|
|
12
|
+
- Decode Kitty CSI-u printable input so type-to-filter search works in terminals using Kitty keyboard disambiguation.
|
|
13
|
+
- Validate the package and its core copy flows with an isolated Pi 0.84.0 installation.
|
|
14
|
+
|
|
3
15
|
## 1.0.11 - 2026-07-16
|
|
4
16
|
|
|
5
17
|
- Refresh the local Pi development lock and compatibility baseline to 0.80.9; the unified model runtime and additive Kimi/model-catalog changes do not affect the message-history picker.
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ A [pi](https://github.com/earendil-works/pi-mono) extension that adds `/copy-mes
|
|
|
11
11
|
- Copies raw stored session message text, not rendered terminal lines
|
|
12
12
|
- Shows messages in chat order: oldest at top, newest at bottom
|
|
13
13
|
- Selects the newest visible message by default
|
|
14
|
-
- Supports role filters for user, assistant,
|
|
14
|
+
- Supports role filters for user, assistant, tool/bash, and custom messages
|
|
15
15
|
- Hides tool/bash messages by default
|
|
16
16
|
- Supports type-to-filter search across role and message text, with `time:<term>` for timestamp search
|
|
17
17
|
- Supports Home/End jumps for oldest/newest visible messages
|
|
@@ -93,14 +93,17 @@ Copy with role and timestamp metadata instead of raw text only:
|
|
|
93
93
|
|---|---|
|
|
94
94
|
| Configured `tui.select.up` (default: `↑`) | Move to older visible message |
|
|
95
95
|
| Configured `tui.select.down` (default: `↓`) | Move to newer visible message |
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
96
|
+
| Configured `tui.select.pageUp` / `tui.select.pageDown` (default: `PageUp` / `PageDown`) | Move by one visible page in regular mode |
|
|
97
|
+
| `Ctrl+PageUp` / `Ctrl+PageDown` | Move by one visible page in fullscreen mode |
|
|
98
|
+
| `Home` / `End` | Jump to oldest/newest visible message in regular mode |
|
|
99
|
+
| `Ctrl+Home` / `Ctrl+End` | Jump to oldest/newest visible message in fullscreen mode |
|
|
98
100
|
| Type text | Filter visible messages |
|
|
99
101
|
| `time:<term>` | Search timestamps |
|
|
100
102
|
| `Backspace` | Delete one search character |
|
|
101
103
|
| `Ctrl+U` | Toggle user messages |
|
|
102
104
|
| `Ctrl+A` | Toggle assistant messages |
|
|
103
105
|
| `Ctrl+T` | Toggle tool/bash messages |
|
|
106
|
+
| `Alt+C` | Toggle custom messages |
|
|
104
107
|
| `Tab` | Toggle a wrapped preview of the selected message |
|
|
105
108
|
| `Alt+M` | Toggle raw vs metadata copy format |
|
|
106
109
|
| Configured `tui.select.confirm` (default: `Enter`) | Copy selected message text |
|
|
@@ -114,16 +117,16 @@ Copy with role and timestamp metadata instead of raw text only:
|
|
|
114
117
|
- General search does not match timestamps; use `time:<term>` when you want to search by displayed time.
|
|
115
118
|
- Filter labels honor the active pi theme.
|
|
116
119
|
- Selection key hints show the active Pi bindings. Configured selection actions take precedence if they collide with the picker's filter, preview, or format shortcuts.
|
|
117
|
-
-
|
|
120
|
+
- Only custom messages with `display: true` can appear or be copied from current `custom_message` entries or legacy message-wrapped entries; visible ones can be toggled with `Alt+C`.
|
|
118
121
|
- Copy notifications include the role and a short preview so you can verify what was copied.
|
|
119
|
-
- `/copy-message latest`
|
|
122
|
+
- `/copy-message latest` and numbered selectors include user, assistant, and visible custom messages by default while hiding tool/bash messages. `Alt+C` only affects the picker. If only hidden messages exist, `latest` falls back to the newest message so the command still does something useful.
|
|
120
123
|
- `/copy-message` with no direct selector requires interactive TUI mode because the picker is a custom TUI component.
|
|
121
124
|
- Direct commands such as `/copy-user`, `/copy-message latest`, and `/copy-message 3` do not require TUI mode, though non-UI modes may not display notifications.
|
|
122
125
|
|
|
123
126
|
## Compatibility
|
|
124
127
|
|
|
125
|
-
-
|
|
126
|
-
- Pi
|
|
128
|
+
- Requires and is tested with Pi 0.84.0 or later
|
|
129
|
+
- Pi includes `Ctrl+X` for copying the latest assistant response; this extension remains useful for searchable history, other roles, metadata, and direct selectors.
|
|
127
130
|
- Supported Node.js range for local repo tooling: `>=22.19.0`
|
|
128
131
|
- `.nvmrc` pins Node 22.19.0 for local development
|
|
129
132
|
|
|
@@ -139,4 +142,4 @@ npm run check
|
|
|
139
142
|
Key files:
|
|
140
143
|
|
|
141
144
|
- `extensions/copy-message.ts` — publishable extension implementation
|
|
142
|
-
- `tests/copy-message.test.ts` — regression tests for command wiring, filtering, search,
|
|
145
|
+
- `tests/copy-message.test.ts` — regression tests for command wiring, filtering, search, navigation, and copy formatting
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import type { ExtensionAPI, ExtensionCommandContext, KeybindingsManager } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import type { AutocompleteItem } from "@earendil-works/pi-tui";
|
|
4
|
-
import { matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
3
|
+
import type { AutocompleteItem, TuiMode } from "@earendil-works/pi-tui";
|
|
4
|
+
import { decodeKittyPrintable, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
5
5
|
|
|
6
6
|
const MAX_VISIBLE_MESSAGES = 8;
|
|
7
7
|
const MAX_PEEK_LINES = 16;
|
|
@@ -241,12 +241,14 @@ export interface MessageVisibility {
|
|
|
241
241
|
showAssistant: boolean;
|
|
242
242
|
showUser: boolean;
|
|
243
243
|
showTools: boolean;
|
|
244
|
+
showCustom: boolean;
|
|
244
245
|
}
|
|
245
246
|
|
|
246
247
|
function isVisibleMessage(message: CopyableMessage, visibility: MessageVisibility): boolean {
|
|
247
248
|
if (isToolMessage(message)) return visibility.showTools;
|
|
248
249
|
if (message.role === "assistant") return visibility.showAssistant;
|
|
249
250
|
if (message.role === "user") return visibility.showUser;
|
|
251
|
+
if (message.role === "custom") return visibility.showCustom;
|
|
250
252
|
return true;
|
|
251
253
|
}
|
|
252
254
|
|
|
@@ -274,7 +276,7 @@ export function filteredMessages(messages: CopyableMessage[], visibility: Messag
|
|
|
274
276
|
}
|
|
275
277
|
|
|
276
278
|
export function defaultVisibleMessages(messages: CopyableMessage[]): CopyableMessage[] {
|
|
277
|
-
return filteredMessages(messages, { showAssistant: true, showUser: true, showTools: false });
|
|
279
|
+
return filteredMessages(messages, { showAssistant: true, showUser: true, showTools: false, showCustom: true });
|
|
278
280
|
}
|
|
279
281
|
|
|
280
282
|
export function latestDefaultMessage(messages: CopyableMessage[]): CopyableMessage | undefined {
|
|
@@ -370,7 +372,7 @@ function bindingHint(keybindings: PickerKeybindings | undefined, action: "up" |
|
|
|
370
372
|
return keys.reduce((shortest, key) => visibleWidth(key) < visibleWidth(shortest) ? key : shortest, keys[0] ?? "unbound");
|
|
371
373
|
}
|
|
372
374
|
|
|
373
|
-
function helpLines(width: number, keybindings
|
|
375
|
+
function helpLines(width: number, keybindings: PickerKeybindings | undefined, tuiMode: TuiMode, hasCustomMessages: boolean): string[] {
|
|
374
376
|
const up = bindingHint(keybindings, "up");
|
|
375
377
|
const down = bindingHint(keybindings, "down");
|
|
376
378
|
const confirm = bindingHint(keybindings, "confirm");
|
|
@@ -379,6 +381,8 @@ function helpLines(width: number, keybindings?: PickerKeybindings): string[] {
|
|
|
379
381
|
!keybindings ||
|
|
380
382
|
!(keybindings.matches(data, "tui.select.up") ||
|
|
381
383
|
keybindings.matches(data, "tui.select.down") ||
|
|
384
|
+
keybindings.matches(data, "tui.select.pageUp") ||
|
|
385
|
+
keybindings.matches(data, "tui.select.pageDown") ||
|
|
382
386
|
keybindings.matches(data, "tui.select.confirm") ||
|
|
383
387
|
keybindings.matches(data, "tui.select.cancel"));
|
|
384
388
|
const peek = available("\t") ? "Tab peek" : undefined;
|
|
@@ -388,11 +392,17 @@ function helpLines(width: number, keybindings?: PickerKeybindings): string[] {
|
|
|
388
392
|
{ hint: "T", data: "\x14" },
|
|
389
393
|
].filter(({ data }) => available(data));
|
|
390
394
|
const filterHint = filters.length > 0 ? `Ctrl+${filters.map(({ hint }) => hint).join("/")} filters` : undefined;
|
|
391
|
-
const
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
395
|
+
const custom = hasCustomMessages && available("\x1b[99;3u") ? "Alt+C custom" : undefined;
|
|
396
|
+
const meta = available("\x1b[109;3u") ? "Alt+M meta" : undefined;
|
|
397
|
+
const jumps = (tuiMode === "fullscreen"
|
|
398
|
+
? [
|
|
399
|
+
{ hint: "Ctrl+Home", data: "\x1b[1;5H" },
|
|
400
|
+
{ hint: "Ctrl+End", data: "\x1b[1;5F" },
|
|
401
|
+
]
|
|
402
|
+
: [
|
|
403
|
+
{ hint: "Home", data: "\x1b[H" },
|
|
404
|
+
{ hint: "End", data: "\x1b[F" },
|
|
405
|
+
]).filter(({ data }) => available(data));
|
|
396
406
|
const jumpHint = jumps.length > 0 ? `${jumps.map(({ hint }) => hint).join("/")} jump` : undefined;
|
|
397
407
|
const join = (...hints: Array<string | undefined>) => hints.filter(Boolean).join(" · ");
|
|
398
408
|
const core = width < 74 ? [`${up}/${down} nav`, `${confirm} copy`, `${cancel} cancel`] : [`${up} older`, `${down} newer`, `${confirm} copy`, `${cancel} cancel`];
|
|
@@ -408,7 +418,7 @@ function helpLines(width: number, keybindings?: PickerKeybindings): string[] {
|
|
|
408
418
|
return lines;
|
|
409
419
|
}
|
|
410
420
|
|
|
411
|
-
for (const hint of width < 74 ? [jumpHint, peek, filterHint, meta] : ["type search", jumpHint, peek, filterHint, meta]) {
|
|
421
|
+
for (const hint of width < 74 ? [jumpHint, peek, filterHint, custom, meta] : ["type search", jumpHint, peek, filterHint, custom, meta]) {
|
|
412
422
|
if (hint && visibleWidth(join(...optional, hint, ...core)) <= width) optional.push(hint);
|
|
413
423
|
}
|
|
414
424
|
|
|
@@ -422,6 +432,7 @@ export class CopyMessagePickerState {
|
|
|
422
432
|
showAssistant: true,
|
|
423
433
|
showUser: true,
|
|
424
434
|
showTools: false,
|
|
435
|
+
showCustom: true,
|
|
425
436
|
};
|
|
426
437
|
search = "";
|
|
427
438
|
visibleMessages: CopyableMessage[];
|
|
@@ -445,10 +456,11 @@ export class CopyMessagePickerState {
|
|
|
445
456
|
return selected ? formatMessageForCopy(selected, this.format) : undefined;
|
|
446
457
|
}
|
|
447
458
|
|
|
448
|
-
render(width: number, theme: CopyMessageTheme, keybindings?: PickerKeybindings): string[] {
|
|
459
|
+
render(width: number, theme: CopyMessageTheme, keybindings?: PickerKeybindings, tuiMode: TuiMode = "regular"): string[] {
|
|
449
460
|
const maxVisible = Math.min(this.visibleMessages.length, MAX_VISIBLE_MESSAGES);
|
|
450
461
|
const start = maxVisible === 0 ? 0 : Math.max(0, Math.min(this.selectedIndex - maxVisible + 1, this.visibleMessages.length - maxVisible));
|
|
451
462
|
const end = Math.min(this.visibleMessages.length, start + maxVisible);
|
|
463
|
+
const hasCustomMessages = this.messages.some((message) => message.role === "custom");
|
|
452
464
|
const userState = filterLabel(theme, "user", this.visibility.showUser, "warning");
|
|
453
465
|
const assistantState = filterLabel(theme, "assistant", this.visibility.showAssistant, "accent");
|
|
454
466
|
const toolState = filterLabel(theme, "tools", this.visibility.showTools, "dim");
|
|
@@ -474,9 +486,11 @@ export class CopyMessagePickerState {
|
|
|
474
486
|
}
|
|
475
487
|
|
|
476
488
|
const position = this.visibleMessages.length === 0 ? "0/0" : `${this.selectedIndex + 1}/${this.visibleMessages.length}`;
|
|
477
|
-
|
|
489
|
+
const filters = [userState, assistantState, toolState];
|
|
490
|
+
if (hasCustomMessages) filters.push(filterLabel(theme, "custom", this.visibility.showCustom, "dim"));
|
|
491
|
+
lines.push([theme.fg("dim", `(${position})`), ...filters, searchState, formatState].join(" · "));
|
|
478
492
|
lines.push("");
|
|
479
|
-
lines.push(...helpLines(width, keybindings).map((line) => hotkeyHint(theme, line)));
|
|
493
|
+
lines.push(...helpLines(width, keybindings, tuiMode, hasCustomMessages).map((line) => hotkeyHint(theme, line)));
|
|
480
494
|
lines.push("");
|
|
481
495
|
return lines.map((line) => truncateToWidth(line, width, ""));
|
|
482
496
|
}
|
|
@@ -490,6 +504,14 @@ export class CopyMessagePickerState {
|
|
|
490
504
|
this.move(1);
|
|
491
505
|
return "render";
|
|
492
506
|
}
|
|
507
|
+
if (keybindings?.matches(data, "tui.select.pageUp")) {
|
|
508
|
+
this.move(-MAX_VISIBLE_MESSAGES);
|
|
509
|
+
return "render";
|
|
510
|
+
}
|
|
511
|
+
if (keybindings?.matches(data, "tui.select.pageDown")) {
|
|
512
|
+
this.move(MAX_VISIBLE_MESSAGES);
|
|
513
|
+
return "render";
|
|
514
|
+
}
|
|
493
515
|
if (keybindings?.matches(data, "tui.select.confirm")) {
|
|
494
516
|
return this.visibleMessages.length > 0 ? "copy" : "none";
|
|
495
517
|
}
|
|
@@ -509,6 +531,11 @@ export class CopyMessagePickerState {
|
|
|
509
531
|
this.refreshMessages();
|
|
510
532
|
return "render";
|
|
511
533
|
}
|
|
534
|
+
if (matchesKey(data, "alt+c")) {
|
|
535
|
+
this.visibility.showCustom = !this.visibility.showCustom;
|
|
536
|
+
this.refreshMessages();
|
|
537
|
+
return "render";
|
|
538
|
+
}
|
|
512
539
|
if (matchesKey(data, "alt+m")) {
|
|
513
540
|
this.format = this.format === "raw" ? "metadata" : "raw";
|
|
514
541
|
return "render";
|
|
@@ -521,8 +548,9 @@ export class CopyMessagePickerState {
|
|
|
521
548
|
this.setSearch(this.search.slice(0, -1));
|
|
522
549
|
return "render";
|
|
523
550
|
}
|
|
524
|
-
|
|
525
|
-
|
|
551
|
+
const printable = decodeKittyPrintable(data) ?? data;
|
|
552
|
+
if (isPrintableSearchInput(printable)) {
|
|
553
|
+
this.setSearch(this.search + printable);
|
|
526
554
|
return "render";
|
|
527
555
|
}
|
|
528
556
|
if (!keybindings && matchesKey(data, "up")) {
|
|
@@ -533,6 +561,30 @@ export class CopyMessagePickerState {
|
|
|
533
561
|
this.move(1);
|
|
534
562
|
return "render";
|
|
535
563
|
}
|
|
564
|
+
if (!keybindings && matchesKey(data, "pageUp")) {
|
|
565
|
+
this.move(-MAX_VISIBLE_MESSAGES);
|
|
566
|
+
return "render";
|
|
567
|
+
}
|
|
568
|
+
if (!keybindings && matchesKey(data, "pageDown")) {
|
|
569
|
+
this.move(MAX_VISIBLE_MESSAGES);
|
|
570
|
+
return "render";
|
|
571
|
+
}
|
|
572
|
+
if (matchesKey(data, "ctrl+pageUp")) {
|
|
573
|
+
this.move(-MAX_VISIBLE_MESSAGES);
|
|
574
|
+
return "render";
|
|
575
|
+
}
|
|
576
|
+
if (matchesKey(data, "ctrl+pageDown")) {
|
|
577
|
+
this.move(MAX_VISIBLE_MESSAGES);
|
|
578
|
+
return "render";
|
|
579
|
+
}
|
|
580
|
+
if (matchesKey(data, "ctrl+home")) {
|
|
581
|
+
this.jumpToTop();
|
|
582
|
+
return "render";
|
|
583
|
+
}
|
|
584
|
+
if (matchesKey(data, "ctrl+end")) {
|
|
585
|
+
this.jumpToBottom();
|
|
586
|
+
return "render";
|
|
587
|
+
}
|
|
536
588
|
if (matchesKey(data, "home")) {
|
|
537
589
|
this.jumpToTop();
|
|
538
590
|
return "render";
|
|
@@ -631,7 +683,7 @@ async function pickMessage(ctx: ExtensionCommandContext, messages: CopyableMessa
|
|
|
631
683
|
const state = new CopyMessagePickerState(messages, initialFormat);
|
|
632
684
|
return {
|
|
633
685
|
render(width: number) {
|
|
634
|
-
return state.render(width, theme, keybindings);
|
|
686
|
+
return state.render(width, theme, keybindings, tui.mode);
|
|
635
687
|
},
|
|
636
688
|
invalidate() {},
|
|
637
689
|
handleInput(data: string) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-copy-message",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "2.1.0",
|
|
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",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"LICENSE"
|
|
45
45
|
],
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"@earendil-works/pi-coding-agent": "
|
|
48
|
-
"@earendil-works/pi-tui": "
|
|
47
|
+
"@earendil-works/pi-coding-agent": "0.84.0",
|
|
48
|
+
"@earendil-works/pi-tui": "0.84.0",
|
|
49
49
|
"@types/node": "^25.9.1",
|
|
50
50
|
"tsx": "^4.22.4",
|
|
51
51
|
"typescript": "^6.0.3"
|
|
@@ -59,5 +59,13 @@
|
|
|
59
59
|
"peerDependencies": {
|
|
60
60
|
"@earendil-works/pi-coding-agent": "*",
|
|
61
61
|
"@earendil-works/pi-tui": "*"
|
|
62
|
+
},
|
|
63
|
+
"peerDependenciesMeta": {
|
|
64
|
+
"@earendil-works/pi-coding-agent": {
|
|
65
|
+
"optional": true
|
|
66
|
+
},
|
|
67
|
+
"@earendil-works/pi-tui": {
|
|
68
|
+
"optional": true
|
|
69
|
+
}
|
|
62
70
|
}
|
|
63
71
|
}
|