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 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, and tool/bash messages
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
- | `Home` | Jump to oldest visible message |
97
- | `End` | Jump to newest visible message |
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
- - Hidden custom messages are excluded in both current `custom_message` entries and legacy message-wrapped entries; only messages with `display: true` can appear or be copied.
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` respects default visibility: user and assistant messages are visible, tool/bash messages are hidden. If only hidden messages exist, it falls back to the newest message so the command still does something useful.
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
- - Tested with pi 0.80.9
126
- - Pi 0.80.7 includes `Ctrl+X` for copying the latest assistant response; this extension remains useful for searchable history, other roles, metadata, and direct selectors.
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, jumps, and clipboard behavior
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?: PickerKeybindings): string[] {
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 meta = available("\x1bm") ? "Alt+M meta" : undefined;
392
- const jumps = [
393
- { hint: "Home", data: "\x1b[H" },
394
- { hint: "End", data: "\x1b[F" },
395
- ].filter(({ data }) => available(data));
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
- lines.push(`${theme.fg("dim", `(${position})`)} · ${userState} · ${assistantState} · ${toolState} · ${formatState} · ${searchState}`);
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
- if (isPrintableSearchInput(data)) {
525
- this.setSearch(this.search + data);
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.11",
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": "^0.80.9",
48
- "@earendil-works/pi-tui": "^0.80.9",
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
  }