pi-session-peek 0.1.0 → 0.2.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 +6 -0
- package/README.md +15 -1
- package/README.zh-CN.md +15 -0
- package/docs/screenshot.png +0 -0
- package/index.ts +13 -12
- package/package.json +4 -3
- package/src/i18n.ts +124 -0
- package/src/peek-component.ts +489 -464
- package/src/query.ts +116 -18
- package/src/text.ts +1 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
- 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.
|
|
6
|
+
- Keyword highlighting is applied on top of the rendered lines, so it no longer breaks links or code blocks. A keyword that is split across a wrapped line is not highlighted at that spot; hit counting and Ctrl+N / Ctrl+P are unaffected.
|
|
7
|
+
|
|
3
8
|
## 0.1.0
|
|
4
9
|
|
|
5
10
|
First release.
|
|
@@ -10,3 +15,4 @@ First release.
|
|
|
10
15
|
- Enter resumes, Ctrl+O forks, Ctrl+R renames, Ctrl+D deletes (moves to trash when the `trash` CLI is available).
|
|
11
16
|
- Tab toggles between the current directory tree and all projects.
|
|
12
17
|
- `/peek [keyword]` command, `--rp` and `--peek=<keyword>` startup flags.
|
|
18
|
+
- Chinese and English interface, following the system locale. Override with `PI_SESSION_PEEK_LANG=zh|en`.
|
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ Search your [pi](https://pi.dev) session history by what was actually said, read
|
|
|
11
11
|
## ✨ Features
|
|
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
|
+
- 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.
|
|
14
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
16
|
- Requires every space-separated keyword to match, and shows a snippet around the first hit in the list.
|
|
16
17
|
- Limits results to recently active sessions with `@7d`, `@24h`, `@2w` or `@1m`.
|
|
@@ -82,10 +83,22 @@ The filter is case-insensitive and matches anywhere in the text.
|
|
|
82
83
|
|
|
83
84
|
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.
|
|
84
85
|
|
|
86
|
+
## 🌐 Language
|
|
87
|
+
|
|
88
|
+
The interface follows your system language: Chinese on `zh-*` locales, English everywhere else. Detection order is `PI_SESSION_PEEK_LANG`, then `LC_ALL` / `LC_MESSAGES` / `LANG`, then the OS locale.
|
|
89
|
+
|
|
90
|
+
Force a language:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
PI_SESSION_PEEK_LANG=en pi
|
|
94
|
+
PI_SESSION_PEEK_LANG=zh pi
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
On Windows PowerShell: `$env:PI_SESSION_PEEK_LANG = "en"` before starting pi. The language is read once when the extension loads, so change it before starting pi or run `/reload`.
|
|
98
|
+
|
|
85
99
|
## 🚧 Limitations
|
|
86
100
|
|
|
87
101
|
- TUI mode only.
|
|
88
|
-
- The interface text is Chinese.
|
|
89
102
|
- No mouse support. Pi only enables mouse input in its experimental fullscreen mode, and this picker does not handle it yet.
|
|
90
103
|
- The preview shows the last 500 messages of very long sessions. Hits in earlier messages are still counted and announced.
|
|
91
104
|
- `pi --peek` without a value is rejected by pi at startup. Use `pi --rp` to open without a keyword.
|
|
@@ -100,6 +113,7 @@ pi-session-peek/
|
|
|
100
113
|
│ ├── peek-component.ts # The two-pane TUI component
|
|
101
114
|
│ ├── sessions.ts # Scans and parses session JSONL, rename / delete helpers
|
|
102
115
|
│ ├── query.ts # Query parsing, highlighting, hit snippets
|
|
116
|
+
│ ├── i18n.ts # Chinese / English UI strings and locale detection
|
|
103
117
|
│ └── text.ts # Path, time and width helpers
|
|
104
118
|
├── scripts/screenshot.mjs # Optional: renders a synthetic screenshot from component output
|
|
105
119
|
└── test/ # node:test suites
|
package/README.zh-CN.md
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
## ✨ 功能
|
|
12
12
|
|
|
13
13
|
- `/peek` 或 `/peek 关键词` 打开左右分栏的选择器:左边是会话列表,右边是完整对话。
|
|
14
|
+
- 右栏用 pi 自己的 Markdown 渲染器画,标题、带语法高亮的代码块、表格、列表和主界面里的对话一个样子。
|
|
14
15
|
- 边打字边过滤,搜的是对话正文、会话名和工作目录。工具调用的参数和结果不进索引,所以只会命中真正讨论过这个词的会话。
|
|
15
16
|
- 空格分隔多个关键词,全部命中才显示;列表里直接显示第一个命中处的片段。
|
|
16
17
|
- `@7d`、`@24h`、`@2w`、`@1m` 只看最近活动过的会话。
|
|
@@ -82,6 +83,19 @@ pi 扩展以你的用户权限运行,只装信得过的包。
|
|
|
82
83
|
|
|
83
84
|
索引里有用户和助手的消息、会话名、工作目录。没有工具调用的参数和结果。
|
|
84
85
|
|
|
86
|
+
## 🌐 界面语言
|
|
87
|
+
|
|
88
|
+
跟随系统语言:区域是 `zh-*` 显示中文,其他显示英文。判断顺序是 `PI_SESSION_PEEK_LANG`,然后 `LC_ALL` / `LC_MESSAGES` / `LANG`,最后是操作系统的区域设置。
|
|
89
|
+
|
|
90
|
+
强制指定:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
PI_SESSION_PEEK_LANG=en pi
|
|
94
|
+
PI_SESSION_PEEK_LANG=zh pi
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Windows PowerShell 里先执行 `$env:PI_SESSION_PEEK_LANG = "en"` 再启动 pi。语言在扩展加载时读一次,改了之后要重启 pi 或 `/reload`。
|
|
98
|
+
|
|
85
99
|
## 🚧 限制
|
|
86
100
|
|
|
87
101
|
- 只支持 TUI 模式。
|
|
@@ -99,6 +113,7 @@ pi-session-peek/
|
|
|
99
113
|
│ ├── peek-component.ts # 双栏 TUI 组件
|
|
100
114
|
│ ├── sessions.ts # 扫描解析会话 JSONL,重命名 / 删除
|
|
101
115
|
│ ├── query.ts # 查询解析、高亮、命中片段
|
|
116
|
+
│ ├── i18n.ts # 中英文界面文案和语言检测
|
|
102
117
|
│ └── text.ts # 路径、时间、宽度工具
|
|
103
118
|
├── scripts/screenshot.mjs # 可选:用组件渲染输出生成一张合成截图
|
|
104
119
|
└── test/ # node:test 测试
|
package/docs/screenshot.png
CHANGED
|
Binary file
|
package/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { SessionManager, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import { getMarkdownTheme, SessionManager, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { basename } from "node:path";
|
|
3
|
+
import { msg } from "./src/i18n.ts";
|
|
3
4
|
import { PeekComponent } from "./src/peek-component.ts";
|
|
4
5
|
import { deleteSession, renameSession, scanSessions, type PeekSession } from "./src/sessions.ts";
|
|
5
6
|
|
|
@@ -10,11 +11,11 @@ let lastQuery = ""; // 进程内记住上次搜索词
|
|
|
10
11
|
export default function (pi: ExtensionAPI) {
|
|
11
12
|
// pi 会把 boolean flag 的值一律强转成 true,所以带关键词的形式只能是 string 类型
|
|
12
13
|
pi.registerFlag("peek", {
|
|
13
|
-
description: "
|
|
14
|
+
description: msg("flagPeek"),
|
|
14
15
|
type: "string",
|
|
15
16
|
});
|
|
16
17
|
pi.registerFlag("rp", {
|
|
17
|
-
description: "
|
|
18
|
+
description: msg("flagRp"),
|
|
18
19
|
type: "boolean",
|
|
19
20
|
});
|
|
20
21
|
|
|
@@ -44,10 +45,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
44
45
|
});
|
|
45
46
|
|
|
46
47
|
pi.registerCommand("peek", {
|
|
47
|
-
description: "
|
|
48
|
+
description: msg("cmdDesc"),
|
|
48
49
|
handler: async (args, ctx) => {
|
|
49
50
|
if (ctx.mode !== "tui") {
|
|
50
|
-
ctx.ui.notify("
|
|
51
|
+
ctx.ui.notify(msg("needTui"), "error");
|
|
51
52
|
return;
|
|
52
53
|
}
|
|
53
54
|
|
|
@@ -55,14 +56,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
55
56
|
const currentFile = ctx.sessionManager.getSessionFile();
|
|
56
57
|
const all = scanSessions().filter((s) => s.path !== currentFile);
|
|
57
58
|
if (!all.length) {
|
|
58
|
-
ctx.ui.notify("
|
|
59
|
+
ctx.ui.notify(msg("noSessions"), "info");
|
|
59
60
|
return;
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
const initial = ((args ?? "").trim() || lastQuery) ?? "";
|
|
63
64
|
type Picked = { s: PeekSession; action: "resume" | "fork" } | null;
|
|
64
65
|
const picked = await ctx.ui.custom<Picked>((tui, theme, _kb, done) => {
|
|
65
|
-
const comp = new PeekComponent(all, ctx.cwd, theme, process.stdout.rows || 24, initial);
|
|
66
|
+
const comp = new PeekComponent(all, ctx.cwd, theme, getMarkdownTheme(), process.stdout.rows || 24, initial);
|
|
66
67
|
comp.requestRender = () => tui.requestRender();
|
|
67
68
|
comp.onResume = (s) => {
|
|
68
69
|
lastQuery = comp.getQuery();
|
|
@@ -78,7 +79,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
78
79
|
};
|
|
79
80
|
comp.onDelete = async (s) => {
|
|
80
81
|
const ok = await deleteSession(s.path, (cmd, a) => pi.exec(cmd, a, { timeout: 4000 }));
|
|
81
|
-
ctx.ui.notify(ok ?
|
|
82
|
+
ctx.ui.notify(ok ? msg("deleted", { file: basename(s.path) }) : msg("deleteFailed"), ok ? "info" : "error");
|
|
82
83
|
return ok;
|
|
83
84
|
};
|
|
84
85
|
comp.onRename = async (s, name) => {
|
|
@@ -88,7 +89,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
88
89
|
} catch {
|
|
89
90
|
ok = false;
|
|
90
91
|
}
|
|
91
|
-
ctx.ui.notify(ok ?
|
|
92
|
+
ctx.ui.notify(ok ? msg("renamed", { name }) : msg("renameFailed"), ok ? "info" : "error");
|
|
92
93
|
return ok;
|
|
93
94
|
};
|
|
94
95
|
return comp;
|
|
@@ -102,16 +103,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
102
103
|
try {
|
|
103
104
|
const forked = SessionManager.forkFrom(picked.s.path, ctx.cwd);
|
|
104
105
|
target = forked.getSessionFile() ?? target;
|
|
105
|
-
ctx.ui.notify(
|
|
106
|
+
ctx.ui.notify(msg("forked", { file: basename(target) }), "info");
|
|
106
107
|
} catch (e) {
|
|
107
|
-
ctx.ui.notify(
|
|
108
|
+
ctx.ui.notify(msg("forkFailed", { error: e instanceof Error ? e.message : String(e) }), "error");
|
|
108
109
|
return;
|
|
109
110
|
}
|
|
110
111
|
}
|
|
111
112
|
|
|
112
113
|
const result = await ctx.switchSession(target);
|
|
113
114
|
if (result?.cancelled) {
|
|
114
|
-
ctx.ui.notify("
|
|
115
|
+
ctx.ui.notify(msg("switchCancelled"), "info");
|
|
115
116
|
}
|
|
116
117
|
},
|
|
117
118
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-session-peek",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
],
|
|
17
17
|
"repository": {
|
|
18
18
|
"type": "git",
|
|
19
|
-
"url": "https://github.com/Jeffropz/pi-session-peek.git"
|
|
19
|
+
"url": "git+https://github.com/Jeffropz/pi-session-peek.git"
|
|
20
20
|
},
|
|
21
21
|
"homepage": "https://github.com/Jeffropz/pi-session-peek#readme",
|
|
22
22
|
"bugs": "https://github.com/Jeffropz/pi-session-peek/issues",
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
"pi": {
|
|
33
33
|
"extensions": [
|
|
34
34
|
"./index.ts"
|
|
35
|
-
]
|
|
35
|
+
],
|
|
36
|
+
"image": "https://raw.githubusercontent.com/Jeffropz/pi-session-peek/main/docs/screenshot.png"
|
|
36
37
|
},
|
|
37
38
|
"scripts": {
|
|
38
39
|
"typecheck": "tsc --noEmit",
|
package/src/i18n.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
export type Lang = "zh" | "en";
|
|
2
|
+
|
|
3
|
+
const zh = {
|
|
4
|
+
flagPeek: "启动时打开会话搜索预览并预填关键词(--peek=关键词)",
|
|
5
|
+
flagRp: "启动时打开会话搜索预览(不带关键词)",
|
|
6
|
+
cmdDesc: "搜索历史会话:左右分栏预览、关键词高亮,Enter 进入对话",
|
|
7
|
+
needTui: "/peek 需要在交互模式(TUI)下使用",
|
|
8
|
+
noSessions: "没有找到历史会话",
|
|
9
|
+
deleted: "已删除会话 {file}",
|
|
10
|
+
deleteFailed: "删除会话失败",
|
|
11
|
+
renamed: "已重命名为「{name}」",
|
|
12
|
+
renameFailed: "重命名失败",
|
|
13
|
+
forked: "已分叉为新会话 {file}",
|
|
14
|
+
forkFailed: "分叉失败:{error}",
|
|
15
|
+
switchCancelled: "切换会话已取消",
|
|
16
|
+
searchPlaceholder: "关键词过滤(空格分隔=同时命中;@7d 限定近 7 天)",
|
|
17
|
+
renamePlaceholder: "新会话名(Enter 确认 / Esc 取消,留空取消)",
|
|
18
|
+
title: "🔍 会话搜索预览",
|
|
19
|
+
scopeLabel: " 范围[Tab]: ",
|
|
20
|
+
scopeAll: "全局",
|
|
21
|
+
scopeCurrent: "当前目录树",
|
|
22
|
+
timeLabel: " 时间: ",
|
|
23
|
+
matchCount: " 匹配 {n}/{total}",
|
|
24
|
+
noMatch: "没有匹配的会话",
|
|
25
|
+
msgCount: "{n} 条消息",
|
|
26
|
+
truncated: "(会话过长,仅显示最后 {n} 条)",
|
|
27
|
+
hitInTruncated: "关键词命中在未显示的更早消息中",
|
|
28
|
+
hitOnlyMeta: "关键词仅命中会话名或目录,对话正文无匹配",
|
|
29
|
+
you: "👤 你",
|
|
30
|
+
ai: "🤖 AI",
|
|
31
|
+
renamePrefix: "✏ 重命名: ",
|
|
32
|
+
confirmDelete: "⚠ 删除会话 [{desc}]?按 y / Enter 确认,任意其他键取消",
|
|
33
|
+
footer: "↑↓ 选择 PgUp/Dn·^U/^F·⇧↑↓ 滚动 ^N/^P 命中 Tab 范围 ^D 删除 ^R 改名 ^O 分叉 Enter 进入 Esc/^C 关闭",
|
|
34
|
+
since: "近{n}{unit}",
|
|
35
|
+
unitH: "小时",
|
|
36
|
+
unitD: "天",
|
|
37
|
+
unitW: "周",
|
|
38
|
+
unitM: "个月",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const en: Record<keyof typeof zh, string> = {
|
|
42
|
+
flagPeek: "Open the session picker at startup with a keyword (--peek=<keyword>)",
|
|
43
|
+
flagRp: "Open the session picker at startup",
|
|
44
|
+
cmdDesc: "Search session history: two-pane preview with keyword highlighting, Enter to resume",
|
|
45
|
+
needTui: "/peek only works in interactive (TUI) mode",
|
|
46
|
+
noSessions: "No sessions found",
|
|
47
|
+
deleted: "Deleted session {file}",
|
|
48
|
+
deleteFailed: "Failed to delete session",
|
|
49
|
+
renamed: "Renamed to “{name}”",
|
|
50
|
+
renameFailed: "Rename failed",
|
|
51
|
+
forked: "Forked into new session {file}",
|
|
52
|
+
forkFailed: "Fork failed: {error}",
|
|
53
|
+
switchCancelled: "Session switch cancelled",
|
|
54
|
+
searchPlaceholder: "Filter (space = all words must match; @7d = last 7 days)",
|
|
55
|
+
renamePlaceholder: "New session name (Enter to confirm, Esc to cancel, empty = cancel)",
|
|
56
|
+
title: "🔍 Session search",
|
|
57
|
+
scopeLabel: " Scope[Tab]: ",
|
|
58
|
+
scopeAll: "all projects",
|
|
59
|
+
scopeCurrent: "current dir tree",
|
|
60
|
+
timeLabel: " Time: ",
|
|
61
|
+
matchCount: " {n}/{total} matched",
|
|
62
|
+
noMatch: "No matching sessions",
|
|
63
|
+
msgCount: "{n} messages",
|
|
64
|
+
truncated: "(long session, showing the last {n} messages)",
|
|
65
|
+
hitInTruncated: "Keyword hits are in earlier messages not shown here",
|
|
66
|
+
hitOnlyMeta: "Keyword matched only the session name or directory, not the conversation",
|
|
67
|
+
you: "👤 You",
|
|
68
|
+
ai: "🤖 AI",
|
|
69
|
+
renamePrefix: "✏ Rename: ",
|
|
70
|
+
confirmDelete: "⚠ Delete session [{desc}]? y / Enter to confirm, any other key to cancel",
|
|
71
|
+
footer: "↑↓ select PgUp/Dn·^U/^F·⇧↑↓ scroll ^N/^P hits Tab scope ^D del ^R rename ^O fork Enter open Esc close",
|
|
72
|
+
since: "last {n}{unit}",
|
|
73
|
+
unitH: "h",
|
|
74
|
+
unitD: "d",
|
|
75
|
+
unitW: "w",
|
|
76
|
+
unitM: "mo",
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const messages = { zh, en };
|
|
80
|
+
export type MsgKey = keyof typeof zh;
|
|
81
|
+
|
|
82
|
+
function classify(v: string | undefined): Lang | undefined {
|
|
83
|
+
const s = v?.trim().toLowerCase();
|
|
84
|
+
if (!s || s === "c" || s === "posix") return undefined;
|
|
85
|
+
if (s.startsWith("zh")) return "zh";
|
|
86
|
+
if (s.startsWith("en")) return "en";
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// PI_SESSION_PEEK_LANG > LC_ALL / LC_MESSAGES / LANG > 系统区域;都认不出来就用英文
|
|
91
|
+
export function detectLang(
|
|
92
|
+
env: Record<string, string | undefined> = process.env,
|
|
93
|
+
systemLocale: () => string = () => Intl.DateTimeFormat().resolvedOptions().locale,
|
|
94
|
+
): Lang {
|
|
95
|
+
const forced = classify(env.PI_SESSION_PEEK_LANG);
|
|
96
|
+
if (forced) return forced;
|
|
97
|
+
for (const v of [env.LC_ALL, env.LC_MESSAGES, env.LANG]) {
|
|
98
|
+
const l = classify(v);
|
|
99
|
+
if (l) return l;
|
|
100
|
+
if (v && v.trim() && !["c", "posix"].includes(v.trim().toLowerCase())) return "en";
|
|
101
|
+
}
|
|
102
|
+
let loc = "";
|
|
103
|
+
try {
|
|
104
|
+
loc = systemLocale();
|
|
105
|
+
} catch {
|
|
106
|
+
}
|
|
107
|
+
return loc.toLowerCase().startsWith("zh") ? "zh" : "en";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let current: Lang = detectLang();
|
|
111
|
+
|
|
112
|
+
export function getLang(): Lang {
|
|
113
|
+
return current;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function setLang(lang: Lang): void {
|
|
117
|
+
current = lang;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function msg(key: MsgKey, vars?: Record<string, string | number>): string {
|
|
121
|
+
let s: string = messages[current][key];
|
|
122
|
+
if (vars) for (const [k, v] of Object.entries(vars)) s = s.split(`{${k}}`).join(String(v));
|
|
123
|
+
return s;
|
|
124
|
+
}
|