pi-session-peek 0.2.0 → 0.2.1

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,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.1
4
+
5
+ - Ctrl+N / Ctrl+P now jump between hit lines instead of hit messages. Several hits inside one long reply used to collapse into a single stop, so the preview appeared not to move. Hits already on screen are skipped, and the top-right corner shows the current hit as `hit k/n`.
6
+
3
7
  ## 0.2.0
4
8
 
5
9
  - The preview renders messages with pi's own Markdown renderer: headings, bold, inline code, syntax-highlighted code blocks, tables with borders and cell wrapping, lists, quotes and links now look the same as in the main transcript.
package/README.md CHANGED
@@ -64,7 +64,7 @@ In TUI mode, run `/peek` and start typing. Press `Enter` on a session to continu
64
64
  | `PgUp` `PgDn` | Scroll preview by a page |
65
65
  | `Ctrl+U` `Ctrl+F` | Scroll preview by half a page |
66
66
  | `Shift+↑` `Shift+↓` | Scroll preview by three lines |
67
- | `Ctrl+N` `Ctrl+P` | Next / previous hit |
67
+ | `Ctrl+N` `Ctrl+P` | Next / previous hit line outside the current view; the top-right corner shows `hit k/n` |
68
68
  | `Enter` | Resume the session |
69
69
  | `Ctrl+O` | Fork the session and open the fork |
70
70
  | `Ctrl+R` | Rename. Writes the same `session_info` entry as `/name`. |
package/README.zh-CN.md CHANGED
@@ -64,7 +64,7 @@ pi 扩展以你的用户权限运行,只装信得过的包。
64
64
  | `PgUp` `PgDn` | 预览翻页 |
65
65
  | `Ctrl+U` `Ctrl+F` | 预览翻半页 |
66
66
  | `Shift+↑` `Shift+↓` | 预览滚三行 |
67
- | `Ctrl+N` `Ctrl+P` | 下一个 / 上一个命中 |
67
+ | `Ctrl+N` `Ctrl+P` | 跳到当前屏幕外的下一处 / 上一处命中行,右上角显示「命中 k/n」 |
68
68
  | `Enter` | 进入会话 |
69
69
  | `Ctrl+O` | 分叉出新会话并进入 |
70
70
  | `Ctrl+R` | 重命名,写入的记录和 `/name` 一样 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-session-peek",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Pi extension: search session history in a two-pane picker with full conversation preview, then resume or fork.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/i18n.ts CHANGED
@@ -23,6 +23,7 @@ const zh = {
23
23
  matchCount: " 匹配 {n}/{total}",
24
24
  noMatch: "没有匹配的会话",
25
25
  msgCount: "{n} 条消息",
26
+ hitPos: " 命中 {k}/{n}",
26
27
  truncated: "(会话过长,仅显示最后 {n} 条)",
27
28
  hitInTruncated: "关键词命中在未显示的更早消息中",
28
29
  hitOnlyMeta: "关键词仅命中会话名或目录,对话正文无匹配",
@@ -61,6 +62,7 @@ const en: Record<keyof typeof zh, string> = {
61
62
  matchCount: " {n}/{total} matched",
62
63
  noMatch: "No matching sessions",
63
64
  msgCount: "{n} messages",
65
+ hitPos: " hit {k}/{n}",
64
66
  truncated: "(long session, showing the last {n} messages)",
65
67
  hitInTruncated: "Keyword hits are in earlier messages not shown here",
66
68
  hitOnlyMeta: "Keyword matched only the session name or directory, not the conversation",
@@ -10,7 +10,7 @@ import {
10
10
  type MarkdownTheme,
11
11
  } from "@earendil-works/pi-tui";
12
12
  import { msg } from "./i18n.ts";
13
- import { anyKw, highlight, parseQuery, snippet, type ParsedQuery } from "./query.ts";
13
+ import { anyKw, highlight, lineHasKw, parseQuery, snippet, type ParsedQuery } from "./query.ts";
14
14
  import type { PeekMsg, PeekSession } from "./sessions.ts";
15
15
  import { fmtTime, normPath, padEndVisible } from "./text.ts";
16
16
 
@@ -25,7 +25,7 @@ export class PeekComponent implements Component, Focusable {
25
25
  private previewOffset = 0;
26
26
  private previewKey = ""; // 预览缓存的键(会话 + 关键词 + 宽度),置空强制重建
27
27
  private previewLines: string[] = [];
28
- private matchLines: number[] = []; // 预览里命中消息所在的行,Ctrl+N / Ctrl+P 用
28
+ private matchLines: number[] = []; // 预览里含关键词的行(升序),Ctrl+N / Ctrl+P 用
29
29
  private rendered = new WeakMap<PeekMsg, { w: number; lines: string[] }>(); // 每条消息渲染好的行,按宽度缓存
30
30
  private confirmingDelete = false; // Ctrl+D 之后等待确认
31
31
  private renaming = false; // Ctrl+R 之后正在输入名字
@@ -250,15 +250,18 @@ export class PeekComponent implements Component, Focusable {
250
250
  this.invalidate();
251
251
  }
252
252
 
253
- // 预览里跳到下一个 / 上一个命中,到头了回绕
253
+ // 预览里跳到当前视口之外的下一个 / 上一个命中行,到头了回绕。同一屏里的多个命中算一处,
254
+ // 否则一段里连着几行都命中要按好几次才过得去
254
255
  private jumpMatch(dir: 1 | -1): void {
255
256
  if (!this.matchLines.length) return;
256
- const cur = this.previewOffset + 2;
257
+ const H = this.bodyHeight();
258
+ const top = this.previewOffset;
259
+ const bottom = Math.min(top + H, this.previewLines.length) - 1;
257
260
  let target: number | undefined;
258
261
  if (dir === 1) {
259
- target = this.matchLines.find((l) => l > cur) ?? this.matchLines[0];
262
+ target = this.matchLines.find((l) => l > bottom) ?? this.matchLines[0];
260
263
  } else {
261
- target = [...this.matchLines].reverse().find((l) => l < cur) ?? this.matchLines[this.matchLines.length - 1];
264
+ target = [...this.matchLines].reverse().find((l) => l < top) ?? this.matchLines[this.matchLines.length - 1];
262
265
  }
263
266
  if (target !== undefined) {
264
267
  this.previewOffset = Math.max(0, target - 2);
@@ -383,7 +386,7 @@ export class PeekComponent implements Component, Focusable {
383
386
 
384
387
  for (const m of msgs) {
385
388
  const isMatch = kws.length > 0 && anyKw(m.text.toLowerCase(), kws);
386
- if (isMatch) this.matchLines.push(lines.length);
389
+ const head = lines.length;
387
390
  if (m.role === "user") {
388
391
  lines.push(
389
392
  t.bg("userMessageBg", padEndVisible(t.bold(t.fg("accent", fillLabel(msg("you")))), rw)),
@@ -392,7 +395,13 @@ export class PeekComponent implements Component, Focusable {
392
395
  lines.push(t.fg("muted", fillLabel(msg("ai"))));
393
396
  }
394
397
  // 先渲染再高亮:往 markdown 源码里插转义码会把链接、代码块的语法弄坏
395
- for (const l of this.renderMsg(m, rw)) lines.push(isMatch ? highlight(l, kws, t) : l);
398
+ const before = this.matchLines.length;
399
+ for (const l of this.renderMsg(m, rw)) {
400
+ if (isMatch && lineHasKw(l, kws)) this.matchLines.push(lines.length);
401
+ lines.push(isMatch ? highlight(l, kws, t) : l);
402
+ }
403
+ // 关键词被折行拆开时哪一行都找不到,退回到消息标题行,别把这条消息漏掉
404
+ if (isMatch && this.matchLines.length === before) this.matchLines.push(head);
396
405
  lines.push("");
397
406
  }
398
407
 
@@ -435,13 +444,15 @@ export class PeekComponent implements Component, Focusable {
435
444
  const maxOff = Math.max(0, this.previewLines.length - H);
436
445
  if (this.previewOffset > maxOff) this.previewOffset = maxOff;
437
446
  const rightRows = this.previewLines.slice(this.previewOffset, this.previewOffset + H);
438
- const scrollInfo =
439
- this.previewLines.length > H
440
- ? t.fg(
441
- "dim",
442
- ` (${this.previewOffset + 1}-${Math.min(this.previewOffset + H, this.previewLines.length)}/${this.previewLines.length})`,
443
- )
444
- : "";
447
+ // 右上角:有命中时显示"第几处/共几处"(视口内第一个命中的序号),行数多时再加行号范围
448
+ const end = Math.min(this.previewOffset + H, this.previewLines.length);
449
+ let info = "";
450
+ if (this.matchLines.length) {
451
+ const i = this.matchLines.findIndex((l) => l >= this.previewOffset && l < end);
452
+ info += msg("hitPos", { k: i >= 0 ? String(i + 1) : "-", n: this.matchLines.length });
453
+ }
454
+ if (this.previewLines.length > H) info += ` (${this.previewOffset + 1}-${end}/${this.previewLines.length})`;
455
+ const scrollInfo = info ? t.fg("dim", info) : "";
445
456
 
446
457
  for (let i = 0; i < H; i++) {
447
458
  const l = leftRows[i] ?? " ".repeat(lw);
package/src/query.ts CHANGED
@@ -96,6 +96,11 @@ function applySgr(st: Sgr, seq: string): void {
96
96
  }
97
97
  }
98
98
 
99
+ // 渲染后的一行(可带 ANSI)里有没有关键词,预览按行记命中用
100
+ export function lineHasKw(line: string, kws: string[]): boolean {
101
+ return kws.length > 0 && anyKw(line.replace(ESC_RE, "").toLowerCase(), kws);
102
+ }
103
+
99
104
  // 在一行文字里高亮关键词,返回带 ANSI 的字符串。
100
105
  // 输入可以是已经渲染过、带样式的行:转义序列不参与匹配也不会被切断;高亮结束时把原来的
101
106
  // 前景色 / 粗体 / 下划线恢复回去,所以放在标题、链接等样式里面也不会把后面的文字弄丢样式。