pi-copy-picker 1.0.0 → 1.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 +10 -0
- package/README.md +11 -76
- package/RELEASING.md +2 -2
- package/package.json +1 -1
- package/src/history.ts +53 -45
- package/src/picker.ts +6 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 1.1.0 - 2026-11-10
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Show the number of matching items while searching.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Refactored message target parsing without changing selection or copy behavior.
|
|
14
|
+
|
|
5
15
|
## 1.0.0 - 2026-10-31
|
|
6
16
|
|
|
7
17
|
### Added
|
package/README.md
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# Pi copy picker
|
|
2
2
|
|
|
3
|
-

|
|
4
|
-
|
|
5
3
|
Browse and copy messages from the current Pi branch with `/copy-picker`.
|
|
6
4
|
The fullscreen picker has a paginated message list above a Markdown preview.
|
|
7
5
|
It selects the newest visible message and closes after a successful copy.
|
|
@@ -9,29 +7,15 @@ It selects the newest visible message and closes after a successful copy.
|
|
|
9
7
|

|
|
10
8
|
|
|
11
9
|
Requires interactive TUI mode and `@earendil-works/pi-coding-agent` and `@earendil-works/pi-tui` version 1.0.0 or newer. The host supplies these optional peer dependencies.
|
|
12
|
-
|
|
10
|
+
Tests have passed with versions 1.0.0 and 1.4.0.
|
|
13
11
|
|
|
14
12
|
## Open the picker
|
|
15
13
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
```sh
|
|
19
|
-
pi -e ./src/index.ts
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
For directory installation, run these commands from the checkout. No build is required:
|
|
23
|
-
|
|
24
|
-
```sh
|
|
25
|
-
pi install "$PWD"
|
|
26
|
-
pi -e .
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
After the first npm publication, install the published package with:
|
|
14
|
+
Install the package in Pi:
|
|
30
15
|
|
|
31
16
|
```sh
|
|
32
|
-
|
|
17
|
+
pi install npm:pi-copy-picker
|
|
33
18
|
```
|
|
34
|
-
|
|
35
19
|
Run `/copy-picker` in Pi. Use `/reload` or restart an existing session after installation or source changes.
|
|
36
20
|
The command does not change history or tool configuration.
|
|
37
21
|
|
|
@@ -62,67 +46,18 @@ A small-window message appears below 32 columns or when the list has fewer than
|
|
|
62
46
|
|
|
63
47
|
## Message and clipboard behavior
|
|
64
48
|
|
|
65
|
-
|
|
66
|
-
Abandoned branches are excluded. Reopen the picker to load newer messages.
|
|
67
|
-
Thinking, signatures, system messages, custom messages, and administrative entries are excluded. Assistant text records connected by tool-use steps form one reply, ending at stop, error, abort, a user message, or another history boundary. When tools are shown, unselected assistant text and tool events follow their original branch order.
|
|
68
|
-
|
|
69
|
-
User cards use the chat theme's user background and text colors. Assistant cards have no background.
|
|
70
|
-
The selected card has an accent border. Tool cards retain a dim border and the tool name.
|
|
71
|
-
The upper list shows original Markdown source in conversation order. Headings, lists, tables, references, and blank source lines stay literal. Long context lines wrap at the list width.
|
|
72
|
-
Each selected target has an accent frame with up to seven interior rows. The frame grows with its content and shrinks to fit short windows. Source lines appear in order, including code fences and punctuation. A single blank line stays blank. A run of blank lines becomes a muted `...`. If the source or its wrapped lines exceed the available rows, the last row is a muted `...`. Literal `...` remains ordinary source text. Preview and copy retain the complete selected entity. When tools are shown, a selected grouped reply frames its first segment and adds a muted `...` inside the frame if later segments contain text. Later segments remain unframed after their tool events. Preview and copy cover the whole reply. Selecting a block replaces only its original source interval. If a block spans tool events, its frame appears at the block's start and may summarize text that originally followed those events. The tool events remain in their original order, and text after the block remains visible. Changing selection centers the new frame, clamped at history ends. Reserved `...` rows indicate additional list content above and below. The bottom Pi-rendered preview shows only the selected entity.
|
|
73
|
-
A local source scanner finds fenced and indented code and nonempty blockquotes. Pi's Markdown tokenizer identifies standalone inline-code lines. These are the only selectable children. A standalone inline-code line contains one nonempty span and copies its content without backticks. Inline code within a sentence does not create a separate block. Lists, headings, tables, and prose remain in the whole-message preview and copy, but do not create children.
|
|
74
|
-
Search matches whole messages and tool names, so matching messages retain all their children.
|
|
75
|
-
Search filters as you type. Every effective query edit clears selection and preview. Clearing the query immediately restores all messages. Down selects the first message on the visible page and Up selects the last, without needing Enter first. Repeated Enter presses do nothing until you select a message.
|
|
76
|
-
While selection is inactive, the preview shows the dim, centered prompt `Use ↑/↓ to select a message`. Paging, tool visibility, preview scrolling, and resizing do not activate selection.
|
|
77
|
-
Block frames leave two columns on both sides. Parent frames use the full width. Block frames have no type labels. Code excerpts show the original fence lines, language info and quote/list prefixes when they fit. Unclosed fences get no invented closing marker. Indented code keeps its source indentation without added fences. Code previews show only the highlighted body, and code copy still excludes fences and container prefixes.
|
|
78
|
-
|
|
79
|
-
Source display lines and preview Markdown rendering are cached per message or block until width or theme changes. Many long messages can increase memory use until the picker closes.
|
|
80
|
-
|
|
81
|
-
Whole-message copy joins original text blocks within a record with newlines and preserves surrounding whitespace. For grouped assistant records it adds enough newline characters at each seam to make a paragraph separator of at least two line breaks, preserving existing line breaks. A single record stays unchanged.
|
|
82
|
-
Blockquote children copy plain text with one outer `>` prefix and at most one following space or tab removed per line. Quoted blank paragraphs stay in one target. Nested quote prefixes, internal whitespace, and line endings are preserved. Lazy continuation lines stay unchanged until an unquoted blank paragraph. Empty quotes have no child. Quote previews still render the original Markdown. Code children copy only code, without fences or Markdown quote/list prefixes. Copy removes trailing newline characters from code and quote blocks while preserving indentation, spaces, and internal line endings.
|
|
83
|
-
Quote fences stop when their quote container ends. List code indentation is measured four spaces beyond the list content indentation. A dedented nonblank line ends that list context.
|
|
84
|
-
The scanner covers space-indented lists and these common block forms rather than the full CommonMark grammar. Tab-expanded list indentation and lazy container continuation are not parsed as full CommonMark containers. All source remains available in the whole-message copy. Inline-code children require an unambiguous top-level paragraph token. Some valid spans in nested or otherwise ambiguous Markdown are skipped. Pi still renders and syntax-highlights the previews.
|
|
85
|
-
Images are not copied. Empty messages and image-only messages show `No copyable text`.
|
|
86
|
-
Tool calls copy formatted argument JSON, not the original request bytes.
|
|
87
|
-
Tool results copy text blocks without their `details` field.
|
|
88
|
-
Only displayed text has terminal control sequences removed. Do not paste untrusted copied text directly into a shell.
|
|
89
|
-
Generated UI text is English. Original messages keep their language.
|
|
90
|
-
|
|
91
|
-
Copy uses Pi's clipboard helper. Remote sessions may depend on OSC52 support in the terminal and multiplexer.
|
|
92
|
-
Pi 1.4.0 limits OSC52 to 100000 Base64 characters. Large copies can fail without a local clipboard backend.
|
|
93
|
-
Repeated Enter presses do not submit duplicate copies while a copy is pending. A failure leaves the picker open for retry.
|
|
49
|
+
The picker reads the current branch when it opens. Reopen it to see newer messages. It excludes abandoned branches, thinking, and images.
|
|
94
50
|
|
|
95
|
-
|
|
96
|
-
In regular Pi, success uses `ctx.ui.notify` with `Copied message to clipboard`, without a dismissal timer.
|
|
97
|
-
Success means the helper returned, not that the terminal confirmed clipboard contents.
|
|
51
|
+
Select a whole message, a code block, a standalone inline-code line, or a blockquote. The list shows Markdown source; the preview renders the selected content. Long selections are shortened in the list, but preview and copy include the complete selection.
|
|
98
52
|
|
|
99
|
-
|
|
53
|
+
Code copies without fences or Markdown container prefixes. Standalone inline code copies without backticks. Blockquotes copy without the outer quote marker. Images are not copied. Tool calls copy formatted arguments, and tool results copy text.
|
|
100
54
|
|
|
101
|
-
|
|
102
|
-
Copy the source to a sandbox-local directory. Repeat the copy when source or metadata changes.
|
|
103
|
-
Run the following from the checkout, using a fresh or dedicated `/tmp/pi-copy-picker-dev` directory:
|
|
55
|
+
Search matches messages and tool names and shows the number of matching items. Changing the query clears the selection; use Up or Down to select a result before copying.
|
|
104
56
|
|
|
105
|
-
|
|
106
|
-
mkdir -p /tmp/pi-copy-picker-dev
|
|
107
|
-
tar --exclude='./node_modules' --exclude='./.git' --exclude='*.tgz' -cf - . | tar -xf - -C /tmp/pi-copy-picker-dev
|
|
108
|
-
cd /tmp/pi-copy-picker-dev
|
|
109
|
-
npm ci
|
|
110
|
-
npm run check
|
|
111
|
-
npm pack
|
|
112
|
-
```
|
|
57
|
+
Copy uses Pi's clipboard helper. Remote sessions may require OSC 52 support in the terminal and multiplexer. Large copies may fail when no local clipboard is available. If copying fails, the picker stays open for retry.
|
|
113
58
|
|
|
114
|
-
|
|
115
|
-
`check` runs type checking, runtime tests, release guard tests, and a real tarball contract check.
|
|
116
|
-
`prepack` runs the same checks. The package checker uses `--ignore-scripts` to avoid recursive packing.
|
|
117
|
-
The package ships the three TypeScript source modules. Pi loads them with its TypeScript loader.
|
|
118
|
-
The package checker extracts the tarball and uses Pi's loader to verify command registration.
|
|
119
|
-
The entry is for Pi, not a promise that plain Node can execute the TypeScript source.
|
|
120
|
-
|
|
121
|
-
`src/history.ts` converts history. `src/picker.ts` owns selection, search, scrolling, and copy state.
|
|
122
|
-
`src/index.ts` registers the command. Tests use the real Input, Markdown renderer, and key parser with injected clipboard and dimensions.
|
|
123
|
-
IME positioning and actual clipboard reception still need checks in your target terminal.
|
|
59
|
+
Only displayed text has terminal control sequences removed. Do not paste untrusted copied text directly into a shell.
|
|
124
60
|
|
|
125
|
-
|
|
126
|
-
The [MIT license](LICENSE) and [changelog](CHANGELOG.md) ship with the package.
|
|
61
|
+
## Development
|
|
127
62
|
|
|
128
|
-
|
|
63
|
+
See [Development](https://github.com/tallshort/pi-copy-picker/blob/main/DEVELOPMENT.md) for local setup and implementation details. See [Release the package](RELEASING.md) for publication instructions.
|
package/RELEASING.md
CHANGED
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
2. Set `origin` fetch and push URLs to that repository. If the repository changes, update `repository.url`, `homepage`, and `bugs.url` in `package.json` before publishing.
|
|
7
7
|
3. Commit the package, lockfile, docs, and workflow on `main`. Push `main`. Keep the working tree clean. Do not commit `node_modules/`. The package publishes `src/` directly.
|
|
8
8
|
4. Confirm that your npm account can publish the top-level package `pi-copy-picker`. The package name is not scoped; `tallshort` is the author. Use Node 24 and npm 11.5.1 or newer. Trusted publishing requires at least Node 22.14.0 and npm 11.5.1.
|
|
9
|
-
5. Follow the
|
|
9
|
+
5. Follow the [development setup](https://github.com/tallshort/pi-copy-picker/blob/main/DEVELOPMENT.md) and checks. From the checked copy, use an interactive npm login and publish the first version manually:
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
12
|
npm login
|
|
13
13
|
npm publish --access public
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
The human runs these commands.
|
|
16
|
+
The human runs these commands.
|
|
17
17
|
A package must exist before its trusted publisher can be configured. Do not expect OIDC to bootstrap an unpublished package.
|
|
18
18
|
The first manual publication does not claim GitHub Actions provenance.
|
|
19
19
|
|
package/package.json
CHANGED
package/src/history.ts
CHANGED
|
@@ -85,51 +85,28 @@ export type PickTarget = Readonly<{
|
|
|
85
85
|
rawText: string;
|
|
86
86
|
} & ({ kind: "message" } | { kind: "block"; blockKind: "quote" | "code" | "inline-code"; language?: string; sourceText: string; sourceStart: number; sourceEnd: number; fence?: { opening: string; closing?: string } })>;
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
for (const segment of message.segments) {
|
|
96
|
-
const last = spans.at(-1);
|
|
97
|
-
if (last?.entryId === segment.entryId) last.sourceEnd = segment.sourceEnd;
|
|
98
|
-
else spans.push({ ...segment });
|
|
99
|
-
}
|
|
100
|
-
const blocks = spans.flatMap(segment => {
|
|
101
|
-
const start = segment.sourceStart + (message.rawText[segment.sourceStart - 1] === "\r" && message.rawText[segment.sourceStart] === "\n" ? 1 : 0);
|
|
102
|
-
const offset = (message.rawText.slice(0, start).match(/\r\n|\r|\n/g) ?? []).length;
|
|
103
|
-
return toPickTargets({ ...message, segments: undefined, rawText: message.rawText.slice(start, segment.sourceEnd) })
|
|
104
|
-
.filter((target): target is Extract<PickTarget, { kind: "block" }> => target.kind === "block")
|
|
105
|
-
.map(target => ({ ...target, message, sourceStart: target.sourceStart + offset, sourceEnd: target.sourceEnd + offset,
|
|
106
|
-
id: `${message.id}:block:${target.sourceStart + offset}`,
|
|
107
|
-
sourceText: sourceLines.slice(target.sourceStart + offset, target.sourceEnd + offset).join("") }));
|
|
108
|
-
});
|
|
109
|
-
return [parent, ...blocks];
|
|
88
|
+
function segmentBlocks(message: PickItem, segments: NonNullable<PickItem["segments"]>): PickTarget[] {
|
|
89
|
+
const sourceLines = message.rawText.match(/[^\r\n]*(?:\r\n|\r|\n)|[^\r\n]+$/g) ?? [];
|
|
90
|
+
const spans: { sourceStart: number; sourceEnd: number; entryId: string }[] = [];
|
|
91
|
+
for (const segment of segments) {
|
|
92
|
+
const last = spans.at(-1);
|
|
93
|
+
if (last?.entryId === segment.entryId) last.sourceEnd = segment.sourceEnd;
|
|
94
|
+
else spans.push({ ...segment });
|
|
110
95
|
}
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
96
|
+
const blocks = spans.flatMap(segment => {
|
|
97
|
+
const start = segment.sourceStart + (message.rawText[segment.sourceStart - 1] === "\r" && message.rawText[segment.sourceStart] === "\n" ? 1 : 0);
|
|
98
|
+
const offset = (message.rawText.slice(0, start).match(/\r\n|\r|\n/g) ?? []).length;
|
|
99
|
+
return toPickTargets({ ...message, segments: undefined, rawText: message.rawText.slice(start, segment.sourceEnd) })
|
|
100
|
+
.filter((target): target is Extract<PickTarget, { kind: "block" }> => target.kind === "block")
|
|
101
|
+
.map(target => ({ ...target, message, sourceStart: target.sourceStart + offset, sourceEnd: target.sourceEnd + offset,
|
|
102
|
+
id: `${message.id}:block:${target.sourceStart + offset}`,
|
|
103
|
+
sourceText: sourceLines.slice(target.sourceStart + offset, target.sourceEnd + offset).join("") }));
|
|
104
|
+
});
|
|
105
|
+
return blocks;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function analyzeMarkdownLines(rawText: string, lines: string[]) {
|
|
114
109
|
const content = (line: string) => line.replace(/(?:\r\n|\r|\n)$/, "");
|
|
115
|
-
const unquote = (line: string) => line.replace(/^(?: {0,3}>[ \t]?)+/, "");
|
|
116
|
-
const listIndents: number[] = [];
|
|
117
|
-
const listContext = (line: string) => {
|
|
118
|
-
const text = unquote(content(line));
|
|
119
|
-
if (!text.trim()) return;
|
|
120
|
-
const indent = /^ */.exec(text)![0].length;
|
|
121
|
-
while (listIndents.length && indent < listIndents.at(-1)!) listIndents.pop();
|
|
122
|
-
const marker = /^( *)(?:[-+*]|\d+[.)])([ \t]+)/.exec(text);
|
|
123
|
-
if (marker && indent <= (listIndents.at(-1) ?? 0) + 3) {
|
|
124
|
-
const contentIndent = marker[0].length;
|
|
125
|
-
if (listIndents.at(-1) !== contentIndent) listIndents.push(contentIndent);
|
|
126
|
-
}
|
|
127
|
-
};
|
|
128
|
-
const codeIndent = () => (listIndents.at(-1) ?? 0) + 4;
|
|
129
|
-
const fence = (line: string) => {
|
|
130
|
-
const match = /^( *)(?:([-+*]|\d+[.)])[ \t]+)?(`{3,}|~{3,})([^\r\n]*)$/.exec(unquote(content(line)));
|
|
131
|
-
return match && (match[1]!.length <= (listIndents.at(-1) ?? 0) + 3) ? match : null;
|
|
132
|
-
};
|
|
133
110
|
const inlineCode = (line: string): string | undefined => {
|
|
134
111
|
const text = content(line);
|
|
135
112
|
const opener = /^ {0,3}(`+)(?!`)/.exec(text);
|
|
@@ -143,7 +120,6 @@ export function toPickTargets(message: PickItem): readonly PickTarget[] {
|
|
|
143
120
|
}
|
|
144
121
|
return undefined;
|
|
145
122
|
};
|
|
146
|
-
const structure = (line: string) => /^(?: {0,3}(?:#{1,6}(?:\s|$)|>|[-+*]\s|\d+[.)]\s)| {4}|\t)|^ {0,3}(?:(?:\*\s*){3,}|(?:-\s*){3,}|(?:_\s*){3,})$/.test(content(line));
|
|
147
123
|
const inlineCandidates = new Map<number, { line: number; raw: string; payload: string }>();
|
|
148
124
|
let sourceOffset = 0;
|
|
149
125
|
for (let line = 0; line < lines.length; line++) {
|
|
@@ -156,7 +132,7 @@ export function toPickTargets(message: PickItem): readonly PickTarget[] {
|
|
|
156
132
|
sourceOffset += lines[line]!.replace(/\r\n|\r/g, "\n").length;
|
|
157
133
|
}
|
|
158
134
|
const inlineTargets = new Map<number, string>();
|
|
159
|
-
const normalized =
|
|
135
|
+
const normalized = rawText.replace(/\r\n|\r/g, "\n");
|
|
160
136
|
const protectedLines = new Set<number>();
|
|
161
137
|
{
|
|
162
138
|
let offset = 0;
|
|
@@ -184,6 +160,38 @@ export function toPickTargets(message: PickItem): readonly PickTarget[] {
|
|
|
184
160
|
offset += token.raw.length;
|
|
185
161
|
}
|
|
186
162
|
}
|
|
163
|
+
return { inlineTargets, protectedLines };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Source lines keep clipboard payloads independent of rendering.
|
|
167
|
+
export function toPickTargets(message: PickItem): readonly PickTarget[] {
|
|
168
|
+
const parent: PickTarget = { kind: "message", id: message.id, messageId: message.id, message, rawText: message.rawText };
|
|
169
|
+
if (message.kind !== "user" && message.kind !== "assistant") return [parent];
|
|
170
|
+
if (message.segments && message.segments.length > 1) return [parent, ...segmentBlocks(message, message.segments)];
|
|
171
|
+
const lines = message.rawText.match(/[^\r\n]*(?:\r\n|\r|\n)|[^\r\n]+$/g) ?? [];
|
|
172
|
+
const blocks: PickTarget[] = [];
|
|
173
|
+
let rich = false;
|
|
174
|
+
const content = (line: string) => line.replace(/(?:\r\n|\r|\n)$/, "");
|
|
175
|
+
const unquote = (line: string) => line.replace(/^(?: {0,3}>[ \t]?)+/, "");
|
|
176
|
+
const listIndents: number[] = [];
|
|
177
|
+
const listContext = (line: string) => {
|
|
178
|
+
const text = unquote(content(line));
|
|
179
|
+
if (!text.trim()) return;
|
|
180
|
+
const indent = /^ */.exec(text)![0].length;
|
|
181
|
+
while (listIndents.length && indent < listIndents.at(-1)!) listIndents.pop();
|
|
182
|
+
const marker = /^( *)(?:[-+*]|\d+[.)])([ \t]+)/.exec(text);
|
|
183
|
+
if (marker && indent <= (listIndents.at(-1) ?? 0) + 3) {
|
|
184
|
+
const contentIndent = marker[0].length;
|
|
185
|
+
if (listIndents.at(-1) !== contentIndent) listIndents.push(contentIndent);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
const codeIndent = () => (listIndents.at(-1) ?? 0) + 4;
|
|
189
|
+
const fence = (line: string) => {
|
|
190
|
+
const match = /^( *)(?:([-+*]|\d+[.)])[ \t]+)?(`{3,}|~{3,})([^\r\n]*)$/.exec(unquote(content(line)));
|
|
191
|
+
return match && (match[1]!.length <= (listIndents.at(-1) ?? 0) + 3) ? match : null;
|
|
192
|
+
};
|
|
193
|
+
const structure = (line: string) => /^(?: {0,3}(?:#{1,6}(?:\s|$)|>|[-+*]\s|\d+[.)]\s)| {4}|\t)|^ {0,3}(?:(?:\*\s*){3,}|(?:-\s*){3,}|(?:_\s*){3,})$/.test(content(line));
|
|
194
|
+
const { inlineTargets, protectedLines } = analyzeMarkdownLines(message.rawText, lines);
|
|
187
195
|
const inlineAt = (index: number) => inlineTargets.get(index);
|
|
188
196
|
const add = (start: number, end: number, blockKind: "quote" | "code" | "inline-code", rawText = lines.slice(start, end).join(""), language?: string, fence?: { opening: string; closing?: string }) => {
|
|
189
197
|
const sourceText = lines.slice(start, end).join("");
|
package/src/picker.ts
CHANGED
|
@@ -513,9 +513,12 @@ export class PickerScreen implements Component, Focusable {
|
|
|
513
513
|
: Math.min(pageCount - 1, this.pageIndex ?? Math.ceil(this.viewportStart / this.listRows()));
|
|
514
514
|
const help = "Enter copy · ←/→ prev/next page · Ctrl+T tools · PgUp/PgDn scroll preview · Esc close";
|
|
515
515
|
const status = this.status();
|
|
516
|
+
const matches = this.visibleItems().length;
|
|
517
|
+
const countText = this.queryInput.getValue() ? `${matches} ${matches === 1 ? "match" : "matches"}` : "";
|
|
518
|
+
const matchCount = countText && width >= visibleWidth("type to search > ") + visibleWidth(this.queryInput.getValue()) + visibleWidth(countText) + 2 ? " " + this.theme.fg("dim", countText) : "";
|
|
516
519
|
const prefix = `Page ${pageIndex + 1}/${pageCount} ${this.theme.fg("dim", `(tool messages ${this.showTools ? "included" : "excluded"})`)}, `;
|
|
517
|
-
const inputWidth = Math.min(width, visibleWidth("type to search > ") + visibleWidth(this.queryInput.getValue()) + 1);
|
|
518
|
-
const shownPrefix = truncateToWidth(prefix, Math.max(0, width - inputWidth), "");
|
|
520
|
+
const inputWidth = Math.min(width - visibleWidth(matchCount), visibleWidth("type to search > ") + visibleWidth(this.queryInput.getValue()) + 1);
|
|
521
|
+
const shownPrefix = truncateToWidth(prefix, Math.max(0, width - visibleWidth(matchCount) - inputWidth), "");
|
|
519
522
|
const arrow = (show: boolean, shaded: boolean) => {
|
|
520
523
|
if (!show) return "";
|
|
521
524
|
const text = " " + this.theme.fg("muted", "...");
|
|
@@ -530,7 +533,7 @@ export class PickerScreen implements Component, Focusable {
|
|
|
530
533
|
while (padding.has(visibleEnd - 1) && visibleEnd > visibleStart) visibleEnd--;
|
|
531
534
|
const lines = [this.theme.fg("dim", help),
|
|
532
535
|
status ? this.theme.fg(this.copyState.kind === "error" ? "error" : "muted", status)
|
|
533
|
-
: shownPrefix + this.queryInput.render(Math.max(1, width - visibleWidth(shownPrefix)))[0],
|
|
536
|
+
: shownPrefix + this.queryInput.render(Math.max(1, width - visibleWidth(shownPrefix) - visibleWidth(matchCount)))[0] + matchCount,
|
|
534
537
|
arrow(stream.content.slice(0, visibleStart).some(Boolean), !!stream.userBackground[visibleStart - 1] && !!stream.userBackground[visibleStart])];
|
|
535
538
|
const cardRows = stream.lines.slice(visibleStart, visibleEnd);
|
|
536
539
|
if (!stream.lines.length) cardRows.push(" No matching messages");
|