pi-timeline 0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hayston1001
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # pi-timeline
2
+
3
+ English | [简体中文](README_zh.md)
4
+
5
+ A message timeline overview built for the case where a long conversation in pi
6
+ makes it painful to get back to a point: it lists the session's user messages
7
+ (`!` shell commands included), filters them as you type, and scrolls the
8
+ transcript viewport precisely to the one you pick.
9
+
10
+ > [!IMPORTANT]
11
+ > The **TUI mode** is required.
12
+
13
+ ## Usage
14
+
15
+ | Key | Action |
16
+ | --- | --- |
17
+ | `alt+h` (configurable) | Open the message list |
18
+ | just type | Filter messages |
19
+ | `↑`/`↓` (or `PgUp`/`PgDn`) | Move the selection |
20
+ | `Enter` | Jump |
21
+ | `Ctrl+Enter` | Insert the message text into the editor |
22
+ | `Esc` | Close |
23
+
24
+ > [!TIP]
25
+ > `/timeline-settings` opens the settings panel.
26
+
27
+ > [!NOTE] Jump target
28
+ > `user` the user message itself
29
+ > `reply` the final answer
30
+
31
+ > [!WARNING]
32
+ > Invalid shortcuts are rejected. Without a modifier, only named keys (such as
33
+ > `f2`) are accepted.
34
+
35
+ ## Install
36
+
37
+ ```sh
38
+ pi install npm:pi-timeline
39
+ pi -e npm:pi-timeline # one-shot trial
40
+ ```
41
+
42
+ ## Configuration file
43
+
44
+ At `~/.pi/agent/timeline.json`
45
+
46
+ | Key | Type | Default | Meaning |
47
+ | --- | --- | --- | --- |
48
+ | `shortcut` | string / array of strings / null | `"alt+h"` | Case-insensitive; duplicates removed automatically |
49
+ | `jumpTo` | string | `"user"` | `"user"` / `"reply"` |
50
+ | `language` | string | `"auto"` | `"auto"` / `"zh"` / `"en"` |
51
+
52
+ > [!WARNING]
53
+ > A bad value falls back to the default and raises a warning when the session starts. `jumpTo` is re-read on every invocation, so changing it applies immediately; the shortcut is registered at load time, so changing it needs `/reload`.
54
+
55
+ ## Development
56
+
57
+ ```sh
58
+ node test/run.mjs # all tests
59
+ node test/run.mjs unit config # named suites: unit | real | config
60
+ ```
package/README_zh.md ADDED
@@ -0,0 +1,58 @@
1
+ # pi-timeline
2
+
3
+ [English](README.md) | 简体中文
4
+
5
+ ## 这是什么
6
+
7
+ 为解决 pi 中长对话无法便捷跳转节点而打造的消息时间线概览, 可列出会话里的用户消息(含 `!` shell 命令), 输入关键字过滤, 选中后精准滚动 transcript 视口
8
+
9
+ > [!IMPORTANT]
10
+ > 必须使用 **TUI 模式**
11
+
12
+ ## 如何使用
13
+
14
+ | 按键 | 作用 |
15
+ | --- | --- |
16
+ | `alt+h`(可自定义) | 打开消息列表 |
17
+ | 直接输入 | 过滤消息 |
18
+ | `↑`/`↓`(或 `PgUp`/`PgDn`) | 移动选中项 |
19
+ | `Enter` | 跳转 |
20
+ | `Ctrl+Enter` | 把消息原文放进输入框 |
21
+ | `Esc` | 关闭 |
22
+
23
+ > [!TIP]
24
+ > `/timeline-settings` 打开设置面板
25
+
26
+ > [!NOTE] 落脚点
27
+ > `user` 用户消息
28
+ > `reply` 最终回答
29
+
30
+ > [!WARNING]
31
+ > 不合法的快捷键会被拒绝. 不带修饰键的键只有命名键(如 `f2`)才放行.
32
+
33
+ ## 如何安装
34
+
35
+ ```sh
36
+ pi install npm:pi-timeline
37
+ pi -e npm:pi-timeline # 一次性试用
38
+ ```
39
+
40
+ ## 配置文件
41
+
42
+ 位于 `~/.pi/agent/timeline.json`
43
+
44
+ | 键 | 类型 | 默认 | 说明 |
45
+ | --- | --- | --- | --- |
46
+ | `shortcut` | 字符串/字符串数组/null | `"alt+h"` | 大小写不敏感, 自动去重 |
47
+ | `jumpTo` | 字符串 | `"user"` | `"user"`/`"reply"` |
48
+ | `language` | 字符串 | `"auto"` | `"auto"`/`"zh"`/`"en"` |
49
+
50
+ > [!WARNING]
51
+ > 值不合法时退回默认值, 并在会话启动时弹警告.`jumpTo` 每次调用都重读, 改完立即生效; 快捷键在加载期注册, 改动要 `/reload`.
52
+
53
+ ## 开发
54
+
55
+ ```sh
56
+ node test/run.mjs # 全部测试
57
+ node test/run.mjs unit config # 指定测试集: unit | real | config
58
+ ```
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "pi-timeline",
3
+ "version": "0.1.0",
4
+ "description": "Message timeline for pi: list a session's user messages and jump to any of them with exact, instant scrolling.",
5
+ "keywords": ["pi-package", "pi-extension", "timeline", "navigation"],
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Hayston1001/pi-extensions.git",
10
+ "directory": "packages/timeline"
11
+ },
12
+ "files": ["src/index.ts", "src/i18n.ts", "README.md", "README_zh.md"],
13
+ "pi": {
14
+ "extensions": ["./src/index.ts"]
15
+ },
16
+ "peerDependencies": {
17
+ "@earendil-works/pi-coding-agent": "*",
18
+ "@earendil-works/pi-tui": "*"
19
+ }
20
+ }
package/src/i18n.ts ADDED
@@ -0,0 +1,273 @@
1
+ /*
2
+ * timeline · 界面文案(zh / en)
3
+ *
4
+ * 所有用户可见字符串集中在这一张表里; 配置的 `language` 字段(`auto` | `zh` | `en`)决定用哪一套,
5
+ * `auto` 跟随系统区域. 新增文案两套都要写.
6
+ */
7
+
8
+ /* 配置文件名: configPath() 和文案里的提示共用这一个名字 */
9
+ export const CONFIG_FILE_NAME = "timeline.json";
10
+
11
+ export type Language = "auto" | "zh" | "en";
12
+ /** 解析后的实际语言(auto 已展开) */
13
+ export type Lang = "zh" | "en";
14
+
15
+ export const LANGUAGES: Language[] = ["auto", "zh", "en"];
16
+
17
+ /** 系统区域判定: 环境变量优先, 否则看 Node 的区域信息(zh-CN / en-US ...). */
18
+ export function detectSystemLang(): Lang {
19
+ let tag = "";
20
+ try {
21
+ tag = process.env.LC_ALL || process.env.LC_MESSAGES || process.env.LANG || process.env.LANGUAGE || "";
22
+ } catch {
23
+ // 取不到环境变量不算错误
24
+ }
25
+ if (!tag) {
26
+ try {
27
+ tag = Intl.DateTimeFormat().resolvedOptions().locale ?? "";
28
+ } catch {
29
+ // 没有 ICU 时退回英文
30
+ }
31
+ }
32
+ return /^zh/i.test(tag) ? "zh" : "en";
33
+ }
34
+
35
+ export function resolveLang(language: Language | undefined): Lang {
36
+ if (language === "zh" || language === "en") return language;
37
+ return detectSystemLang();
38
+ }
39
+
40
+ /** 设置项的 id: 既是面板里的行 id, 也是配置的字段名 */
41
+ export type SettingId = "jumpTo" | "shortcut" | "language";
42
+
43
+ /** 落脚点: 跳到消息本身, 还是跳到下面的正文(助手的回答) */
44
+ export type JumpTo = "user" | "reply";
45
+ export const JUMP_TO_VALUES: JumpTo[] = ["user", "reply"];
46
+
47
+ export interface SettingText {
48
+ label: string;
49
+ description: string;
50
+ }
51
+
52
+ export interface Messages {
53
+ /** 快捷键在命令面板里的 description(`/timeline-settings` 的命令描述是固定英文, 不走这张表) */
54
+ shortcutDescription: string;
55
+
56
+ // —— 消息列表 ——
57
+ pickerCount: (count: number) => string;
58
+ pickerMode: (mode: string) => string;
59
+ pickerUnlocatable: (count: number) => string;
60
+ pickerFilter: string;
61
+ pickerNoMatch: string;
62
+ pickerHint: string;
63
+ currentTag: string;
64
+ unlocatableTag: string;
65
+ emptyMessage: string;
66
+
67
+ // —— 拿不到视口时的说明面板 ——
68
+ noticeNoViewportTitle: string;
69
+ noticeNoViewportBody: string;
70
+ noticeFullscreenTitle: string;
71
+ noticeFullscreenBody1: string;
72
+ noticeFullscreenBody2: string;
73
+ noticeFullscreenHint: string;
74
+ noticeCloseHint: string;
75
+
76
+ // —— 通知 ——
77
+ notifyTuiOnly: string;
78
+ notifyNoMessages: string;
79
+ notifyInserted: string;
80
+ notifyNotLocatable: string;
81
+ notifyBadShortcut: (key: string, problem: string) => string;
82
+ notifyShortcutChanged: (key: string) => string;
83
+ notifySavedWithShortcut: string;
84
+ notifySaved: string;
85
+ /** 没有界面时打印的当前设置 */
86
+ status: (jumpTo: string, shortcut: string, language: string) => string;
87
+
88
+ // —— 配置告警 ——
89
+ configParseError: (message: string, path: string) => string;
90
+ configBadJumpTo: (value: string, path: string) => string;
91
+ configBadShortcutType: (path: string) => string;
92
+ configBadShortcut: (entry: string, problem: string, path: string) => string;
93
+ configNoShortcut: (keys: string) => string;
94
+
95
+ // —— 快捷键校验 ——
96
+ validateMissingKey: string;
97
+ validateBadModifier: (part: string) => string;
98
+ validatePlainKey: string;
99
+ validateBadKey: (base: string) => string;
100
+
101
+ // —— 设置面板 ——
102
+ settingsTitle: string;
103
+ settings: Record<SettingId, SettingText>;
104
+ jumpToLabels: Record<JumpTo, string>;
105
+ languageLabels: Record<Language, string>;
106
+ shortcutOffValue: string;
107
+ customShortcutChoice: string;
108
+ shortcutOffChoice: string;
109
+ customShortcutTitle: string;
110
+ customShortcutPlaceholder: string;
111
+
112
+ // —— 降级对话框 ——
113
+ dialogTitle: (label: string) => string;
114
+ pickPrompt: (title: string, current: string) => string;
115
+ }
116
+
117
+ const ZH: Messages = {
118
+ shortcutDescription: "Timeline: 跳到某条用户消息",
119
+
120
+ pickerCount: (count) => `· 消息 ${count} 条`,
121
+ pickerMode: (mode) => `· 模式: ${mode}`,
122
+ pickerUnlocatable: (count) => `(${count} 条无法定位)`,
123
+ pickerFilter: "过滤消息内容...",
124
+ pickerNoMatch: " 没有匹配的消息",
125
+ pickerHint: "↑↓ 选择 · Enter 跳转 · Ctrl+Enter 放入输入框 · Esc 关闭",
126
+ currentTag: "当前",
127
+ unlocatableTag: "不可定位",
128
+ emptyMessage: "(空白消息)",
129
+
130
+ noticeNoViewportTitle: "找不到 transcript 视口",
131
+ noticeNoViewportBody:
132
+ "全屏模式下也没拿到可滚动的 transcript. 这通常意味着 Pi 的内部结构变了(版本升级), 插件需要跟着改. ",
133
+ noticeFullscreenTitle: "timeline 需要全屏模式",
134
+ noticeFullscreenBody1: "普通模式下的 transcript 由终端自己管理, 插件无法定位到其中某一行. ",
135
+ noticeFullscreenBody2: "开启全屏后 transcript 归 Pi 管, 才能按行滚过去. ",
136
+ noticeFullscreenHint: "/settings → tui-mode → fullscreen 后重试 · 按任意键关闭",
137
+ noticeCloseHint: "按任意键关闭",
138
+
139
+ notifyTuiOnly: "timeline: 仅交互模式可用",
140
+ notifyNoMessages: "timeline: 这个会话还没有可跳转的消息",
141
+ notifyInserted: "timeline: 消息内容已放进输入框",
142
+ notifyNotLocatable: "timeline: 这条消息在当前视图里定位不到(可能已被压缩, 或渲染中没有对应内容)",
143
+ notifyBadShortcut: (key, problem) => `timeline: 快捷键「${key}」用不了: ${problem}`,
144
+ notifyShortcutChanged: (key) => `timeline: 快捷键已改成 ${key}, /reload 后生效`,
145
+ notifySavedWithShortcut: "timeline: 设置已保存(jumpTo 立即生效; 快捷键需要 /reload)",
146
+ notifySaved: "timeline: 设置已保存, 立即生效",
147
+ status: (jumpTo, shortcut, language) =>
148
+ `timeline 当前设置:\n落脚点: ${jumpTo}\n快捷键: ${shortcut}\n语言: ${language}`,
149
+
150
+ configParseError: (message, path) => `${CONFIG_FILE_NAME} 解析失败: ${message}(${path})`,
151
+ configBadJumpTo: (value, path) =>
152
+ `${CONFIG_FILE_NAME} 里的 "jumpTo" 只认 "user"(跳到消息)或 "reply"(跳到下方正文), 现在写的是 ${value}, 先按 "user" 处理(${path})`,
153
+ configBadShortcutType: (path) => `${CONFIG_FILE_NAME} 里的 "shortcut" 只能是字符串或字符串数组(${path})`,
154
+ configBadShortcut: (entry, problem, path) => `快捷键「${entry}」用不了: ${problem}(${path})`,
155
+ configNoShortcut: (keys) => `没有可用的快捷键, 先退回默认的 ${keys}`,
156
+
157
+ validateMissingKey: "缺少按键(例如 ctrl+shift+g)",
158
+ validateBadModifier: (part) => `认不出的修饰键「${part}」`,
159
+ validatePlainKey: "不带修饰键的普通按键会把正常输入也吃掉, 请至少加上 ctrl / alt / shift / super",
160
+ validateBadKey: (base) => `认不出的按键「${base}」`,
161
+
162
+ settingsTitle: "Timeline 设置",
163
+ settings: {
164
+ jumpTo: {
165
+ label: "落脚点",
166
+ description:
167
+ "跳过去时落在哪一块: 「消息本身」是那条用户消息; 「下方正文」是这一轮的真正回答. 纯文本回复落块的第一行(和 pi 原生 Ctrl+↑/↓ 一致), 带思考的回复跳过思考落正文. 回车切换. ",
168
+ },
169
+ shortcut: {
170
+ label: "快捷键",
171
+ description:
172
+ "打开消息列表的快捷键; 需要 /reload 才会重新注册. 回车进入候选列表, 选「自定义...」可以直接输一个键. ",
173
+ },
174
+ language: {
175
+ label: "Language",
176
+ description: "界面语言: 自动 = 跟随系统区域. 改完面板会立即以新语言重开; 快捷键的说明文字需 /reload. ",
177
+ },
178
+ },
179
+ jumpToLabels: { user: "消息本身(user)", reply: "下方正文(reply)" },
180
+ languageLabels: { auto: "自动", zh: "中文", en: "English" },
181
+ shortcutOffValue: "关闭",
182
+ customShortcutChoice: "自定义...",
183
+ shortcutOffChoice: "关闭快捷键",
184
+ customShortcutTitle: "快捷键",
185
+ customShortcutPlaceholder: "例如 alt+t / f8(不带修饰键的普通键不行)",
186
+
187
+ dialogTitle: (label) => `${label}? `,
188
+ pickPrompt: (title, current) => `${title}(当前: ${current})`,
189
+ };
190
+
191
+ const EN: Messages = {
192
+ shortcutDescription: "Timeline: jump to a user message",
193
+
194
+ pickerCount: (count) => `· ${count} messages`,
195
+ pickerMode: (mode) => `· Mode: ${mode}`,
196
+ pickerUnlocatable: (count) => ` (${count} unlocatable)`,
197
+ pickerFilter: "Filter messages...",
198
+ pickerNoMatch: " No matching messages",
199
+ pickerHint: "↑↓ Navigate · Enter jump · Ctrl+Enter insert into editor · Esc close",
200
+ currentTag: "current",
201
+ unlocatableTag: "unlocatable",
202
+ emptyMessage: "(empty message)",
203
+
204
+ noticeNoViewportTitle: "Transcript viewport not found",
205
+ noticeNoViewportBody:
206
+ "No scrollable transcript even in fullscreen mode. Pi's internals probably changed (version upgrade) and this extension needs an update.",
207
+ noticeFullscreenTitle: "timeline needs fullscreen mode",
208
+ noticeFullscreenBody1:
209
+ "In regular mode the transcript is managed by the terminal itself, so the extension cannot jump to a specific line.",
210
+ noticeFullscreenBody2: "Fullscreen mode hands the transcript to Pi, which makes line-precise scrolling possible.",
211
+ noticeFullscreenHint: "/settings → tui-mode → fullscreen, then retry · Press any key to close",
212
+ noticeCloseHint: "Press any key to close",
213
+
214
+ notifyTuiOnly: "timeline: only available in the interactive TUI",
215
+ notifyNoMessages: "timeline: no messages to jump to in this session",
216
+ notifyInserted: "timeline: message text inserted into the editor",
217
+ notifyNotLocatable:
218
+ "timeline: cannot locate this message in the current view (it may have been compacted away)",
219
+ notifyBadShortcut: (key, problem) => `timeline: shortcut "${key}" does not work: ${problem}`,
220
+ notifyShortcutChanged: (key) => `timeline: shortcut changed to ${key}; run /reload to apply`,
221
+ notifySavedWithShortcut: "timeline: settings saved (jump target applies immediately; the shortcut needs /reload)",
222
+ notifySaved: "timeline: settings saved and applied immediately",
223
+ status: (jumpTo, shortcut, language) =>
224
+ `timeline settings:\nJump target: ${jumpTo}\nShortcut: ${shortcut}\nLanguage: ${language}`,
225
+
226
+ configParseError: (message, path) => `${CONFIG_FILE_NAME} failed to parse: ${message} (${path})`,
227
+ configBadJumpTo: (value, path) =>
228
+ `"jumpTo" in ${CONFIG_FILE_NAME} only accepts "user" (the message) or "reply" (the answer below), got ${value}; using "user" (${path})`,
229
+ configBadShortcutType: (path) => `"shortcut" in ${CONFIG_FILE_NAME} must be a string or an array of strings (${path})`,
230
+ configBadShortcut: (entry, problem, path) => `shortcut "${entry}" does not work: ${problem} (${path})`,
231
+ configNoShortcut: (keys) => `no usable shortcut; falling back to the default ${keys}`,
232
+
233
+ validateMissingKey: "missing key (e.g. ctrl+shift+g)",
234
+ validateBadModifier: (part) => `unknown modifier "${part}"`,
235
+ validatePlainKey: "a plain key would swallow normal typing; add at least ctrl / alt / shift / super",
236
+ validateBadKey: (base) => `unknown key "${base}"`,
237
+
238
+ settingsTitle: "Timeline Settings",
239
+ settings: {
240
+ jumpTo: {
241
+ label: "Jump target",
242
+ description:
243
+ "Which block the jump lands on: 'the user message' is the message itself, 'the reply below' is this turn's real answer. Plain-text replies land on the block's first line (same as pi's native Ctrl+up/down); replies with thinking skip the thinking and land on the answer text. Enter to switch.",
244
+ },
245
+ shortcut: {
246
+ label: "Shortcut",
247
+ description:
248
+ "Shortcut that opens the message list; it needs /reload to re-register. Enter for the candidate list; pick 'Custom...' to type a key.",
249
+ },
250
+ language: {
251
+ label: "Language",
252
+ description:
253
+ "UI language: Auto = follow the system locale. The panel reopens immediately in the new language; the shortcut's help text needs /reload.",
254
+ },
255
+ },
256
+ jumpToLabels: { user: "the user message (user)", reply: "the reply below (reply)" },
257
+ languageLabels: { auto: "Auto", zh: "中文", en: "English" },
258
+ shortcutOffValue: "off",
259
+ customShortcutChoice: "Custom...",
260
+ shortcutOffChoice: "Disable shortcut",
261
+ customShortcutTitle: "Shortcut",
262
+ customShortcutPlaceholder: "e.g. alt+t / f8 (plain keys without a modifier are rejected)",
263
+
264
+ dialogTitle: (label) => `${label}?`,
265
+ pickPrompt: (title, current) => `${title} (current: ${current})`,
266
+ };
267
+
268
+ export const MESSAGES: Record<Lang, Messages> = { zh: ZH, en: EN };
269
+
270
+ /** 按配置里的 language 取文案(auto 跟随系统). */
271
+ export function messages(language: Language | undefined): Messages {
272
+ return MESSAGES[resolveLang(language)];
273
+ }