pi-web-ui 0.91.0 → 0.93.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 +90 -1
- package/README.md +69 -14
- package/README.zh-CN.md +36 -10
- package/bin/pi-web-ui.mjs +39 -8
- package/dist/server/agent-service.js +938 -163
- package/dist/server/auth-cookie.js +26 -0
- package/dist/server/claim-files-tool.js +115 -0
- package/dist/server/claim-store.js +301 -0
- package/dist/server/client-state.js +114 -6
- package/dist/server/conversation-read-tool.js +371 -28
- package/dist/server/conversation-touches.js +357 -0
- package/dist/server/delegate-task.js +10 -3
- package/dist/server/dsh/dsh-agent-service.js +50 -1
- package/dist/server/files-service.js +78 -26
- package/dist/server/http-proxy.js +72 -0
- package/dist/server/index.js +94 -11
- package/dist/server/model-admin.js +124 -2
- package/dist/server/model-enrich.js +478 -0
- package/dist/server/patch-node-pty.js +11 -0
- package/dist/server/plugin-schedule.js +27 -2
- package/dist/server/plugins.js +66 -10
- package/dist/server/present-files-tool.js +303 -0
- package/dist/server/protocol-version.js +1 -1
- package/dist/server/read-tool.js +125 -0
- package/dist/server/resolve-global-sdk.js +72 -0
- package/dist/server/sdk-origin.js +64 -0
- package/dist/server/serialize.js +80 -22
- package/dist/server/settings-service.js +19 -1
- package/dist/server/skill-tool.js +2 -1
- package/dist/server/subagents.js +30 -13
- package/dist/server/terminals.js +38 -18
- package/dist/server/text-sniff.js +15 -0
- package/dist/server/themes.js +32 -2
- package/dist/server/tool-info.js +89 -0
- package/dist/server/tool-manager.js +110 -11
- package/package.json +2 -2
- package/plugin-sdk/README.md +1 -0
- package/plugins/catalog.json +20 -0
- package/themes/ayu-light.css +136 -0
- package/themes/catppuccin-latte.css +140 -0
- package/themes/catppuccin.css +144 -0
- package/themes/codex.css +136 -0
- package/themes/cyberpunk.css +4 -0
- package/themes/dazzle.css +4 -0
- package/themes/everforest-light.css +136 -0
- package/themes/geist.css +136 -0
- package/themes/gruvbox-light.css +136 -0
- package/themes/kanagawa-lotus.css +136 -0
- package/themes/md-preview.css +4 -0
- package/themes/mist.css +4 -0
- package/themes/nord.css +134 -0
- package/themes/one-dark.css +135 -0
- package/themes/paper.css +16 -12
- package/themes/rose-pine-dawn.css +136 -0
- package/themes/sakura.css +4 -0
- package/themes/solarized-light.css +135 -0
- package/themes/tokyo-night.css +144 -0
- package/themes/white.css +4 -0
- package/web/dist/assets/{TerminalPanel-GtDQ1h85.js → TerminalPanel-C1A23aI-.js} +1 -1
- package/web/dist/assets/index-B48QWf0I.js +364 -0
- package/web/dist/assets/index-DGL5OInG.css +1 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-CdsKltOS.css +0 -1
- package/web/dist/assets/index-OoULNF70.js +0 -363
|
@@ -22,14 +22,24 @@
|
|
|
22
22
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
23
23
|
import { Type } from "typebox";
|
|
24
24
|
import { bilingual, pick } from "./i18n.js";
|
|
25
|
+
import { CONVERSATION_READ_TOOL_NAME } from "./tool-manager.js";
|
|
26
|
+
import { extractTouches, formatTouchEntry, formatTouchesCompact, toolCallRefsOfContent, unionTouchLists, } from "./conversation-touches.js";
|
|
27
|
+
/** 由 content 块提 toolCalls 字段(additive;格式化输出不受影响,见接口注释)。 */
|
|
28
|
+
function fillToolCalls(target, content) {
|
|
29
|
+
const refs = toolCallRefsOfContent(content);
|
|
30
|
+
if (refs.length > 0)
|
|
31
|
+
target.toolCalls = refs;
|
|
32
|
+
}
|
|
25
33
|
/** AgentMessage → 转录最小结构(角色原样保留,内容不动,格式化延后)。 */
|
|
26
34
|
export function toTranscriptInput(m) {
|
|
27
35
|
const role = m.role;
|
|
28
36
|
const r = typeof role === "string" ? role : "unknown";
|
|
29
37
|
const base = { role: r };
|
|
30
38
|
const anyM = m;
|
|
31
|
-
if (typeof anyM.content !== "undefined")
|
|
39
|
+
if (typeof anyM.content !== "undefined") {
|
|
32
40
|
base.content = anyM.content;
|
|
41
|
+
fillToolCalls(base, anyM.content);
|
|
42
|
+
}
|
|
33
43
|
if (typeof anyM.toolName === "string")
|
|
34
44
|
base.toolName = anyM.toolName;
|
|
35
45
|
if (typeof anyM.isError === "boolean")
|
|
@@ -118,8 +128,14 @@ function roleLabel(m) {
|
|
|
118
128
|
return `tool result${m.toolName ? ` (${m.toolName})` : ""}`;
|
|
119
129
|
return m.role;
|
|
120
130
|
}
|
|
131
|
+
/** 单条消息截断:超出部分标注剩余字符数(调用方一眼知道丢了多少)。 */
|
|
132
|
+
export function truncateCounted(s, cap) {
|
|
133
|
+
if (s.length <= cap)
|
|
134
|
+
return s;
|
|
135
|
+
return `${s.slice(0, cap)}\n… +${s.length - cap} chars`;
|
|
136
|
+
}
|
|
121
137
|
function trunc(s, cap) {
|
|
122
|
-
return s
|
|
138
|
+
return truncateCounted(s, cap);
|
|
123
139
|
}
|
|
124
140
|
/** 转录分页格式化:序号形如 [12/135 user],尾部带翻页提示位(调用方拼 header)。 */
|
|
125
141
|
export function formatTranscript(messages, opts) {
|
|
@@ -165,8 +181,10 @@ export function parseTranscriptLines(text) {
|
|
|
165
181
|
if (e?.type === "message" && e.message && typeof e.message.role === "string") {
|
|
166
182
|
const msg = e.message;
|
|
167
183
|
const m = { role: msg.role };
|
|
168
|
-
if (typeof msg.content !== "undefined")
|
|
184
|
+
if (typeof msg.content !== "undefined") {
|
|
169
185
|
m.content = msg.content;
|
|
186
|
+
fillToolCalls(m, msg.content);
|
|
187
|
+
}
|
|
170
188
|
if (typeof msg.toolName === "string")
|
|
171
189
|
m.toolName = msg.toolName;
|
|
172
190
|
if (typeof msg.isError === "boolean")
|
|
@@ -191,6 +209,134 @@ export function parseTranscriptLines(text) {
|
|
|
191
209
|
function matchesHay(hay, q) {
|
|
192
210
|
return q === "" || hay.toLowerCase().includes(q);
|
|
193
211
|
}
|
|
212
|
+
/** read 选择器(纯函数):view → query → last 的顺序固定,见注释。 */
|
|
213
|
+
export function selectReadMessages(messages, opts) {
|
|
214
|
+
const list = Array.isArray(messages) ? messages : [];
|
|
215
|
+
const view = opts?.view === "full" ? "full" : "chat";
|
|
216
|
+
const inView = view === "full" ? list : list.filter((m) => m.role === "user" || m.role === "assistant");
|
|
217
|
+
const q = (opts?.query ?? "").trim().toLowerCase();
|
|
218
|
+
let selected = inView;
|
|
219
|
+
let hitIndices = [];
|
|
220
|
+
if (q) {
|
|
221
|
+
// 在 view 内按转录文本搜:hitIndices 只记命中(报数/定位用),
|
|
222
|
+
// selected 是命中 ±1 条上下文展开(去重保序)。
|
|
223
|
+
const hits = [];
|
|
224
|
+
const keep = new Set();
|
|
225
|
+
for (let i = 0; i < inView.length; i++) {
|
|
226
|
+
if (transcriptText(inView[i]).toLowerCase().includes(q)) {
|
|
227
|
+
hits.push(i);
|
|
228
|
+
for (let j = Math.max(0, i - 1); j <= Math.min(inView.length - 1, i + 1); j++)
|
|
229
|
+
keep.add(j);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
hitIndices = hits;
|
|
233
|
+
selected = [...keep].sort((a, b) => a - b).map((i) => inView[i]);
|
|
234
|
+
}
|
|
235
|
+
const last = opts?.last;
|
|
236
|
+
if (typeof last === "number" && Number.isFinite(last)) {
|
|
237
|
+
const n = Math.min(200, Math.max(1, Math.floor(last)));
|
|
238
|
+
if (selected.length > n) {
|
|
239
|
+
selected = selected.slice(selected.length - n);
|
|
240
|
+
// last 裁掉上下文外层时命中序号跟走(只留仍在选区内的)。
|
|
241
|
+
if (hitIndices.length > 0) {
|
|
242
|
+
const keep = new Set(selected);
|
|
243
|
+
hitIndices = hitIndices.filter((i) => keep.has(inView[i]));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return { selected, totalInView: inView.length, hitIndices };
|
|
248
|
+
}
|
|
249
|
+
/** 工具参数里的一句话提示(path/command,单行 ≤80 字符;拿不到就空串)。 */
|
|
250
|
+
function toolHint(args) {
|
|
251
|
+
if (!args || typeof args !== "object")
|
|
252
|
+
return "";
|
|
253
|
+
const a = args;
|
|
254
|
+
for (const k of ["path", "file_path", "filePath", "command"]) {
|
|
255
|
+
const v = a[k];
|
|
256
|
+
if (typeof v === "string" && v.trim()) {
|
|
257
|
+
const oneLine = v.trim().replace(/\s+/g, " ");
|
|
258
|
+
return oneLine.length > 80 ? `${oneLine.slice(0, 80)}…` : oneLine;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return "";
|
|
262
|
+
}
|
|
263
|
+
/** action=status 的摘要器(纯函数):只读转录,不碰 host。 */
|
|
264
|
+
export function summarizeConversation(messages) {
|
|
265
|
+
const list = Array.isArray(messages) ? messages : [];
|
|
266
|
+
let lastTool;
|
|
267
|
+
let lastAssistant;
|
|
268
|
+
let lastAskIdx = -1;
|
|
269
|
+
let lastAskResultIdx = -1;
|
|
270
|
+
for (let i = 0; i < list.length; i++) {
|
|
271
|
+
const m = list[i];
|
|
272
|
+
if (!m || typeof m !== "object")
|
|
273
|
+
continue;
|
|
274
|
+
if (m.role === "assistant") {
|
|
275
|
+
const textParts = [];
|
|
276
|
+
const calls = toolCallRefsOfContent(m.content);
|
|
277
|
+
if (m.toolCalls && m.toolCalls.length > 0) {
|
|
278
|
+
for (const c of m.toolCalls) {
|
|
279
|
+
if (c && typeof c.name === "string") {
|
|
280
|
+
lastTool = { name: c.name, hint: toolHint(c.args) };
|
|
281
|
+
if (c.name === "ask_user_question")
|
|
282
|
+
lastAskIdx = i;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
for (const c of calls) {
|
|
288
|
+
lastTool = { name: c.name, hint: toolHint(c.args) };
|
|
289
|
+
if (c.name === "ask_user_question")
|
|
290
|
+
lastAskIdx = i;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (Array.isArray(m.content)) {
|
|
294
|
+
for (const b of m.content) {
|
|
295
|
+
if (b && typeof b === "object" && b.type === "text") {
|
|
296
|
+
const t = b.text;
|
|
297
|
+
if (typeof t === "string" && t)
|
|
298
|
+
textParts.push(t);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
const joined = textParts.join("\n").trim();
|
|
303
|
+
if (joined)
|
|
304
|
+
lastAssistant = joined;
|
|
305
|
+
}
|
|
306
|
+
else if (m.role === "bashExecution" && typeof m.command === "string") {
|
|
307
|
+
lastTool = { name: "bash", hint: toolHint({ command: m.command }) };
|
|
308
|
+
}
|
|
309
|
+
else if (m.role === "toolResult" && m.toolName === "ask_user_question") {
|
|
310
|
+
lastAskResultIdx = i;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return { lastTool, lastAssistant, waitingQuestion: lastAskIdx >= 0 && lastAskResultIdx < lastAskIdx };
|
|
314
|
+
}
|
|
315
|
+
/** read 的附加说明行:query 命中位置 + chat 视图裁剪说明(纯函数)。 */
|
|
316
|
+
export function readExtraLines(L, query, sel, rawTotal, view) {
|
|
317
|
+
const lines = [];
|
|
318
|
+
const q = query.trim();
|
|
319
|
+
if (q && sel.hitIndices.length > 0) {
|
|
320
|
+
const shown = sel.hitIndices.slice(0, 20).map((i) => i + 1);
|
|
321
|
+
const idx = sel.hitIndices.length > 20 ? `${shown.join("、")}… (+${sel.hitIndices.length - 20})` : shown.join("、");
|
|
322
|
+
lines.push(pick(L, `在对话内搜「${q}」:命中 ${sel.hitIndices.length} 条(选区第 ${idx} 条),上下文共 ${sel.selected.length} 条。`, `Searched for "${q}": ${sel.hitIndices.length} hit(s) (#${idx} in this selection), ${sel.selected.length} with context.`, "convread.read.query.head", { q, hits: sel.hitIndices.length, indices: idx, shown: sel.selected.length }));
|
|
323
|
+
}
|
|
324
|
+
if (view === "chat" && rawTotal > sel.totalInView) {
|
|
325
|
+
lines.push(pick(L, `(chat 视图只含 user/assistant 共 ${sel.totalInView} 条;另有 ${rawTotal - sel.totalInView} 条工具调用/结果,用 view="full" 看。)`, `(chat view: only user/assistant, ${sel.totalInView} messages; ${rawTotal - sel.totalInView} more tool messages with view="full".)`, "convread.read.chat.note", { chat: sel.totalInView, tools: rawTotal - sel.totalInView }));
|
|
326
|
+
}
|
|
327
|
+
return lines.length > 0 ? `\n${lines.join("\n")}` : "";
|
|
328
|
+
}
|
|
329
|
+
/** 认领行格式化(files/status 共用,纯函数;head 文案各调用方自己 pick)。
|
|
330
|
+
* style: bullets = 独立成行(files 块用);inline = 行内逗分(status 一行用)。 */
|
|
331
|
+
export function formatClaimLines(claims, maxItems = 5, style = "bullets") {
|
|
332
|
+
const list = Array.isArray(claims) ? claims : [];
|
|
333
|
+
const shown = list.slice(0, Math.max(1, Math.floor(maxItems)));
|
|
334
|
+
const dash = style === "bullets" ? "- " : "";
|
|
335
|
+
const lines = shown.map((c) => `${dash}${c.path} · 「${c.ownerTitle}」${c.note ? ` · ${c.note}` : ""}`);
|
|
336
|
+
if (list.length > shown.length)
|
|
337
|
+
lines.push(`… (+${list.length - shown.length})`);
|
|
338
|
+
return lines;
|
|
339
|
+
}
|
|
194
340
|
export function filterRunning(list, query) {
|
|
195
341
|
const q = query.trim().toLowerCase();
|
|
196
342
|
if (!q)
|
|
@@ -207,25 +353,38 @@ export function filterHistory(list, query) {
|
|
|
207
353
|
export function shortPath(p, keep = 48) {
|
|
208
354
|
return p.length <= keep ? p : `…${p.slice(p.length - keep)}`;
|
|
209
355
|
}
|
|
210
|
-
|
|
356
|
+
/** action=list 的默认条数:历史默认 15(以前无上限,几百条一次吐完);
|
|
357
|
+
* 运行中默认不限但给上限(并发对话再多也不会撑爆 details 的 64KB)。 */
|
|
358
|
+
export const HISTORY_LIST_DEFAULT = 15;
|
|
359
|
+
export const RUNNING_LIST_CAP = 50;
|
|
360
|
+
/** action=read 单条消息默认截断(约 600 字符,超出标 “… +N chars”;
|
|
361
|
+
* 以前默认 2000,一条 bash 输出就能吃掉整个 maxChars 预算)。 */
|
|
362
|
+
export const READ_PER_MSG_CAP = 600;
|
|
363
|
+
export function makeConversationReadTool(host, lang, extras) {
|
|
211
364
|
const getLang = lang ?? (() => "en");
|
|
212
365
|
const text = (t, details = {}) => ({
|
|
213
366
|
content: [{ type: "text", text: t }],
|
|
214
367
|
details,
|
|
215
368
|
});
|
|
216
369
|
return defineTool({
|
|
217
|
-
name:
|
|
370
|
+
name: CONVERSATION_READ_TOOL_NAME,
|
|
218
371
|
label: "Read another conversation",
|
|
219
|
-
description: bilingual('Read ANOTHER conversation
|
|
372
|
+
description: bilingual('Read ANOTHER conversation: a running conversation of this client (including subagents — use its conversation id like "c3", see action=list) or a persisted history session transcript (use its file path). ' +
|
|
220
373
|
'Use it when the user references another chat (a quoted conversation, a pasted conversation id, or "see the other chat about X"). ' +
|
|
221
|
-
"action=list shows running conversations
|
|
374
|
+
"action=list shows running conversations plus history sessions (kind=running|history|all, query filter, limit; history defaults to 15). " +
|
|
375
|
+
'action=read returns one transcript: view=chat (default: only user/assistant text) or full (incl. tool calls/results); last=N takes the latest N messages instead of guessing offset; query searches within the conversation (hits ±1 context); each message is capped (~600 chars, marked "… +N chars"). ' +
|
|
376
|
+
"action=files lists files the conversation created/modified (with counts). " +
|
|
377
|
+
"action=status gives a short summary: last tool call, last assistant text, recently touched files, whether it is waiting for a question answer. " +
|
|
222
378
|
"Only sessions from the session list can be read (arbitrary file paths are refused). " +
|
|
223
|
-
"Live runs in OTHER browser tabs are not readable here — they appear in history once persisted.", "
|
|
379
|
+
"Live runs in OTHER browser tabs are not readable here — they appear in history once persisted.", "读取**另一个对话**:本客户端的运行中对话(含子代理——用 action=list 查 conversation id,如 c3)或已落盘的历史会话转录(用它的文件 path)。" +
|
|
224
380
|
"用户引用了别的对话(引用 chip、粘过来的对话 id、“看看之前那个关于 X 的对话”)时用它。" +
|
|
225
|
-
"action=list
|
|
381
|
+
"action=list 列运行中对话与历史会话(kind=running|history|all,query 过滤,limit;历史默认 15 条)。" +
|
|
382
|
+
"action=read 取某一份转录:view=chat(默认:只给 user/assistant 文本)或 full(含工具调用与结果);last=N 取最新 N 条(不用猜 offset);query 在对话内搜索(命中 ±1 条上下文);每条消息独立截断(约 600 字符,超出标 “… +N chars”)。" +
|
|
383
|
+
"action=files 列该对话创建/修改过的文件(含次数)。" +
|
|
384
|
+
"action=status 给一两行摘要:最后一次工具调用、最后一句 assistant、最近动过的文件、是否在等用户回答问卷。" +
|
|
226
385
|
"只能读会话列表里的转录(任意文件路径会被拒绝)。" +
|
|
227
386
|
"其他浏览器标签页里的实时运行在这里读不到——它们落盘后会出现在历史里。"),
|
|
228
|
-
promptSnippet: "read another conversation
|
|
387
|
+
promptSnippet: "read another conversation: list/find chats, read transcript (chat view / tail / search), touched files, status summary",
|
|
229
388
|
parameters: Type.Object({
|
|
230
389
|
action: Type.Optional(Type.String({
|
|
231
390
|
description: 'list = show running conversations + history sessions; read = fetch one transcript. Default "list".',
|
|
@@ -233,11 +392,22 @@ export function makeConversationReadTool(host, lang) {
|
|
|
233
392
|
scope: Type.Optional(Type.String({
|
|
234
393
|
description: 'list only: "current" = this project, "all" = every project. Default "all".',
|
|
235
394
|
})),
|
|
236
|
-
|
|
395
|
+
kind: Type.Optional(Type.String({
|
|
396
|
+
description: 'list only: "running" | "history" | "all" (which sections to show). Default "all".',
|
|
397
|
+
})),
|
|
398
|
+
query: Type.Optional(Type.String({
|
|
399
|
+
description: "list: case-insensitive filter over id/title/path/first message. read/files: search within the transcript (read returns hits ±1 context).",
|
|
400
|
+
})),
|
|
401
|
+
view: Type.Optional(Type.String({
|
|
402
|
+
description: 'read only: "chat" = only user/assistant text (default, cheap); "full" = incl. tool calls/results.',
|
|
403
|
+
})),
|
|
404
|
+
last: Type.Optional(Type.Number({ description: "read only: take the latest N messages (1-200) instead of guessing offset." })),
|
|
237
405
|
id: Type.Optional(Type.String({ description: 'read only: running conversation id (e.g. "c3"), from action=list.' })),
|
|
238
406
|
path: Type.Optional(Type.String({ description: "read only: history session file path, from action=list." })),
|
|
239
407
|
offset: Type.Optional(Type.Number({ description: "read only: first message index (0-based). Default 0." })),
|
|
240
|
-
limit: Type.Optional(Type.Number({
|
|
408
|
+
limit: Type.Optional(Type.Number({
|
|
409
|
+
description: "list: max entries per section (history defaults to 15, running uncapped up to 50). read/files: max messages/entries (1-200, default 50).",
|
|
410
|
+
})),
|
|
241
411
|
maxChars: Type.Optional(Type.Number({ description: "read only: max characters (1000-60000). Default 20000." })),
|
|
242
412
|
}),
|
|
243
413
|
execute: async (_id, p, _signal, _onUpdate, ctx) => {
|
|
@@ -248,50 +418,105 @@ export function makeConversationReadTool(host, lang) {
|
|
|
248
418
|
return text(pick(getLang(), `scope 非法:${p.scope}(只能是 current 或 all)。`, `Invalid scope: ${p.scope} (must be "current" or "all").`, "convread.list.bad.scope", { "p.scope": p.scope }));
|
|
249
419
|
}
|
|
250
420
|
const scope = scopeRaw;
|
|
421
|
+
const kindRaw = (p.kind ?? "all").trim().toLowerCase();
|
|
422
|
+
if (kindRaw !== "running" && kindRaw !== "history" && kindRaw !== "all") {
|
|
423
|
+
return text(pick(getLang(), `kind 非法:${p.kind}(只能是 running、history 或 all)。`, `Invalid kind: ${p.kind} (must be "running", "history" or "all").`, "convread.list.bad.kind", { "p.kind": p.kind }));
|
|
424
|
+
}
|
|
425
|
+
const kind = kindRaw;
|
|
251
426
|
const query = typeof p.query === "string" ? p.query : "";
|
|
252
|
-
const
|
|
253
|
-
|
|
427
|
+
const cap = typeof p.limit === "number" && Number.isFinite(p.limit)
|
|
428
|
+
? Math.min(200, Math.max(1, Math.floor(p.limit)))
|
|
429
|
+
: undefined;
|
|
430
|
+
// kind 过滤掉的一整节直接不取不展(不为了它多一次 I/O)。
|
|
431
|
+
const running = kind === "history" ? [] : filterRunning(host.listRunningConversations(), query);
|
|
432
|
+
const history = kind === "running" ? [] : filterHistory(await host.listHistorySessions(scope, ctx.cwd), query);
|
|
433
|
+
const shownRunning = running.slice(0, cap ?? RUNNING_LIST_CAP);
|
|
434
|
+
const shownHistory = history.slice(0, cap ?? HISTORY_LIST_DEFAULT);
|
|
254
435
|
const L = getLang();
|
|
255
|
-
const runLines =
|
|
256
|
-
const histLines =
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
436
|
+
const runLines = shownRunning.map((c) => `- ${c.id} · ${c.title} · ${c.isSubagent ? (L === "zh" ? "子代理" : "subagent") : L === "zh" ? "对话" : "chat"}${c.isStreaming ? (L === "zh" ? "(进行中)" : " (streaming)") : ""} · ${c.messageCount} msgs · ${c.cwd}`);
|
|
437
|
+
const histLines = shownHistory.map((s) => `- ${shortPath(s.path)} · ${s.name || s.firstMessage || (L === "zh" ? "(空对话)" : "(empty)")}${s.messageCount ? ` · ${s.messageCount} msgs` : ""}`);
|
|
438
|
+
// 超出 cap 时给计数 + 收窄指引(静默丢弃是大忌:调用方会以为这就是全量)。
|
|
439
|
+
if (running.length > shownRunning.length) {
|
|
440
|
+
runLines.push(pick(L, `… 还有 ${running.length - shownRunning.length} 条运行中(用 query 或 limit 收窄)。`, `… ${running.length - shownRunning.length} more running (narrow with query or limit).`, "convread.list.running.more", { n: running.length - shownRunning.length }));
|
|
441
|
+
}
|
|
442
|
+
if (history.length > shownHistory.length) {
|
|
443
|
+
histLines.push(pick(L, `… 还有 ${history.length - shownHistory.length} 条历史(用 query 或 limit 收窄;默认只给 ${HISTORY_LIST_DEFAULT} 条)。`, `… ${history.length - shownHistory.length} more history (narrow with query or limit; default shows ${HISTORY_LIST_DEFAULT}).`, "convread.list.history.more", { n: history.length - shownHistory.length }));
|
|
444
|
+
}
|
|
445
|
+
const head = kind === "running"
|
|
446
|
+
? pick(L, `运行中对话(${running.length}):`, `Running conversations (${running.length}):`, "convread.list.head.running", { n: running.length })
|
|
447
|
+
: kind === "history"
|
|
448
|
+
? pick(L, `历史会话(${history.length},scope=${scope}):`, `History sessions (${history.length}, scope=${scope}):`, "convread.list.head.history", { n: history.length })
|
|
449
|
+
: pick(L, `运行中对话(${running.length})+ 历史会话(${history.length},scope=${scope}):`, `Running conversations (${running.length}) + history sessions (${history.length}, scope=${scope}):`, "convread.list.head.all", { running: running.length, history: history.length });
|
|
260
450
|
const runHead = L === "zh" ? "【运行中】" : "[running]";
|
|
261
451
|
const histHead = L === "zh" ? "【历史】" : "[history]";
|
|
262
452
|
const empty = L === "zh" ? "(无)" : "(none)";
|
|
263
453
|
const tail = L === "zh"
|
|
264
|
-
? `读某一份:conversation_read(action="read", id="c…") 或 conversation_read(action="read", path="…"),长转录用 offset/limit
|
|
265
|
-
: `To read one: conversation_read(action="read", id="c…") or conversation_read(action="read", path="…"); page long transcripts with offset/limit.`;
|
|
266
|
-
|
|
454
|
+
? `读某一份:conversation_read(action="read", id="c…") 或 conversation_read(action="read", path="…"),长转录用 offset/limit 翻页;只看文件用 action="files",先看摘要再决定读不读用 action="status"。`
|
|
455
|
+
: `To read one: conversation_read(action="read", id="c…") or conversation_read(action="read", path="…"); page long transcripts with offset/limit; action="files" lists touched files, action="status" summarizes before you decide to read.`;
|
|
456
|
+
const sections = kind === "history"
|
|
457
|
+
? `${head}\n${histHead}\n${histLines.join("\n") || empty}`
|
|
458
|
+
: kind === "running"
|
|
459
|
+
? `${head}\n${runHead}\n${runLines.join("\n") || empty}`
|
|
460
|
+
: `${head}\n${runHead}\n${runLines.join("\n") || empty}\n${histHead}\n${histLines.join("\n") || empty}`;
|
|
461
|
+
// details 同样有界:只带展示出的条目(历史的 firstMessage 再掐到 160
|
|
462
|
+
// 字符),以前整量 history 进 details 会撑爆 64KB 被整段丢弃。
|
|
463
|
+
return text(`${sections}\n${tail}`, {
|
|
464
|
+
scope,
|
|
465
|
+
kind,
|
|
466
|
+
runningTotal: running.length,
|
|
467
|
+
historyTotal: history.length,
|
|
468
|
+
running: shownRunning,
|
|
469
|
+
history: shownHistory.map((s) => ({
|
|
470
|
+
...s,
|
|
471
|
+
firstMessage: typeof s.firstMessage === "string" && s.firstMessage.length > 160
|
|
472
|
+
? `${s.firstMessage.slice(0, 160)}…`
|
|
473
|
+
: s.firstMessage,
|
|
474
|
+
})),
|
|
475
|
+
truncated: running.length > shownRunning.length || history.length > shownHistory.length,
|
|
476
|
+
});
|
|
267
477
|
}
|
|
268
478
|
if (action === "read") {
|
|
479
|
+
const viewRaw = (p.view ?? "chat").trim().toLowerCase();
|
|
480
|
+
if (viewRaw !== "chat" && viewRaw !== "full") {
|
|
481
|
+
return text(pick(getLang(), `view 非法:${p.view}(只能是 chat 或 full)。`, `Invalid view: ${p.view} (must be "chat" or "full").`, "convread.read.bad.view", { "p.view": p.view }));
|
|
482
|
+
}
|
|
483
|
+
const view = viewRaw;
|
|
269
484
|
const id = typeof p.id === "string" && p.id.trim() ? p.id.trim() : undefined;
|
|
270
485
|
const path = typeof p.path === "string" && p.path.trim() ? p.path.trim() : undefined;
|
|
271
486
|
if ((id && path) || (!id && !path)) {
|
|
272
487
|
return text(pick(getLang(), `action=read 需要且只需要 id 或 path 其中之一(id 读运行中对话,path 读历史转录)。`, `action=read needs exactly one of id or path (id = running conversation, path = history transcript).`, "convread.read.bad.args"));
|
|
273
488
|
}
|
|
274
|
-
const
|
|
489
|
+
const query = typeof p.query === "string" ? p.query : "";
|
|
490
|
+
const opts = { offset: p.offset, limit: p.limit, maxChars: p.maxChars, perMsgCap: READ_PER_MSG_CAP };
|
|
275
491
|
if (id) {
|
|
276
492
|
const found = host.readRunningConversation(id);
|
|
277
493
|
if (!found) {
|
|
278
494
|
return text(pick(getLang(), `未找到运行中对话 ${id}(可能已关闭;用 action=list 看当前列表,落盘的可按 path 读历史)。`, `Running conversation ${id} not found (may be closed; use action=list for the current list, or read persisted ones by path).`, "convread.read.id.not.found", { id }));
|
|
279
495
|
}
|
|
280
|
-
const
|
|
496
|
+
const rawTotal = found.messages.length;
|
|
497
|
+
const sel = selectReadMessages(found.messages, { view, last: p.last, query });
|
|
281
498
|
const L = getLang();
|
|
499
|
+
if (query.trim() && sel.hitIndices.length === 0) {
|
|
500
|
+
return text(pick(L, `在对话内搜「${query.trim()}」:没有命中。换个词,或用 view="full" 把工具输出也纳入搜索。`, `No hits for "${query.trim()}". Try another term, or use view="full" to include tool outputs.`, "convread.read.query.empty", { q: query.trim() }), { id, title: found.title, view, query: query.trim(), hits: 0 });
|
|
501
|
+
}
|
|
502
|
+
const f = formatTranscript(sel.selected, opts);
|
|
282
503
|
const head = L === "zh"
|
|
283
504
|
? `对话「${found.title}」(id=${id}${found.isSubagent ? ",子代理" : ""},${found.cwd},共 ${f.total} 条,${f.from + 1}-${f.to}):`
|
|
284
505
|
: `Conversation "${found.title}" (id=${id}${found.isSubagent ? ", subagent" : ""}, ${found.cwd}, ${f.total} messages, showing ${f.from + 1}-${f.to}):`;
|
|
506
|
+
const notes = readExtraLines(L, query, sel, rawTotal, view);
|
|
285
507
|
const more = f.truncated && f.total > 0
|
|
286
508
|
? L === "zh"
|
|
287
509
|
? `\n…还有后文(offset=${f.to} 再取)。`
|
|
288
510
|
: `\n…more below (re-call with offset=${f.to}).`
|
|
289
511
|
: "";
|
|
290
512
|
const emptyNote = f.total === 0 ? (L === "zh" ? "(该对话暂无消息)" : "(no messages yet)") : "";
|
|
291
|
-
return text(`${head}\n${f.text || emptyNote}${more}`, {
|
|
513
|
+
return text(`${head}${notes}\n${f.text || emptyNote}${more}`, {
|
|
292
514
|
id,
|
|
293
515
|
title: found.title,
|
|
516
|
+
view,
|
|
517
|
+
...(query.trim() ? { query: query.trim(), hitIndices: sel.hitIndices.slice(0, 500) } : {}),
|
|
294
518
|
total: f.total,
|
|
519
|
+
rawTotal,
|
|
295
520
|
from: f.from,
|
|
296
521
|
to: f.to,
|
|
297
522
|
});
|
|
@@ -300,26 +525,144 @@ export function makeConversationReadTool(host, lang) {
|
|
|
300
525
|
if (!found) {
|
|
301
526
|
return text(pick(getLang(), `读不到历史会话 ${path}(不在会话列表里:只能读 action=list 列出的转录路径)。`, `Cannot read history session ${path} (not in the session list: only transcripts from action=list can be read).`, "convread.read.path.not.found", { path }));
|
|
302
527
|
}
|
|
303
|
-
const
|
|
528
|
+
const rawTotal = found.messages.length;
|
|
529
|
+
const sel = selectReadMessages(found.messages, { view, last: p.last, query });
|
|
304
530
|
const L = getLang();
|
|
531
|
+
if (query.trim() && sel.hitIndices.length === 0) {
|
|
532
|
+
return text(pick(L, `在对话内搜「${query.trim()}」:没有命中。换个词,或用 view="full" 把工具输出也纳入搜索。`, `No hits for "${query.trim()}". Try another term, or use view="full" to include tool outputs.`, "convread.read.query.empty", { q: query.trim() }), { path: found.sessionPath, title: found.title, view, query: query.trim(), hits: 0 });
|
|
533
|
+
}
|
|
534
|
+
const f = formatTranscript(sel.selected, opts);
|
|
305
535
|
const head = L === "zh"
|
|
306
536
|
? `历史会话「${found.title || found.sessionPath}」(${found.cwd},共 ${f.total} 条,${f.from + 1}-${f.to}):`
|
|
307
537
|
: `History session "${found.title || found.sessionPath}" (${found.cwd}, ${f.total} messages, showing ${f.from + 1}-${f.to}):`;
|
|
538
|
+
const notes = readExtraLines(L, query, sel, rawTotal, view);
|
|
308
539
|
const more = f.truncated && f.total > 0
|
|
309
540
|
? L === "zh"
|
|
310
541
|
? `\n…还有后文(offset=${f.to} 再取)。`
|
|
311
542
|
: `\n…more below (re-call with offset=${f.to}).`
|
|
312
543
|
: "";
|
|
313
544
|
const emptyNote = f.total === 0 ? (L === "zh" ? "(该会话暂无消息)" : "(no messages yet)") : "";
|
|
314
|
-
return text(`${head}\n${f.text || emptyNote}${more}`, {
|
|
545
|
+
return text(`${head}${notes}\n${f.text || emptyNote}${more}`, {
|
|
315
546
|
path: found.sessionPath,
|
|
316
547
|
title: found.title,
|
|
548
|
+
view,
|
|
549
|
+
...(query.trim() ? { query: query.trim(), hitIndices: sel.hitIndices.slice(0, 500) } : {}),
|
|
317
550
|
total: f.total,
|
|
551
|
+
rawTotal,
|
|
318
552
|
from: f.from,
|
|
319
553
|
to: f.to,
|
|
320
554
|
});
|
|
321
555
|
}
|
|
322
|
-
|
|
556
|
+
if (action === "files" || action === "status") {
|
|
557
|
+
const id = typeof p.id === "string" && p.id.trim() ? p.id.trim() : undefined;
|
|
558
|
+
const path = typeof p.path === "string" && p.path.trim() ? p.path.trim() : undefined;
|
|
559
|
+
if ((id && path) || (!id && !path)) {
|
|
560
|
+
return text(pick(getLang(), `action=${action} 需要且只需要 id 或 path 其中之一(id 读运行中对话,path 读历史转录)。`, `action=${action} needs exactly one of id or path (id = running conversation, path = history transcript).`, action === "files" ? "convread.files.bad.args" : "convread.status.bad.args"));
|
|
561
|
+
}
|
|
562
|
+
// 取数(错误复用 read 的 not found 文案:同一个“找不到”语义)。
|
|
563
|
+
let title = "";
|
|
564
|
+
let cwd = "";
|
|
565
|
+
let messages = [];
|
|
566
|
+
if (id) {
|
|
567
|
+
const found = host.readRunningConversation(id);
|
|
568
|
+
if (!found) {
|
|
569
|
+
return text(pick(getLang(), `未找到运行中对话 ${id}(可能已关闭;用 action=list 看当前列表,落盘的可按 path 读历史)。`, `Running conversation ${id} not found (may be closed; use action=list for the current list, or read persisted ones by path).`, "convread.read.id.not.found", { id }));
|
|
570
|
+
}
|
|
571
|
+
title = found.title;
|
|
572
|
+
cwd = found.cwd;
|
|
573
|
+
messages = found.messages;
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
const found = await host.readHistorySession(path);
|
|
577
|
+
if (!found) {
|
|
578
|
+
return text(pick(getLang(), `读不到历史会话 ${path}(不在会话列表里:只能读 action=list 列出的转录路径)。`, `Cannot read history session ${path} (not in the session list: only transcripts from action=list can be read).`, "convread.read.path.not.found", { path }));
|
|
579
|
+
}
|
|
580
|
+
title = found.title || found.sessionPath;
|
|
581
|
+
cwd = found.cwd;
|
|
582
|
+
messages = found.messages;
|
|
583
|
+
}
|
|
584
|
+
const L = getLang();
|
|
585
|
+
const where = id ? { id } : { path };
|
|
586
|
+
if (action === "files") {
|
|
587
|
+
const fq = (typeof p.query === "string" ? p.query : "").trim().toLowerCase();
|
|
588
|
+
// sidecar(压缩前的触碰)∪ 实时转录:union 语义,见 unionTouchLists。
|
|
589
|
+
const all = unionTouchLists(host.readTouchSidecar?.(id, path) ?? [], extractTouches(messages));
|
|
590
|
+
const filtered = fq ? all.filter((t) => t.path.toLowerCase().includes(fq)) : all;
|
|
591
|
+
const listedClaims = (() => {
|
|
592
|
+
try {
|
|
593
|
+
const rows = extras?.listClaims?.(cwd) ?? [];
|
|
594
|
+
return fq ? rows.filter((c) => c.path.toLowerCase().includes(fq)) : rows;
|
|
595
|
+
}
|
|
596
|
+
catch {
|
|
597
|
+
return [];
|
|
598
|
+
}
|
|
599
|
+
})();
|
|
600
|
+
const cap = typeof p.limit === "number" && Number.isFinite(p.limit)
|
|
601
|
+
? Math.min(200, Math.max(1, Math.floor(p.limit)))
|
|
602
|
+
: 50;
|
|
603
|
+
const shown = filtered.slice(0, cap);
|
|
604
|
+
if (filtered.length === 0 && listedClaims.length === 0) {
|
|
605
|
+
return text(pick(L, `对话「${title}」还没有可识别的文件写入(只读围观/刚开场/压缩丢了旧记录都有可能)。`, `Conversation "${title}" has no recognizable file writes yet (read-only so far, just started, or pre-compaction touches lost).`, "convread.files.empty", { title }), { ...where, title, total: 0, files: [] });
|
|
606
|
+
}
|
|
607
|
+
const sections = [];
|
|
608
|
+
if (filtered.length > 0) {
|
|
609
|
+
const lines = shown.map((t) => `- ${formatTouchEntry(t)}`);
|
|
610
|
+
if (filtered.length > shown.length) {
|
|
611
|
+
lines.push(pick(L, `… 还有 ${filtered.length - shown.length} 个(用 query 或 limit 收窄)。`, `… ${filtered.length - shown.length} more (narrow with query or limit).`, "convread.files.more", { n: filtered.length - shown.length }));
|
|
612
|
+
}
|
|
613
|
+
sections.push(`${pick(L, `对话「${title}」创建/修改过的文件(${filtered.length} 个,只算写不算读):`, `Files created/modified by "${title}" (${filtered.length}, writes only):`, "convread.files.head", { title, total: filtered.length })}\n${lines.join("\n")}`);
|
|
614
|
+
}
|
|
615
|
+
if (listedClaims.length > 0) {
|
|
616
|
+
sections.push(`${pick(L, `本项目认领(${listedClaims.length} 条,绕行参考,先到先得):`, `Claims in this project (${listedClaims.length}, steer clear, first-wins):`, "convread.files.claims", { total: listedClaims.length })}\n${formatClaimLines(listedClaims).join("\n")}`);
|
|
617
|
+
}
|
|
618
|
+
return text(sections.join("\n"), {
|
|
619
|
+
...where,
|
|
620
|
+
title,
|
|
621
|
+
total: filtered.length,
|
|
622
|
+
files: shown,
|
|
623
|
+
claims: listedClaims.map((c) => ({ path: c.path, ownerTitle: c.ownerTitle })),
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
// action === "status":一两行摘要,读全文前的低成本侦察。
|
|
627
|
+
const sum = summarizeConversation(messages);
|
|
628
|
+
const touched = unionTouchLists(host.readTouchSidecar?.(id, path) ?? [], extractTouches(messages)).slice(0, 6);
|
|
629
|
+
let statusClaims = [];
|
|
630
|
+
try {
|
|
631
|
+
statusClaims = extras?.listClaims?.(cwd) ?? [];
|
|
632
|
+
}
|
|
633
|
+
catch {
|
|
634
|
+
statusClaims = [];
|
|
635
|
+
}
|
|
636
|
+
const head = pick(L, `对话「${title}」状态(共 ${messages.length} 条):`, `Status of "${title}" (${messages.length} messages):`, "convread.status.head", { title, total: messages.length });
|
|
637
|
+
const toolLine = sum.lastTool
|
|
638
|
+
? pick(L, `最后工具:${sum.lastTool.name}${sum.lastTool.hint ? `(${sum.lastTool.hint})` : ""}。`, `Last tool: ${sum.lastTool.name}${sum.lastTool.hint ? ` (${sum.lastTool.hint})` : ""}.`, "convread.status.last.tool", { name: sum.lastTool.name, hint: sum.lastTool.hint })
|
|
639
|
+
: pick(L, `还没调用过工具。`, `No tool calls yet.`, "convread.status.no.tool");
|
|
640
|
+
const sayLine = sum.lastAssistant
|
|
641
|
+
? pick(L, `最后一句:${truncateCounted(sum.lastAssistant, 200)}`, `Last assistant message: ${truncateCounted(sum.lastAssistant, 200)}`, "convread.status.last.say", { text: truncateCounted(sum.lastAssistant, 200) })
|
|
642
|
+
: pick(L, `assistant 还没说过话。`, `No assistant message yet.`, "convread.status.no.say");
|
|
643
|
+
const touchLine = touched.length > 0
|
|
644
|
+
? pick(L, `最近动过:${formatTouchesCompact(touched, { maxItems: 5 })}。`, `Recently touched: ${formatTouchesCompact(touched, { maxItems: 5 })}.`, "convread.status.touched", { files: formatTouchesCompact(touched, { maxItems: 5 }) })
|
|
645
|
+
: pick(L, `最近没动过文件。`, `No files touched recently.`, "convread.status.touched.none");
|
|
646
|
+
// 等问卷只能从转录推断(ask_user_question 调了没回);DSH 问卷不走
|
|
647
|
+
// 工具调用,推断不出 —— 文案写“转录显示”,不夸大。
|
|
648
|
+
const waitLine = sum.waitingQuestion
|
|
649
|
+
? `\n${pick(L, `⚠ 转录显示它在等用户回答问卷(先回它,再干别的)。`, `⚠ The transcript shows it waiting for a question answer (answer first).`, "convread.status.waiting")}`
|
|
650
|
+
: "";
|
|
651
|
+
// 认领只在有的时候占一行(没有就不提,省 token)。
|
|
652
|
+
const claimLine = statusClaims.length > 0
|
|
653
|
+
? `\n- ${pick(L, `认领:${formatClaimLines(statusClaims, 3, "inline").join("、")}。`, `Claims: ${formatClaimLines(statusClaims, 3, "inline").join("; ")}.`, "convread.status.claims", { total: statusClaims.length })}`
|
|
654
|
+
: "";
|
|
655
|
+
return text(`${head}\n- ${toolLine}\n- ${sayLine}\n- ${touchLine}${claimLine}${waitLine}`, {
|
|
656
|
+
...where,
|
|
657
|
+
title,
|
|
658
|
+
lastTool: sum.lastTool,
|
|
659
|
+
lastAssistant: sum.lastAssistant ? truncateCounted(sum.lastAssistant, 200) : undefined,
|
|
660
|
+
touched: touched.slice(0, 5),
|
|
661
|
+
claims: statusClaims.map((c) => ({ path: c.path, ownerTitle: c.ownerTitle })),
|
|
662
|
+
waitingQuestion: sum.waitingQuestion,
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
return text(pick(getLang(), `action 非法:${p.action}(只能是 list、read、files 或 status)。`, `Invalid action: ${p.action} (must be "list", "read", "files" or "status").`, "convread.bad.action", { "p.action": p.action }));
|
|
323
666
|
},
|
|
324
667
|
});
|
|
325
668
|
}
|