@zhushanwen/pi-session-reader 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/index.ts +1 -0
- package/package.json +47 -0
- package/src/__tests__/family.test.ts +207 -0
- package/src/__tests__/find.test.ts +214 -0
- package/src/__tests__/hash-provider.test.ts +498 -0
- package/src/__tests__/index.test.ts +193 -0
- package/src/__tests__/parser.test.ts +166 -0
- package/src/__tests__/real-data.ts +53 -0
- package/src/__tests__/render.test.ts +367 -0
- package/src/__tests__/roots.test.ts +132 -0
- package/src/__tests__/session-command.test.ts +198 -0
- package/src/__tests__/subagents.test.ts +430 -0
- package/src/__tests__/tool-handler.test.ts +510 -0
- package/src/__tests__/toolcall.test.ts +256 -0
- package/src/__tests__/tree.test.ts +101 -0
- package/src/__tests__/turns.test.ts +142 -0
- package/src/core/family.ts +240 -0
- package/src/core/parser.ts +160 -0
- package/src/core/render.ts +584 -0
- package/src/core/toolcall.ts +168 -0
- package/src/core/tree.ts +93 -0
- package/src/core/turns.ts +97 -0
- package/src/discovery/find.ts +247 -0
- package/src/discovery/roots.ts +92 -0
- package/src/discovery/subagents.ts +470 -0
- package/src/index.ts +189 -0
- package/src/tool-handler.ts +1160 -0
- package/src/tui/hash-provider.ts +230 -0
- package/src/tui/session-command.ts +85 -0
- package/vitest.config.ts +7 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent'
|
|
2
|
+
import { getAgentDir } from '@earendil-works/pi-coding-agent'
|
|
3
|
+
import { StringEnum } from '@earendil-works/pi-ai'
|
|
4
|
+
import { Type } from 'typebox'
|
|
5
|
+
import { handleSessionRead, type SessionReadParams } from './tool-handler.js'
|
|
6
|
+
import { createHashAutocompleteProvider } from './tui/hash-provider.js'
|
|
7
|
+
import { createSessionCommand } from './tui/session-command.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* pi-session-reader extension 入口(M3 工具适配层)。
|
|
11
|
+
*
|
|
12
|
+
* 分层(同 scheduler/cw-tool):
|
|
13
|
+
* - tool-handler.ts:纯逻辑 handler,agentDir 注入,零 pi 依赖,可单测
|
|
14
|
+
* - index.ts(本文件):pi 依赖层,registerTool + getAgentDir() 调用 + execute 闭包
|
|
15
|
+
* (catch handler 抛的 Error 转 isError:true,execute 不向 pi 抛——pi 工具契约)
|
|
16
|
+
*
|
|
17
|
+
* M4 将在此 addAutocompleteProvider(TUI # 补全,ctx.mode === 'tui' 时)。
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
// ---- TypeBox 参数 schema(design §3.4 14 字段)----
|
|
21
|
+
|
|
22
|
+
const SessionReadSchema = Type.Object({
|
|
23
|
+
action: StringEnum(
|
|
24
|
+
['find', 'family', 'outline', 'expand', 'detail', 'search', 'export', 'extract'],
|
|
25
|
+
{
|
|
26
|
+
description:
|
|
27
|
+
'Action to perform: find (locate session), family (fork/subagent/workflow relations), outline (turn-level overview), expand (single-turn entries), detail (full text of turns), search (full-text grep), export (materialize to file), extract (pull user messages / commands / files / commits / tool results by type).',
|
|
28
|
+
},
|
|
29
|
+
),
|
|
30
|
+
session: Type.Optional(
|
|
31
|
+
Type.String({
|
|
32
|
+
description:
|
|
33
|
+
'Session id or uuid fragment (e.g. e6c96). Required for family/outline/expand/detail/search/export. # prefix auto-stripped.',
|
|
34
|
+
}),
|
|
35
|
+
),
|
|
36
|
+
query: Type.Optional(
|
|
37
|
+
Type.String({
|
|
38
|
+
description:
|
|
39
|
+
'find action: uuid fragment / filename / name keyword / "recent" (returns most recent N).',
|
|
40
|
+
}),
|
|
41
|
+
),
|
|
42
|
+
turns: Type.Optional(
|
|
43
|
+
Type.String({
|
|
44
|
+
description: 'detail action: turn range, "T013-T015" or "T013".',
|
|
45
|
+
}),
|
|
46
|
+
),
|
|
47
|
+
turn: Type.Optional(
|
|
48
|
+
Type.String({ description: 'expand action: single turn, "T013".' }),
|
|
49
|
+
),
|
|
50
|
+
pattern: Type.Optional(
|
|
51
|
+
Type.String({ description: 'search action: substring or regex.' }),
|
|
52
|
+
),
|
|
53
|
+
scope: Type.Optional(
|
|
54
|
+
StringEnum(['all', 'user', 'assistant', 'toolResult'], {
|
|
55
|
+
description: 'search action: scope filter. Default all.',
|
|
56
|
+
}),
|
|
57
|
+
),
|
|
58
|
+
format: Type.Optional(
|
|
59
|
+
StringEnum(['outline', 'full', 'family'], {
|
|
60
|
+
description: 'export action: materialized form. Default outline.',
|
|
61
|
+
}),
|
|
62
|
+
),
|
|
63
|
+
includeToolResult: Type.Optional(
|
|
64
|
+
Type.Boolean({
|
|
65
|
+
description: 'detail/export: include toolResult full text. Default false (omitted as noise).',
|
|
66
|
+
}),
|
|
67
|
+
),
|
|
68
|
+
includeThinking: Type.Optional(
|
|
69
|
+
Type.Boolean({
|
|
70
|
+
description: 'detail: include thinking blocks. Default false (omitted as noise).',
|
|
71
|
+
}),
|
|
72
|
+
),
|
|
73
|
+
allBranches: Type.Optional(
|
|
74
|
+
Type.Boolean({
|
|
75
|
+
description: 'outline/family: include abandoned side-branches. Default false.',
|
|
76
|
+
}),
|
|
77
|
+
),
|
|
78
|
+
granularity: Type.Optional(
|
|
79
|
+
StringEnum(['turn', 'entry'], {
|
|
80
|
+
description: 'outline: turn-level or entry-flat. Default turn.',
|
|
81
|
+
}),
|
|
82
|
+
),
|
|
83
|
+
cwd: Type.Optional(
|
|
84
|
+
Type.String({ description: 'find: filter by cwd. Optional.' }),
|
|
85
|
+
),
|
|
86
|
+
limit: Type.Optional(
|
|
87
|
+
Type.Number({ description: 'find/search: max results. Default 20.' }),
|
|
88
|
+
),
|
|
89
|
+
what: Type.Optional(
|
|
90
|
+
StringEnum(
|
|
91
|
+
['user-messages', 'commands', 'files', 'commits', 'tool-results'],
|
|
92
|
+
{
|
|
93
|
+
description: 'extract action: what to extract (required for extract).',
|
|
94
|
+
},
|
|
95
|
+
),
|
|
96
|
+
),
|
|
97
|
+
tool: Type.Optional(
|
|
98
|
+
Type.String({
|
|
99
|
+
description: 'extract action: filter commands/tool-results by tool name (e.g. "bash").',
|
|
100
|
+
}),
|
|
101
|
+
),
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
// ---- guidelines(注入 LLM,design §3.4)----
|
|
105
|
+
|
|
106
|
+
const guidelines = [
|
|
107
|
+
'Progressive reading: outline (~500 token overview) → expand (one turn) → detail (full text). Default omits toolResult/thinking noise.',
|
|
108
|
+
'find first to locate a session by uuid fragment or name. TUI #references are uuid fragments.',
|
|
109
|
+
'outline before detail. Never read raw .jsonl files—use this tool.',
|
|
110
|
+
'family traces fork parents/children, subagent sessions, and workflow runs.',
|
|
111
|
+
'extract what=<type> to pull user messages / commands / files / commits / tool results across turns (optional tool= filter for commands/tool-results).',
|
|
112
|
+
'Errors carry a 👉 recovery hint—follow it to retry in one step.',
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
// ---- 工具 description(design §3.4,照搬措辞)----
|
|
116
|
+
|
|
117
|
+
const description = `Read pi session files (conversation history) by semantic structure instead of raw bytes. Use when you need to review another session, trace a fork/subagent/workflow family, or locate a past decision. Eight actions: find (locate by name/uuid fragment), family (fork/subagent/workflow relations), outline (turn-level overview, ~500 token), expand (single-turn entry list), detail (full text of turns), search (full-text grep across a session), export (materialize to file), extract (pull user messages / commands / files / commits / tool results by type). Progressive reading: outline → expand → detail. Do NOT use for the current session (use get_messages) or to edit sessions (pi has /resume /fork).`
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 已注册过 TUI provider/command 的 pi 实例集合。
|
|
121
|
+
*
|
|
122
|
+
* **为什么用 WeakSet<ExtensionAPI> 而非模块级布尔**:resume 会重新加载 extension 并
|
|
123
|
+
* 再次调用 factory(新 session = 新 pi/runner 实例)。模块级布尔跨 factory 持久,会误杀
|
|
124
|
+
* resume 的新 session(跳过 addAutocompleteProvider → 新 editor 没挂 # provider → # 不弹)。
|
|
125
|
+
* WeakSet 按 pi 实例去重:同一 pi 内多次 session_start 不重复注册(防 provider 堆叠),
|
|
126
|
+
* 但 resume 的新 pi 实例能正常注册。
|
|
127
|
+
*/
|
|
128
|
+
const registeredPis = new WeakSet<ExtensionAPI>()
|
|
129
|
+
/**
|
|
130
|
+
* 当前 session 的目录。每次 session_start(含 resume/fork/new)动态更新,provider/command
|
|
131
|
+
* 通过 getter 读取。
|
|
132
|
+
*/
|
|
133
|
+
let currentCwdSessionDir: string | null = null
|
|
134
|
+
|
|
135
|
+
export default function sessionReaderExtension(pi: ExtensionAPI): void {
|
|
136
|
+
pi.registerTool({
|
|
137
|
+
name: 'session_read',
|
|
138
|
+
label: 'Session Reader',
|
|
139
|
+
description,
|
|
140
|
+
parameters: SessionReadSchema,
|
|
141
|
+
promptGuidelines: guidelines,
|
|
142
|
+
async execute(
|
|
143
|
+
_toolCallId: string,
|
|
144
|
+
params: SessionReadParams,
|
|
145
|
+
signal: AbortSignal | undefined,
|
|
146
|
+
_onUpdate: unknown,
|
|
147
|
+
_ctx: ExtensionContext,
|
|
148
|
+
) {
|
|
149
|
+
try {
|
|
150
|
+
// signal 仅 search 消费(MF-5:长扫描可中断,Esc 不再挂死);其余 action 有界不接
|
|
151
|
+
return await handleSessionRead(params, getAgentDir(), signal)
|
|
152
|
+
} catch (e) {
|
|
153
|
+
const msg = e instanceof Error ? e.message : String(e)
|
|
154
|
+
return {
|
|
155
|
+
content: [{ type: 'text' as const, text: msg }],
|
|
156
|
+
details: {},
|
|
157
|
+
isError: true,
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
// ── M4 TUI 层(design §1 + §3.3 D-3/D-4 + 附录 P-hash-trigger)──────────
|
|
164
|
+
// # 引用补全 provider + /session-pick 命令(命令名避开 pi 内置 /session 冲突)。仅 ctx.mode === 'tui' 注册:RPC 模式
|
|
165
|
+
// (xyz-agent 子进程)不用 pi TUI editor / slash 命令,加载即跳过。
|
|
166
|
+
//
|
|
167
|
+
// addAutocompleteProvider 挂在 ctx.ui(非 ExtensionAPI),setup 入口无 ctx,
|
|
168
|
+
// 只能在 event handler 里拿——session_start 是最早且每 session 触发的 event。
|
|
169
|
+
// once-guard + ctx.mode 守卫 + typeof 运行时守卫三重防护。
|
|
170
|
+
//
|
|
171
|
+
// 2026-08-10 重构:数据源从全盘 findSessions(agentDir) 换为 SessionManager.listAll(ctx.sessionManager.getSessionDir())。
|
|
172
|
+
// getSessionDir() 返回当前 session 的目录(encoded cwd),listAll 只扫该目录 →
|
|
173
|
+
// 当前 cwd 化(G1)+ 白送 name/count/firstMessage(G3)+ 19ms vs 1500ms(G5)。
|
|
174
|
+
pi.on('session_start', (_event, ctx) => {
|
|
175
|
+
if (ctx.mode !== 'tui') return
|
|
176
|
+
// 每次 session_start(resume/fork/new 都触发)更新当前 session 目录;
|
|
177
|
+
// provider/command 通过 getter 动态读取,避免首个 session 闭包固定 → resume 后查错目录
|
|
178
|
+
currentCwdSessionDir = ctx.sessionManager.getSessionDir()
|
|
179
|
+
// 按 pi 实例去重:同一 pi 内不重复注册(防 provider 堆叠),resume 新 pi 实例可注册
|
|
180
|
+
if (registeredPis.has(pi)) return
|
|
181
|
+
if (typeof ctx.ui.addAutocompleteProvider !== 'function') return
|
|
182
|
+
registeredPis.add(pi)
|
|
183
|
+
const getCwdSessionDir = (): string => currentCwdSessionDir ?? ''
|
|
184
|
+
pi.registerCommand('session-pick', createSessionCommand(getCwdSessionDir))
|
|
185
|
+
ctx.ui.addAutocompleteProvider((current) =>
|
|
186
|
+
createHashAutocompleteProvider(getCwdSessionDir, current),
|
|
187
|
+
)
|
|
188
|
+
})
|
|
189
|
+
}
|