pi-session-peek 0.4.0 → 0.4.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 +5 -0
- package/README.md +4 -2
- package/README.zh-CN.md +4 -2
- package/package.json +1 -1
- package/src/i18n.ts +2 -2
- package/src/peek-component.ts +30 -8
- package/src/sessions.ts +35 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.1
|
|
4
|
+
|
|
5
|
+
- `Ctrl+T` shows tool calls in the preview: one dim line per call with the tool name and its main argument (command, path, URL, query…), placed under the reply that made it. Off by default. Tool results are still not stored, and the tool lines are never searched or highlighted.
|
|
6
|
+
- Assistant turns that only call tools (no text) are now kept when parsing, so with `Ctrl+T` on the preview no longer jumps from "let me check" straight to the conclusion. With it off they are skipped as before.
|
|
7
|
+
|
|
3
8
|
## 0.4.0
|
|
4
9
|
|
|
5
10
|
- Search syntax: `"exact phrase"` (whitespace inside the quotes matches any whitespace, including line breaks), `a|b` for either, `-word` to exclude, `name:` / `dir:` (`cwd:`) / `user:` / `ai:` (`assistant:`) to limit a term to one field, and `/regex/` for JavaScript regular expressions. Prefixes stack (`-user:"not now"`), quotes escape everything else, and an invalid regex falls back to plain text.
|
package/README.md
CHANGED
|
@@ -12,11 +12,12 @@ Search your [pi](https://pi.dev) session history by what was actually said, read
|
|
|
12
12
|
|
|
13
13
|
- Opens a two-pane picker with `/peek` or `/peek <keyword>`: sessions on the left, the full conversation on the right.
|
|
14
14
|
- Renders the conversation with pi's own Markdown renderer, so headings, code blocks with syntax highlighting, tables and lists look the same as in the main transcript.
|
|
15
|
-
- Filters as you type across conversation text, session name and working directory. Tool call arguments and results are excluded, so a keyword only matches sessions that actually discussed it.
|
|
15
|
+
- Filters as you type across conversation text, session name and working directory. Tool call arguments and results are excluded from the search, so a keyword only matches sessions that actually discussed it.
|
|
16
16
|
- Requires every space-separated keyword to match, and shows a snippet around the first hit in the list.
|
|
17
17
|
- Understands `"exact phrases"`, `either|or`, `-exclude`, `name:` / `dir:` / `user:` / `ai:` prefixes and `/regex/`.
|
|
18
18
|
- Limits results to recently active sessions with `@7d`, `@24h`, `@2w` or `@1m`.
|
|
19
19
|
- Highlights every hit in the preview and jumps between them with `Ctrl+N` / `Ctrl+P`.
|
|
20
|
+
- Shows what the assistant actually did with `Ctrl+T`: one dim line per tool call (`⚙ bash git status`, `⚙ edit src/query.ts`) under the reply that made it. Off by default.
|
|
20
21
|
- Resumes with `Enter`, forks into a new session with `Ctrl+O`, renames with `Ctrl+R`, deletes with `Ctrl+D`.
|
|
21
22
|
- Toggles between the current directory tree and all projects with `Tab`.
|
|
22
23
|
- Starts straight into the picker with `pi --rp` or `pi --peek=<keyword>`.
|
|
@@ -66,6 +67,7 @@ In TUI mode, run `/peek` and start typing. Press `Enter` on a session to continu
|
|
|
66
67
|
| `Ctrl+U` `Ctrl+F` | Scroll preview by half a page |
|
|
67
68
|
| `Shift+↑` `Shift+↓` | Scroll preview by three lines |
|
|
68
69
|
| `Ctrl+N` `Ctrl+P` | Next / previous hit line outside the current view; the top-right corner shows `hit k/n` |
|
|
70
|
+
| `Ctrl+T` | Show / hide tool calls in the preview. Each call is one line with the tool name and its main argument (command, path, URL, query). Results are never shown. |
|
|
69
71
|
| `Enter` | Resume the session |
|
|
70
72
|
| `Ctrl+O` | Fork the session and open the fork |
|
|
71
73
|
| `Ctrl+R` | Rename. Writes the same `session_info` entry as `/name`. |
|
|
@@ -90,7 +92,7 @@ The filter is case-insensitive and matches anywhere in the text. Whitespace sepa
|
|
|
90
92
|
|
|
91
93
|
Quotes are the escape hatch: `"-foo"`, `"a|b"`, `"name:x"` and `"/x/"` search for those characters literally. A regex that fails to compile is searched as plain text. Only positive terms are highlighted in the preview, and `name:` / `dir:` terms never touch the conversation, so `name:auth` alone shows the full conversation with no highlights.
|
|
92
94
|
|
|
93
|
-
The search index holds user and assistant messages, the session name and the working directory. It does not hold tool call arguments or tool results.
|
|
95
|
+
The search index holds user and assistant messages, the session name and the working directory. It does not hold tool call arguments or tool results, and the tool lines shown by `Ctrl+T` are never matched or highlighted.
|
|
94
96
|
|
|
95
97
|
## 🌐 Language
|
|
96
98
|
|
package/README.zh-CN.md
CHANGED
|
@@ -12,11 +12,12 @@
|
|
|
12
12
|
|
|
13
13
|
- `/peek` 或 `/peek 关键词` 打开左右分栏的选择器:左边是会话列表,右边是完整对话。
|
|
14
14
|
- 右栏用 pi 自己的 Markdown 渲染器画,标题、带语法高亮的代码块、表格、列表和主界面里的对话一个样子。
|
|
15
|
-
-
|
|
15
|
+
- 边打字边过滤,搜的是对话正文、会话名和工作目录。工具调用的参数和结果不进搜索索引,所以只会命中真正讨论过这个词的会话。
|
|
16
16
|
- 空格分隔多个关键词,全部命中才显示;列表里直接显示第一个命中处的片段。
|
|
17
17
|
- 支持 `"精确短语"`、`a|b` 任一命中、`-排除`、`name:` / `dir:` / `user:` / `ai:` 限定范围,以及 `/正则/`。
|
|
18
18
|
- `@7d`、`@24h`、`@2w`、`@1m` 只看最近活动过的会话。
|
|
19
19
|
- 预览里所有命中都高亮,`Ctrl+N` / `Ctrl+P` 在命中间跳。
|
|
20
|
+
- `Ctrl+T` 显示 AI 实际做了什么:每次工具调用在对应回复下面占一行灰字(`⚙ bash git status`、`⚙ edit src/query.ts`)。默认关闭。
|
|
20
21
|
- `Enter` 进入会话,`Ctrl+O` 分叉成新会话,`Ctrl+R` 重命名,`Ctrl+D` 删除。
|
|
21
22
|
- `Tab` 在当前目录树和全部项目之间切换。
|
|
22
23
|
- `pi --rp` 或 `pi --peek=关键词` 启动时直接打开。
|
|
@@ -66,6 +67,7 @@ pi 扩展以你的用户权限运行,只装信得过的包。
|
|
|
66
67
|
| `Ctrl+U` `Ctrl+F` | 预览翻半页 |
|
|
67
68
|
| `Shift+↑` `Shift+↓` | 预览滚三行 |
|
|
68
69
|
| `Ctrl+N` `Ctrl+P` | 跳到当前屏幕外的下一处 / 上一处命中行,右上角显示「命中 k/n」 |
|
|
70
|
+
| `Ctrl+T` | 预览里显示 / 隐藏工具调用。每次调用一行:工具名和主参数(命令、路径、URL、查询词)。不显示结果。 |
|
|
69
71
|
| `Enter` | 进入会话 |
|
|
70
72
|
| `Ctrl+O` | 分叉出新会话并进入 |
|
|
71
73
|
| `Ctrl+R` | 重命名,写入的记录和 `/name` 一样 |
|
|
@@ -90,7 +92,7 @@ pi 扩展以你的用户权限运行,只装信得过的包。
|
|
|
90
92
|
|
|
91
93
|
引号是万能转义:`"-foo"`、`"a|b"`、`"name:x"`、`"/x/"` 都按字面搜这些字符。正则写错了就当普通文字搜。预览里只高亮正向的词,`name:` / `dir:` 的词不碰对话正文,所以只输入 `name:auth` 时右栏显示完整对话、没有高亮。
|
|
92
94
|
|
|
93
|
-
|
|
95
|
+
索引里有用户和助手的消息、会话名、工作目录。没有工具调用的参数和结果,`Ctrl+T` 显示的工具行也不参与匹配和高亮。
|
|
94
96
|
|
|
95
97
|
## 🌐 界面语言
|
|
96
98
|
|
package/package.json
CHANGED
package/src/i18n.ts
CHANGED
|
@@ -32,7 +32,7 @@ const zh = {
|
|
|
32
32
|
ai: "🤖 AI",
|
|
33
33
|
renamePrefix: "✏ 重命名: ",
|
|
34
34
|
confirmDelete: "⚠ 删除会话 [{desc}]?按 y / Enter 确认,任意其他键取消",
|
|
35
|
-
footer: "↑↓ 选择 PgUp/Dn·^U/^F·⇧↑↓ 滚动 ^N/^P 命中 Tab 范围 ^D 删除 ^R 改名 ^O 分叉 Enter 进入 Esc/^C 关闭",
|
|
35
|
+
footer: "↑↓ 选择 PgUp/Dn·^U/^F·⇧↑↓ 滚动 ^N/^P 命中 ^T 工具 Tab 范围 ^D 删除 ^R 改名 ^O 分叉 Enter 进入 Esc/^C 关闭",
|
|
36
36
|
since: "近{n}{unit}",
|
|
37
37
|
unitH: "小时",
|
|
38
38
|
unitD: "天",
|
|
@@ -72,7 +72,7 @@ const en: Record<keyof typeof zh, string> = {
|
|
|
72
72
|
ai: "🤖 AI",
|
|
73
73
|
renamePrefix: "✏ Rename: ",
|
|
74
74
|
confirmDelete: "⚠ Delete session [{desc}]? y / Enter to confirm, any other key to cancel",
|
|
75
|
-
footer: "↑↓ select PgUp/Dn·^U/^F·⇧↑↓ scroll ^N/^P hits Tab scope ^D del ^R rename ^O fork Enter open Esc close",
|
|
75
|
+
footer: "↑↓ select PgUp/Dn·^U/^F·⇧↑↓ scroll ^N/^P hits ^T tools Tab scope ^D del ^R rename ^O fork Enter open Esc close",
|
|
76
76
|
since: "last {n}{unit}",
|
|
77
77
|
unitH: "h",
|
|
78
78
|
unitD: "d",
|
package/src/peek-component.ts
CHANGED
|
@@ -23,7 +23,8 @@ export class PeekComponent implements Component, Focusable {
|
|
|
23
23
|
private selected = 0;
|
|
24
24
|
private listOffset = 0;
|
|
25
25
|
private previewOffset = 0;
|
|
26
|
-
private previewKey = ""; // 预览缓存的键(会话 + 关键词 +
|
|
26
|
+
private previewKey = ""; // 预览缓存的键(会话 + 关键词 + 宽度 + 工具开关),置空强制重建
|
|
27
|
+
private showTools = false; // Ctrl+T:预览里显示每次工具调用的一行摘要
|
|
27
28
|
private previewLines: string[] = [];
|
|
28
29
|
private matchLines: number[] = []; // 预览里含关键词的行(升序),Ctrl+N / Ctrl+P 用
|
|
29
30
|
private rendered = new WeakMap<PeekMsg, { w: number; lines: string[] }>(); // 每条消息渲染好的行,按宽度缓存
|
|
@@ -224,6 +225,12 @@ export class PeekComponent implements Component, Focusable {
|
|
|
224
225
|
this.invalidate();
|
|
225
226
|
return;
|
|
226
227
|
}
|
|
228
|
+
if (matchesKey(data, Key.ctrl("t"))) {
|
|
229
|
+
this.showTools = !this.showTools;
|
|
230
|
+
this.previewKey = "";
|
|
231
|
+
this.invalidate();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
227
234
|
if (matchesKey(data, Key.ctrl("n"))) {
|
|
228
235
|
this.jumpMatch(1);
|
|
229
236
|
return;
|
|
@@ -339,7 +346,7 @@ export class PeekComponent implements Component, Focusable {
|
|
|
339
346
|
private buildPreview(rw: number): void {
|
|
340
347
|
const t = this.theme;
|
|
341
348
|
const s = this.filtered[this.selected];
|
|
342
|
-
const key = s ? `${s.path}|${this.getQuery()}|${rw}` : "none";
|
|
349
|
+
const key = s ? `${s.path}|${this.getQuery()}|${rw}|${this.showTools ? "t" : ""}` : "none";
|
|
343
350
|
if (key === this.previewKey) return;
|
|
344
351
|
this.previewKey = key;
|
|
345
352
|
this.matchLines = [];
|
|
@@ -351,9 +358,11 @@ export class PeekComponent implements Component, Focusable {
|
|
|
351
358
|
}
|
|
352
359
|
|
|
353
360
|
const lines: string[] = [];
|
|
361
|
+
// 消息数只算有文字的,纯工具轮次不计
|
|
362
|
+
const textCount = s.msgs.reduce((n, m) => n + (m.text ? 1 : 0), 0);
|
|
354
363
|
lines.push(
|
|
355
364
|
truncateToWidth(
|
|
356
|
-
t.fg("dim", `${s.cwd} • ${msg("msgCount", { n:
|
|
365
|
+
t.fg("dim", `${s.cwd} • ${msg("msgCount", { n: textCount })}${s.name ? " • " + s.name : ""}`),
|
|
357
366
|
rw,
|
|
358
367
|
),
|
|
359
368
|
);
|
|
@@ -385,26 +394,39 @@ export class PeekComponent implements Component, Focusable {
|
|
|
385
394
|
return truncateToWidth(label + " " + "─".repeat(Math.max(2, rw - w - 1)), rw);
|
|
386
395
|
};
|
|
387
396
|
|
|
397
|
+
let prevRole: PeekMsg["role"] | undefined; // 上一条画出来的消息的角色
|
|
388
398
|
for (const m of msgs) {
|
|
399
|
+
const tools = this.showTools ? (m.tools ?? []) : [];
|
|
400
|
+
if (!m.text && !tools.length) continue; // 工具隐藏时,只有工具调用的 AI 轮次整条跳过
|
|
389
401
|
const terms = rt[m.role]; // user: 的词只在用户消息里高亮,ai: 的只在 AI 回复里
|
|
390
402
|
const isMatch = terms.length > 0 && anyMatch(m.text, terms);
|
|
391
|
-
|
|
392
|
-
|
|
403
|
+
// 紧跟在 AI 文字后面、只有工具调用的轮次不重复画标签,接在上一条正文下面,看起来是同一轮
|
|
404
|
+
const continues = m.role === "assistant" && prevRole === "assistant" && !m.text;
|
|
405
|
+
if (continues) {
|
|
406
|
+
lines.pop();
|
|
407
|
+
} else if (m.role === "user") {
|
|
393
408
|
lines.push(
|
|
394
409
|
t.bg("userMessageBg", padEndVisible(t.bold(t.fg("accent", fillLabel(msg("you")))), rw)),
|
|
395
410
|
);
|
|
396
411
|
} else {
|
|
397
412
|
lines.push(t.fg("muted", fillLabel(msg("ai"))));
|
|
398
413
|
}
|
|
414
|
+
const head = lines.length - 1;
|
|
399
415
|
// 先渲染再高亮:往 markdown 源码里插转义码会把链接、代码块的语法弄坏
|
|
400
416
|
const before = this.matchLines.length;
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
417
|
+
if (m.text) {
|
|
418
|
+
for (const l of this.renderMsg(m, rw)) {
|
|
419
|
+
if (isMatch && lineHasMatch(l, terms)) this.matchLines.push(lines.length);
|
|
420
|
+
lines.push(isMatch ? highlight(l, terms, t) : l);
|
|
421
|
+
}
|
|
404
422
|
}
|
|
405
423
|
// 关键词被折行拆开时哪一行都找不到,退回到消息标题行,别把这条消息漏掉
|
|
406
424
|
if (isMatch && this.matchLines.length === before) this.matchLines.push(head);
|
|
425
|
+
for (const tool of tools) {
|
|
426
|
+
lines.push(t.fg("dim", truncateToWidth(` ⚙ ${tool.name}${tool.summary ? " " + tool.summary : ""}`, rw)));
|
|
427
|
+
}
|
|
407
428
|
lines.push("");
|
|
429
|
+
prevRole = m.role;
|
|
408
430
|
}
|
|
409
431
|
|
|
410
432
|
this.previewLines = lines;
|
package/src/sessions.ts
CHANGED
|
@@ -7,9 +7,15 @@ import { join } from "node:path";
|
|
|
7
7
|
// 读 ~/.pi/agent/sessions 下的会话 JSONL,按 mtime 缓存;重命名和删除也在这里。
|
|
8
8
|
// 搜索不预存小写全文:会话正文只在 msgs 里放一份,匹配时用不区分大小写的正则直接扫(见 query.ts 的 matchesSession)
|
|
9
9
|
|
|
10
|
+
export interface PeekTool {
|
|
11
|
+
name: string;
|
|
12
|
+
summary: string; // 主参数(命令 / 路径 / URL…)压成一行,预览里显示用
|
|
13
|
+
}
|
|
14
|
+
|
|
10
15
|
export interface PeekMsg {
|
|
11
16
|
role: "user" | "assistant";
|
|
12
|
-
text: string;
|
|
17
|
+
text: string; // 只发了工具调用、没写字的 AI 轮次为空串
|
|
18
|
+
tools?: PeekTool[]; // 这条消息里的工具调用,没有就不带这个键。不进搜索索引
|
|
13
19
|
}
|
|
14
20
|
|
|
15
21
|
export interface PeekSession {
|
|
@@ -55,6 +61,26 @@ function extractText(content: unknown): string {
|
|
|
55
61
|
.join("\n");
|
|
56
62
|
}
|
|
57
63
|
|
|
64
|
+
// 工具调用的主参数:按常见键名找第一个字符串,都没有就取第一个字符串参数。不存结果,bash 输出动辄几十 KB
|
|
65
|
+
const TOOL_ARG_KEYS = ["command", "path", "url", "query", "pattern", "file_path"];
|
|
66
|
+
const TOOL_SUMMARY_MAX = 120;
|
|
67
|
+
|
|
68
|
+
export function toolSummary(args: unknown): string {
|
|
69
|
+
if (!args || typeof args !== "object") return "";
|
|
70
|
+
const o = args as Record<string, unknown>;
|
|
71
|
+
let v = TOOL_ARG_KEYS.map((k) => o[k]).find((x) => typeof x === "string" && x.trim());
|
|
72
|
+
if (typeof v !== "string") v = Object.values(o).find((x) => typeof x === "string" && (x as string).trim());
|
|
73
|
+
if (typeof v !== "string") return "";
|
|
74
|
+
return v.replace(/\s+/g, " ").trim().slice(0, TOOL_SUMMARY_MAX);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function extractTools(content: unknown): PeekTool[] {
|
|
78
|
+
if (!Array.isArray(content)) return [];
|
|
79
|
+
return content
|
|
80
|
+
.filter((c: any) => c?.type === "toolCall" && typeof c.name === "string")
|
|
81
|
+
.map((c: any) => ({ name: c.name, summary: toolSummary(c.arguments) }));
|
|
82
|
+
}
|
|
83
|
+
|
|
58
84
|
function parseSession(path: string, raw: string, mtime: number): PeekSession | null {
|
|
59
85
|
let cwd = "";
|
|
60
86
|
let time = "";
|
|
@@ -82,12 +108,17 @@ function parseSession(path: string, raw: string, mtime: number): PeekSession | n
|
|
|
82
108
|
const role = o.message?.role;
|
|
83
109
|
if (role !== "user" && role !== "assistant") continue;
|
|
84
110
|
const text = extractText(o.message?.content).trim();
|
|
85
|
-
|
|
111
|
+
const tools = role === "assistant" ? extractTools(o.message.content) : [];
|
|
112
|
+
// 只有工具调用的 AI 轮次也留着,否则预览里"我来看看"直接跳到结论,中间做了什么全没了
|
|
113
|
+
if (tools.length) msgs.push({ role, text, tools });
|
|
114
|
+
else if (text) msgs.push({ role, text });
|
|
86
115
|
} catch {
|
|
87
116
|
}
|
|
88
117
|
}
|
|
89
118
|
|
|
90
|
-
|
|
119
|
+
// 一条文字消息都没有的会话不列出来;列表里的首条也只看有文字的
|
|
120
|
+
const firstText = msgs.find((m) => m.text);
|
|
121
|
+
if (!firstText) return null;
|
|
91
122
|
|
|
92
123
|
return {
|
|
93
124
|
path,
|
|
@@ -96,7 +127,7 @@ function parseSession(path: string, raw: string, mtime: number): PeekSession | n
|
|
|
96
127
|
name,
|
|
97
128
|
msgs,
|
|
98
129
|
mtime,
|
|
99
|
-
first:
|
|
130
|
+
first: firstText.text.replace(/\s+/g, " ").slice(0, 80),
|
|
100
131
|
};
|
|
101
132
|
}
|
|
102
133
|
|