pi-web-ui 0.67.0 → 0.68.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/bin/pi-web-ui.mjs +1876 -1876
- package/dist/server/agent-service.js +300 -15
- package/dist/server/attachments.js +3 -3
- package/dist/server/client-state.js +4 -0
- package/dist/server/dsh/dsh-agent-service.js +17 -0
- package/dist/server/dsh/runtime/cordis.yml +1 -1
- package/dist/server/dsh/runtime/goal-rpc.mjs +645 -645
- package/dist/server/dsh/runtime/launcher.mjs +164 -164
- package/dist/server/dsh/runtime/override.patch.yml +71 -71
- package/dist/server/dsh/runtime/runtime-root.mjs +86 -86
- package/dist/server/index.js +9 -1
- package/dist/server/plugins.js +63 -0
- package/dist/server/serialize.js +24 -0
- package/dist/server/settings-service.js +16 -0
- package/dist/server/vision-bridge.js +9 -9
- package/extensions/webui.ts +190 -190
- package/package.json +4 -2
- package/plugins/catalog.json +9 -0
- package/web/dist/assets/{TerminalPanel-B-4FU7T5.js → TerminalPanel-IJF_fssI.js} +1 -1
- package/web/dist/assets/index-BmiyyjKp.css +10 -0
- package/web/dist/assets/index-qoTr5KXy.js +332 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-BGIkWTsd.css +0 -10
- package/web/dist/assets/index-CqXS2SQU.js +0 -332
|
@@ -27,7 +27,7 @@ import { GoalService } from "./goal-service.js";
|
|
|
27
27
|
import { MarkerService } from "./marker-service.js";
|
|
28
28
|
import { SlashCommandsService, parseSlash } from "./slash-commands.js";
|
|
29
29
|
import { ModelAdminService } from "./model-admin.js";
|
|
30
|
-
import { FilesService, workspacePath } from "./files-service.js";
|
|
30
|
+
import { FilesService, MACHINE_ROOT, workspacePath } from "./files-service.js";
|
|
31
31
|
import { isExtensionDisabled, isExtensionEnabled, ClientStateStore } from "./client-state.js";
|
|
32
32
|
import { SubagentTemplatesStore } from "./subagent-templates.js";
|
|
33
33
|
import { applyHeadTail, makePersistentTerminalTools, makeTerminalBashTool, stripAnsi, TERMINAL_TOOLS_GUIDANCE, TERMINAL_TOOL_NAMES, } from "./terminals.js";
|
|
@@ -36,7 +36,7 @@ import { makeEditSoftTool, SOFT_EDIT_TOOL_NAME } from "./edit-soft-tool.js";
|
|
|
36
36
|
import { makeSubagentTools, subagentTitle, } from "./subagents.js";
|
|
37
37
|
import { buildAttachmentMessages } from "./attachments.js";
|
|
38
38
|
import { BUILTIN_SOUL, DEFAULT_PROMPT_TEMPLATE, renderPromptTemplate, resolveSectionTexts, } from "./prompt-composer.js";
|
|
39
|
-
import { serializeMessage, serializeStreamingMessage } from "./serialize.js";
|
|
39
|
+
import { serializeMessage, serializeStreamingMessage, stripTransientRetryErrors, } from "./serialize.js";
|
|
40
40
|
import { loadCommands, saveCommandsFile, TerminalManager } from "./terminals.js";
|
|
41
41
|
const SNAPSHOT_INTERVAL_MS = 60;
|
|
42
42
|
/** While assistant deltas are flowing, live rendering is carried by
|
|
@@ -296,6 +296,37 @@ function extractAssistantTextFromContent(content) {
|
|
|
296
296
|
.join("\n");
|
|
297
297
|
}
|
|
298
298
|
export { workspacePath };
|
|
299
|
+
/** 轨迹事件 payload 封顶(可直接广播/持久化,不撑爆 storage.json)。 */
|
|
300
|
+
const RUN_TASK_CAP = 500;
|
|
301
|
+
const RUN_ARGS_CAP = 4000;
|
|
302
|
+
const RUN_RESULT_CAP = 4000;
|
|
303
|
+
function truncRun(s, cap) {
|
|
304
|
+
return s.length <= cap ? s : `${s.slice(0, cap)}\n… [truncated]`;
|
|
305
|
+
}
|
|
306
|
+
/** 从 SDK tool result 里抠可读文本预览(text 块拼接,图片/二进制占位,封顶)。 */
|
|
307
|
+
function previewToolResult(result) {
|
|
308
|
+
try {
|
|
309
|
+
const content = result?.content;
|
|
310
|
+
if (Array.isArray(content)) {
|
|
311
|
+
const parts = [];
|
|
312
|
+
for (const c of content) {
|
|
313
|
+
if (c && typeof c === "object" && c.type === "text") {
|
|
314
|
+
parts.push(String(c.text ?? ""));
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
parts.push("[…]");
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return truncRun(parts.join("\n"), RUN_RESULT_CAP);
|
|
321
|
+
}
|
|
322
|
+
if (typeof result === "string")
|
|
323
|
+
return truncRun(result, RUN_RESULT_CAP);
|
|
324
|
+
return truncRun(JSON.stringify(result ?? null), RUN_RESULT_CAP);
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
return "[unserializable result]";
|
|
328
|
+
}
|
|
329
|
+
}
|
|
299
330
|
/** Hard cap on how long ONE tool call may run before the watchdog aborts the
|
|
300
331
|
* session. The SDK bash tool has NO default timeout, so a command that never
|
|
301
332
|
* finishes (servers, watchers, infinite loops) would otherwise hang the whole
|
|
@@ -494,6 +525,12 @@ export class ClientSession {
|
|
|
494
525
|
/** index.ts 注入(经 AgentService 拷贝到每个新会话):把 SDK 工具执行事件转发给
|
|
495
526
|
* 插件(PluginManager.emitToolEvent)。未设置时不做任何事。 */
|
|
496
527
|
onToolEvent = undefined;
|
|
528
|
+
/** index.ts 注入:把运行轨迹事件转发给插件(PluginManager.emitRunEvent,
|
|
529
|
+
* 轨迹视图插件靠它聚合时间线)。未设置时不做任何事。 */
|
|
530
|
+
onRunEvent = undefined;
|
|
531
|
+
/** index.ts 注入:当前打开对话变了(切历史会话/切 running 对话/新对话)时
|
|
532
|
+
* 通知插件(PluginManager.emitConversationChanged)——轨迹视图靠它重拉。 */
|
|
533
|
+
onConversationChanged = undefined;
|
|
497
534
|
/** index.ts 注入:读取插件当前注册的 AI 工具(attach 时拷贝到每个新会话)。 */
|
|
498
535
|
pluginToolsProvider = undefined;
|
|
499
536
|
/** index.ts 注入:读取插件当前注册的斜杠命令(目录展示 + prompt 拦截执行)。 */
|
|
@@ -726,6 +763,10 @@ export class ClientSession {
|
|
|
726
763
|
/** 子代理最近一次运行的结局:最后一条 assistant 消息的 errorMessage / stopReason。
|
|
727
764
|
* 报错 > 中止 > 正常,三者互斥;无 assistant 消息时返回空。 */
|
|
728
765
|
subagentRunOutcome(conv) {
|
|
766
|
+
// 自动重试等待期结局未定:瞬时 error 不算失败,避免向主对话误报
|
|
767
|
+
// 「子代理运行失败」(耗尽后 auto_retry_end 清旗,真正失败照常通知)。
|
|
768
|
+
if (conv.retryState)
|
|
769
|
+
return {};
|
|
729
770
|
try {
|
|
730
771
|
const msgs = conv.session.agent.state.messages;
|
|
731
772
|
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
@@ -1456,6 +1497,76 @@ export class ClientSession {
|
|
|
1456
1497
|
clearTimeout(t);
|
|
1457
1498
|
conv.toolWatchdogs.clear();
|
|
1458
1499
|
}
|
|
1500
|
+
/** 发一条运行轨迹事件给插件(host.onRunEvent 订阅者,如轨迹视图插件)。
|
|
1501
|
+
* 异常隔离——序列化/插件坏了只记日志,绝不影响主流程。 */
|
|
1502
|
+
emitRun(conv, ev) {
|
|
1503
|
+
if (!this.onRunEvent)
|
|
1504
|
+
return;
|
|
1505
|
+
try {
|
|
1506
|
+
this.onRunEvent({ ...ev, conversationId: conv.id, at: Date.now() });
|
|
1507
|
+
}
|
|
1508
|
+
catch (err) {
|
|
1509
|
+
console.error("[agent-service] onRunEvent failed:", err);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
/** 当前打开对话变了 → 通知插件重拉(切历史会话/切 running 对话/新对话)。
|
|
1513
|
+
* 异常隔离——插件坏了只记日志,绝不影响切换流程。 */
|
|
1514
|
+
notifyConversationChanged() {
|
|
1515
|
+
if (!this.onConversationChanged)
|
|
1516
|
+
return;
|
|
1517
|
+
try {
|
|
1518
|
+
this.onConversationChanged();
|
|
1519
|
+
}
|
|
1520
|
+
catch (err) {
|
|
1521
|
+
console.error("[agent-service] onConversationChanged failed:", err);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
/** 插件用:本客户端最近活跃对话的快照(轨迹视图直接显示打开对话的时间线)。
|
|
1525
|
+
* messages/streamingMessage 为引用稳定的只读缓存对象——调用方只读、不得修改。 */
|
|
1526
|
+
readConversationForPlugins() {
|
|
1527
|
+
try {
|
|
1528
|
+
let target = null;
|
|
1529
|
+
for (const c of this.convs.values()) {
|
|
1530
|
+
if (!target || c.lastActiveAt > target.lastActiveAt)
|
|
1531
|
+
target = c;
|
|
1532
|
+
}
|
|
1533
|
+
if (!target)
|
|
1534
|
+
return null;
|
|
1535
|
+
const state = target.session.agent.state;
|
|
1536
|
+
let stats = {
|
|
1537
|
+
totalMessages: 0,
|
|
1538
|
+
tokens: { input: 0, output: 0, total: 0 },
|
|
1539
|
+
cost: 0,
|
|
1540
|
+
};
|
|
1541
|
+
try {
|
|
1542
|
+
const s = target.session.getSessionStats();
|
|
1543
|
+
stats = { totalMessages: s.totalMessages, tokens: s.tokens, cost: s.cost };
|
|
1544
|
+
}
|
|
1545
|
+
catch {
|
|
1546
|
+
/* stats 尽力而为 */
|
|
1547
|
+
}
|
|
1548
|
+
let streamingMessage = null;
|
|
1549
|
+
try {
|
|
1550
|
+
streamingMessage = state.streamingMessage ? serializeStreamingMessage(state.streamingMessage) : null;
|
|
1551
|
+
}
|
|
1552
|
+
catch {
|
|
1553
|
+
/* 尽力而为 */
|
|
1554
|
+
}
|
|
1555
|
+
return {
|
|
1556
|
+
conversationId: target.id,
|
|
1557
|
+
title: target.title,
|
|
1558
|
+
at: target.lastActiveAt,
|
|
1559
|
+
isStreaming: target.session.isStreaming,
|
|
1560
|
+
messages: this.messagesOf(target),
|
|
1561
|
+
streamingMessage,
|
|
1562
|
+
stats,
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1565
|
+
catch (err) {
|
|
1566
|
+
console.error("[agent-service] readConversationForPlugins failed:", err);
|
|
1567
|
+
return null;
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1459
1570
|
onEvent(conv, event) {
|
|
1460
1571
|
// Any SDK event proves the run is alive — feeds the stall watchdog below.
|
|
1461
1572
|
conv.lastSdkEventAt = Date.now();
|
|
@@ -1485,7 +1596,26 @@ export class ClientSession {
|
|
|
1485
1596
|
}
|
|
1486
1597
|
this.armToolWatchdog(conv, event.toolCallId);
|
|
1487
1598
|
// 插件扩展点:工具开始执行(异常由 emitToolEvent 隔离)。
|
|
1488
|
-
this.onToolEvent?.({
|
|
1599
|
+
this.onToolEvent?.({
|
|
1600
|
+
phase: "start",
|
|
1601
|
+
toolName: event.toolName,
|
|
1602
|
+
conversationId: conv.id,
|
|
1603
|
+
toolCallId: event.toolCallId,
|
|
1604
|
+
});
|
|
1605
|
+
// 轨迹事件:带参数预览(JSON 封顶;超大参数只记截断)。
|
|
1606
|
+
let argsText = "null";
|
|
1607
|
+
try {
|
|
1608
|
+
argsText = truncRun(JSON.stringify(event.args ?? null), RUN_ARGS_CAP);
|
|
1609
|
+
}
|
|
1610
|
+
catch {
|
|
1611
|
+
argsText = "[unserializable args]";
|
|
1612
|
+
}
|
|
1613
|
+
this.emitRun(conv, {
|
|
1614
|
+
type: "tool_start",
|
|
1615
|
+
toolCallId: event.toolCallId,
|
|
1616
|
+
toolName: event.toolName,
|
|
1617
|
+
argsText,
|
|
1618
|
+
});
|
|
1489
1619
|
break;
|
|
1490
1620
|
}
|
|
1491
1621
|
case "tool_execution_end": {
|
|
@@ -1502,6 +1632,16 @@ export class ClientSession {
|
|
|
1502
1632
|
phase: "end",
|
|
1503
1633
|
toolName: event.toolName,
|
|
1504
1634
|
conversationId: conv.id,
|
|
1635
|
+
toolCallId: event.toolCallId,
|
|
1636
|
+
...(durationMs !== undefined ? { durationMs } : {}),
|
|
1637
|
+
isError: event.isError,
|
|
1638
|
+
});
|
|
1639
|
+
// 轨迹事件:带结果预览(封顶)+ 耗时/错误标志。
|
|
1640
|
+
this.emitRun(conv, {
|
|
1641
|
+
type: "tool_end",
|
|
1642
|
+
toolCallId: event.toolCallId,
|
|
1643
|
+
toolName: event.toolName,
|
|
1644
|
+
resultText: previewToolResult(event.result),
|
|
1505
1645
|
...(durationMs !== undefined ? { durationMs } : {}),
|
|
1506
1646
|
isError: event.isError,
|
|
1507
1647
|
});
|
|
@@ -1597,9 +1737,58 @@ export class ClientSession {
|
|
|
1597
1737
|
}
|
|
1598
1738
|
break;
|
|
1599
1739
|
}
|
|
1740
|
+
case "auto_retry_start": {
|
|
1741
|
+
// 大模型 API 瞬时报错,SDK 退避重试:填实重试信息。末尾 error
|
|
1742
|
+
// 消息已被(或即将被)SDK 从 state 摘掉,currentMessages() 凭此旗
|
|
1743
|
+
// 过滤,快照只显示温和的重试条。落盘由底部检查点立即 flush。
|
|
1744
|
+
conv.retryState = {
|
|
1745
|
+
attempt: event.attempt,
|
|
1746
|
+
maxAttempts: event.maxAttempts,
|
|
1747
|
+
delayMs: event.delayMs,
|
|
1748
|
+
errorMessage: event.errorMessage,
|
|
1749
|
+
};
|
|
1750
|
+
break;
|
|
1751
|
+
}
|
|
1752
|
+
case "auto_retry_end": {
|
|
1753
|
+
// 重试结束:成功 → 新内容照常显示;耗尽 → error 消息留驻,
|
|
1754
|
+
// 快照永久标红。落盘由底部检查点立即 flush。
|
|
1755
|
+
conv.retryState = null;
|
|
1756
|
+
break;
|
|
1757
|
+
}
|
|
1600
1758
|
// A run finished or a new entry was persisted — keep the session list fresh
|
|
1601
1759
|
// (new chat + first message, completed turns, compaction, etc.).
|
|
1602
1760
|
case "agent_end": {
|
|
1761
|
+
// 可重试错误:SDK 随后发 auto_retry_start 并把末尾 error 消息从
|
|
1762
|
+
// state 摘掉。这里先立占位,让本次立即 flush 的快照就不含瞬时红错
|
|
1763
|
+
// ——否则快照先画红、摘掉后又消失,即「红色报错一闪而过」。
|
|
1764
|
+
if (event.willRetry) {
|
|
1765
|
+
let errorMessage = "";
|
|
1766
|
+
for (let i = event.messages.length - 1; i >= 0; i--) {
|
|
1767
|
+
const m = event.messages[i];
|
|
1768
|
+
if (m.role === "assistant" && typeof m.errorMessage === "string") {
|
|
1769
|
+
errorMessage = m.errorMessage;
|
|
1770
|
+
break;
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
conv.retryState = { attempt: 0, maxAttempts: 0, delayMs: 0, errorMessage };
|
|
1774
|
+
}
|
|
1775
|
+
else {
|
|
1776
|
+
// 本轮结束且无后续重试:任何残留占位都是过期的(会话替换、
|
|
1777
|
+
// 结束信号丢失等),清掉,否则横幅会卡住不消失。
|
|
1778
|
+
conv.retryState = null;
|
|
1779
|
+
}
|
|
1780
|
+
// 轨迹事件:本轮结束(放最前——aborted 中断路径也会 break,
|
|
1781
|
+
// 轨迹里必须留下「已停止」而不是凭空消失)。
|
|
1782
|
+
try {
|
|
1783
|
+
const lastAssistant = [...event.messages].reverse().find((m) => {
|
|
1784
|
+
const a = m;
|
|
1785
|
+
return a.role === "assistant" && typeof a.stopReason === "string";
|
|
1786
|
+
});
|
|
1787
|
+
this.emitRun(conv, lastAssistant?.stopReason ? { type: "run_end", stopReason: lastAssistant.stopReason } : { type: "run_end" });
|
|
1788
|
+
}
|
|
1789
|
+
catch {
|
|
1790
|
+
/* 轨迹尽力而为 */
|
|
1791
|
+
}
|
|
1603
1792
|
this.scheduleSessionsRefresh();
|
|
1604
1793
|
this.refreshConversationTitle(conv);
|
|
1605
1794
|
// 内联标记不在此兜底扫最后一条 assistant:每条气泡结束已走 message_end
|
|
@@ -1659,16 +1848,47 @@ export class ClientSession {
|
|
|
1659
1848
|
break;
|
|
1660
1849
|
}
|
|
1661
1850
|
case "message_end": {
|
|
1851
|
+
// 轨迹事件:一条消息定稿(user/assistant 都收;custom display:false
|
|
1852
|
+
// 的 serializeMessage 返回 null 时跳过)。
|
|
1853
|
+
try {
|
|
1854
|
+
const ui = serializeMessage(event.message, 0);
|
|
1855
|
+
if (ui)
|
|
1856
|
+
this.emitRun(conv, { type: "message", message: ui });
|
|
1857
|
+
}
|
|
1858
|
+
catch {
|
|
1859
|
+
/* 轨迹尽力而为 */
|
|
1860
|
+
}
|
|
1861
|
+
// 每条 assistant 气泡流式结束 → 立即解析其中的内联标记:每个气泡各自
|
|
1662
1862
|
// 每条 assistant 气泡流式结束 → 立即解析其中的内联标记:每个气泡各自
|
|
1663
1863
|
// 生效(不再等整轮 agent_end),同一轮里先前消息的标记也不再丢。
|
|
1664
1864
|
const mm = event.message;
|
|
1665
1865
|
if (mm?.role !== "assistant")
|
|
1666
1866
|
break;
|
|
1867
|
+
// 非 error 的 assistant 定稿 = 重试周期结束(与 SDK 重置
|
|
1868
|
+
// _retryAttempt 的条件一致):即使 auto_retry_end 丢失,横幅也不会卡住。
|
|
1869
|
+
if (mm.stopReason !== "error")
|
|
1870
|
+
conv.retryState = null;
|
|
1667
1871
|
const text = extractAssistantTextFromContent(mm.content);
|
|
1668
1872
|
if (text && text.includes("[["))
|
|
1669
1873
|
void this.markerSvc.handleAssistantText(conv.id, text);
|
|
1670
1874
|
break;
|
|
1671
1875
|
}
|
|
1876
|
+
case "agent_start": {
|
|
1877
|
+
// 轨迹事件:新一轮开始(任务文本由 prompt() 暂存;steer/内部续跑
|
|
1878
|
+
// 无暂存时省略,插件回退为「继续执行」)。
|
|
1879
|
+
const task = conv.pendingTask;
|
|
1880
|
+
conv.pendingTask = undefined;
|
|
1881
|
+
this.emitRun(conv, task ? { type: "run_start", task } : { type: "run_start" });
|
|
1882
|
+
break;
|
|
1883
|
+
}
|
|
1884
|
+
case "turn_start": {
|
|
1885
|
+
this.emitRun(conv, { type: "turn_start" });
|
|
1886
|
+
break;
|
|
1887
|
+
}
|
|
1888
|
+
case "turn_end": {
|
|
1889
|
+
this.emitRun(conv, { type: "turn_end" });
|
|
1890
|
+
break;
|
|
1891
|
+
}
|
|
1672
1892
|
case "message_update": {
|
|
1673
1893
|
// Live assistant-message increment, deliberately OUTSIDE the snapshot
|
|
1674
1894
|
// channel: send() drops snapshots under backpressure (big sessions),
|
|
@@ -1714,7 +1934,11 @@ export class ClientSession {
|
|
|
1714
1934
|
// Snapshot checkpoint policy: deltas carry live rendering during streaming;
|
|
1715
1935
|
// full snapshots are reconciliation checkpoints taken immediately at
|
|
1716
1936
|
// run/tool boundaries and on a slow timer otherwise.
|
|
1717
|
-
if (event.type === "agent_end" ||
|
|
1937
|
+
if (event.type === "agent_end" ||
|
|
1938
|
+
event.type === "tool_execution_end" ||
|
|
1939
|
+
event.type === "compaction_end" ||
|
|
1940
|
+
event.type === "auto_retry_start" ||
|
|
1941
|
+
event.type === "auto_retry_end") {
|
|
1718
1942
|
this.flushSnapshot();
|
|
1719
1943
|
}
|
|
1720
1944
|
else {
|
|
@@ -1751,7 +1975,10 @@ export class ClientSession {
|
|
|
1751
1975
|
}
|
|
1752
1976
|
/** Serialize a persisted message with a STABLE id + cached object reference. */
|
|
1753
1977
|
serializeCached(m) {
|
|
1754
|
-
|
|
1978
|
+
return this.serializeCachedFor(this.conv, m);
|
|
1979
|
+
}
|
|
1980
|
+
/** serializeCached 的按对话版本(插件快照读非活跃对话用;缓存仍按对话隔离)。 */
|
|
1981
|
+
serializeCachedFor(conv, m) {
|
|
1755
1982
|
// toolResult messages are keyed by toolCallId; everything else by
|
|
1756
1983
|
// role+timestamp. A single prompt can emit several same-role messages
|
|
1757
1984
|
// within the SAME millisecond (multiple attachment asides), so the
|
|
@@ -1801,10 +2028,17 @@ export class ClientSession {
|
|
|
1801
2028
|
* Element objects are reference-stable (serializeCached cache), which is
|
|
1802
2029
|
* what lets emitSnapshotNow detect append-only growth via identity walk. */
|
|
1803
2030
|
currentMessages() {
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
2031
|
+
return this.messagesOf(this.conv);
|
|
2032
|
+
}
|
|
2033
|
+
/** currentMessages 的按对话版本(插件快照读非活跃对话用)。 */
|
|
2034
|
+
messagesOf(conv) {
|
|
2035
|
+
let rawMessages = conv.session.agent.state.messages
|
|
2036
|
+
.map((m) => this.serializeCachedFor(conv, m))
|
|
1807
2037
|
.filter((m) => m !== null);
|
|
2038
|
+
// 自动重试等待期:SDK 暂留在 state 末尾的 error 气泡只是中间态(随后被
|
|
2039
|
+
// 摘掉重跑),不进快照——成功则用户永远看不到,耗尽才标红。否则 agent_end
|
|
2040
|
+
// 的立即 flush 会先画红、摘掉后又消失(红色一闪而过)。
|
|
2041
|
+
rawMessages = stripTransientRetryErrors(rawMessages, !!conv.retryState);
|
|
1808
2042
|
// Reuse the previous array when nothing changed: the element objects are
|
|
1809
2043
|
// cached (reference-stable) anyway, and a stable array reference lets the
|
|
1810
2044
|
// frontend memoize derived maps instead of rebuilding them every 60ms.
|
|
@@ -1843,6 +2077,13 @@ export class ClientSession {
|
|
|
1843
2077
|
catch {
|
|
1844
2078
|
// stats are best-effort
|
|
1845
2079
|
}
|
|
2080
|
+
// 流式 error 同样是中间态(定稿走 message_end/agent_end):先藏起
|
|
2081
|
+
// errorMessage,避免红色在 streaming 气泡里闪一下。最终失败会经由
|
|
2082
|
+
// messages 永久标红,不影响告警。
|
|
2083
|
+
let streamingMessage = state.streamingMessage ? serializeStreamingMessage(state.streamingMessage) : null;
|
|
2084
|
+
if (streamingMessage?.stopReason === "error") {
|
|
2085
|
+
streamingMessage = { ...streamingMessage, errorMessage: undefined };
|
|
2086
|
+
}
|
|
1846
2087
|
return {
|
|
1847
2088
|
clientId: this.clientId,
|
|
1848
2089
|
cwd: this.cwd,
|
|
@@ -1850,11 +2091,7 @@ export class ClientSession {
|
|
|
1850
2091
|
sessionFile: this.session.sessionFile,
|
|
1851
2092
|
conversationId: this.activeId,
|
|
1852
2093
|
rev,
|
|
1853
|
-
|
|
1854
|
-
// (the SDK only pushes it into state.messages at message_end). Surfacing
|
|
1855
|
-
// it here is what makes thinking + text stream into the browser at
|
|
1856
|
-
// ~60ms granularity instead of appearing only when the turn finishes.
|
|
1857
|
-
streamingMessage: state.streamingMessage ? serializeStreamingMessage(state.streamingMessage) : null,
|
|
2094
|
+
streamingMessage,
|
|
1858
2095
|
isStreaming: this.session.isStreaming,
|
|
1859
2096
|
model: model
|
|
1860
2097
|
? {
|
|
@@ -1870,6 +2107,7 @@ export class ClientSession {
|
|
|
1870
2107
|
availableThinkingLevels: this.session.getAvailableThinkingLevels(),
|
|
1871
2108
|
queue: { steering: conv.queueSteering, followUp: conv.queueFollowUp },
|
|
1872
2109
|
errorMessage: state.errorMessage,
|
|
2110
|
+
retry: conv.retryState ?? null,
|
|
1873
2111
|
tools: state.tools.map((t) => t.name),
|
|
1874
2112
|
version: ++this.version,
|
|
1875
2113
|
piConfigured: this.isPiConfigured(),
|
|
@@ -2607,6 +2845,9 @@ export class ClientSession {
|
|
|
2607
2845
|
// is refused until admission reopens.
|
|
2608
2846
|
if (this.quiesceBlocked())
|
|
2609
2847
|
return;
|
|
2848
|
+
// 轨迹用:暂存本轮任务文本,下一轮 agent_start 消费(steer/内部续跑
|
|
2849
|
+
// 不经此处,届时 task 缺省,插件回退为「继续执行」)。
|
|
2850
|
+
conv.pendingTask = text.trim() ? truncRun(text.trim(), RUN_TASK_CAP) : undefined;
|
|
2610
2851
|
// Name the conversation from its FIRST prompt immediately, before any
|
|
2611
2852
|
// await: the typed text IS the name. The `conv` reference was captured
|
|
2612
2853
|
// before the try block, so a concurrent switch/new_chat while prompt()
|
|
@@ -3000,6 +3241,8 @@ export class ClientSession {
|
|
|
3000
3241
|
// The new runtime re-discovered skills/templates — refresh the catalog
|
|
3001
3242
|
// so the picker stops showing the previous runtime's list.
|
|
3002
3243
|
void this.pushSlashCommands();
|
|
3244
|
+
// 新对话即当前打开 → 插件重拉(轨迹视图跟随)。
|
|
3245
|
+
this.notifyConversationChanged();
|
|
3003
3246
|
}
|
|
3004
3247
|
catch (err) {
|
|
3005
3248
|
this.emit({
|
|
@@ -3115,6 +3358,8 @@ export class ClientSession {
|
|
|
3115
3358
|
void this.listFiles(undefined);
|
|
3116
3359
|
void this.listCommands();
|
|
3117
3360
|
}
|
|
3361
|
+
// 当前打开对话变了 → 插件重拉(轨迹视图切会话后即刷新,不等轮询)。
|
|
3362
|
+
this.notifyConversationChanged();
|
|
3118
3363
|
this.flushSnapshot();
|
|
3119
3364
|
}
|
|
3120
3365
|
/** Push every running conversation across ALL projects to the client. The
|
|
@@ -3565,6 +3810,8 @@ export class ClientSession {
|
|
|
3565
3810
|
this.pushTerminals();
|
|
3566
3811
|
// The restored conversation has a fresh project-bound resource cache.
|
|
3567
3812
|
void this.pushSlashCommands();
|
|
3813
|
+
// 切历史会话成功 → 插件重拉(轨迹视图立即显示该会话时间线)。
|
|
3814
|
+
this.notifyConversationChanged();
|
|
3568
3815
|
}
|
|
3569
3816
|
catch (err) {
|
|
3570
3817
|
openedTerminals?.killAll();
|
|
@@ -3812,10 +4059,25 @@ export class ClientSession {
|
|
|
3812
4059
|
}
|
|
3813
4060
|
async setCwd(newCwd) {
|
|
3814
4061
|
try {
|
|
3815
|
-
const { resolve } = await import("node:path");
|
|
4062
|
+
const { resolve, sep } = await import("node:path");
|
|
3816
4063
|
this.files.unwatchGit(); // stale repo's watcher must not fire across projects
|
|
3817
4064
|
const fs = await import("node:fs/promises");
|
|
3818
|
-
const
|
|
4065
|
+
const trimmed = newCwd.trim();
|
|
4066
|
+
if (trimmed === MACHINE_ROOT) {
|
|
4067
|
+
// 机器根是虚拟层(盘符列表),不能作工作目录——指引用户选具体目录。
|
|
4068
|
+
this.emit({
|
|
4069
|
+
type: "notice",
|
|
4070
|
+
level: "warning",
|
|
4071
|
+
text: "请选择一个具体目录作为工作目录(此电脑本身不是目录)",
|
|
4072
|
+
textEn: "Pick a concrete directory as the workspace (This PC itself is not a directory)",
|
|
4073
|
+
});
|
|
4074
|
+
return;
|
|
4075
|
+
}
|
|
4076
|
+
// Windows 裸盘符("C:"):resolve 会按该盘当前目录解析,必须显式指到盘根;
|
|
4077
|
+
// 仅 win32 生效——posix 下 "C:" 仍是普通相对路径,避免误伤同名目录。
|
|
4078
|
+
const abs = process.platform === "win32" && /^[A-Za-z]:$/.test(trimmed)
|
|
4079
|
+
? `${trimmed.toUpperCase()}${sep}`
|
|
4080
|
+
: resolve(trimmed);
|
|
3819
4081
|
const st = await fs.stat(abs);
|
|
3820
4082
|
if (!st.isDirectory()) {
|
|
3821
4083
|
throw new Error("路径不是目录");
|
|
@@ -3900,6 +4162,8 @@ export class ClientSession {
|
|
|
3900
4162
|
void this.listFiles(undefined);
|
|
3901
4163
|
// Commands are per-project (.pi/commands.json in the current cwd).
|
|
3902
4164
|
void this.listCommands();
|
|
4165
|
+
// 切项目即换了当前打开对话 → 插件重拉。
|
|
4166
|
+
this.notifyConversationChanged();
|
|
3903
4167
|
}
|
|
3904
4168
|
catch (err) {
|
|
3905
4169
|
this.emit({
|
|
@@ -4080,6 +4344,10 @@ export class AgentService {
|
|
|
4080
4344
|
cwd;
|
|
4081
4345
|
/** index.ts 注入:SDK 工具执行事件的插件转发钩子,attach 时拷贝到每个新会话。 */
|
|
4082
4346
|
onToolEvent = undefined;
|
|
4347
|
+
/** index.ts 注入:运行轨迹事件的插件转发钩子,attach 时拷贝到每个新会话。 */
|
|
4348
|
+
onRunEvent = undefined;
|
|
4349
|
+
/** index.ts 注入:对话切换通知钩子,attach 时拷贝到每个新会话。 */
|
|
4350
|
+
onConversationChanged = undefined;
|
|
4083
4351
|
/** index.ts 注入:读取插件当前注册的 AI 工具(attach 时拷贝到每个新会话)。 */
|
|
4084
4352
|
pluginToolsProvider = undefined;
|
|
4085
4353
|
/** index.ts 注入:读取插件当前注册的斜杠命令(attach 时拷贝到每个新会话)。 */
|
|
@@ -4142,6 +4410,21 @@ export class AgentService {
|
|
|
4142
4410
|
n += cs.pendingMessages();
|
|
4143
4411
|
return n;
|
|
4144
4412
|
}
|
|
4413
|
+
/** 插件用:全客户端最近活跃对话的快照(at 最大者即“当前打开的对话”)。 */
|
|
4414
|
+
readConversationForPlugins() {
|
|
4415
|
+
let best = null;
|
|
4416
|
+
for (const cs of this.clients.values()) {
|
|
4417
|
+
try {
|
|
4418
|
+
const s = cs.readConversationForPlugins();
|
|
4419
|
+
if (s && (!best || s.at > best.at))
|
|
4420
|
+
best = s;
|
|
4421
|
+
}
|
|
4422
|
+
catch {
|
|
4423
|
+
/* 单客户端坏了不影响其他 */
|
|
4424
|
+
}
|
|
4425
|
+
}
|
|
4426
|
+
return best;
|
|
4427
|
+
}
|
|
4145
4428
|
/** index.ts calls this when a browser socket opens/closes. */
|
|
4146
4429
|
noteSocketOpen() {
|
|
4147
4430
|
this.socketCount += 1;
|
|
@@ -4217,6 +4500,8 @@ export class AgentService {
|
|
|
4217
4500
|
// Forward hooks (set once by index.ts) to every session.
|
|
4218
4501
|
cs.onQuit = this.onQuit;
|
|
4219
4502
|
cs.onToolEvent = this.onToolEvent;
|
|
4503
|
+
cs.onRunEvent = this.onRunEvent;
|
|
4504
|
+
cs.onConversationChanged = () => this.onConversationChanged?.();
|
|
4220
4505
|
cs.pluginToolsProvider = this.pluginToolsProvider;
|
|
4221
4506
|
cs.pluginCommandsProvider = this.pluginCommandsProvider;
|
|
4222
4507
|
cs.pluginBgTasksProvider = this.pluginBgTasksProvider;
|
|
@@ -454,9 +454,9 @@ export async function buildAttachmentMessages(ctx, attachments) {
|
|
|
454
454
|
content: [
|
|
455
455
|
{
|
|
456
456
|
type: "text",
|
|
457
|
-
text: `
|
|
458
|
-
<vision-bridge>
|
|
459
|
-
${transcript}
|
|
457
|
+
text: `
|
|
458
|
+
<vision-bridge>
|
|
459
|
+
${transcript}
|
|
460
460
|
</vision-bridge>`,
|
|
461
461
|
},
|
|
462
462
|
...(pathImg
|
|
@@ -211,6 +211,8 @@ export class ClientStateStore {
|
|
|
211
211
|
visionBridgePromptMode: stored?.visionBridgePromptMode === "replace" ? "replace" : "append",
|
|
212
212
|
visionBridgePrompt: stored?.visionBridgePrompt ?? "",
|
|
213
213
|
subagentDefaultModel: stored?.subagentDefaultModel ?? null,
|
|
214
|
+
quickPhrases: stored?.quickPhrases ?? [],
|
|
215
|
+
quickPhrasesEnabled: stored?.quickPhrasesEnabled ?? true,
|
|
214
216
|
reviewPrompt: stored?.reviewPrompt ?? "",
|
|
215
217
|
reviewDisabledSkills: stored?.reviewDisabledSkills ?? [],
|
|
216
218
|
disabledPlugins: stored?.disabledPlugins ?? [],
|
|
@@ -242,6 +244,8 @@ export class ClientStateStore {
|
|
|
242
244
|
reviewPrompt: settings.reviewPrompt ?? cur.reviewPrompt ?? "",
|
|
243
245
|
reviewDisabledSkills: settings.reviewDisabledSkills ?? cur.reviewDisabledSkills ?? [],
|
|
244
246
|
disabledPlugins: settings.disabledPlugins ?? cur.disabledPlugins ?? [],
|
|
247
|
+
quickPhrases: settings.quickPhrases ?? cur.quickPhrases ?? [],
|
|
248
|
+
quickPhrasesEnabled: settings.quickPhrasesEnabled ?? cur.quickPhrasesEnabled ?? true,
|
|
245
249
|
};
|
|
246
250
|
this.save();
|
|
247
251
|
}
|
|
@@ -112,6 +112,8 @@ const DEFAULT_SETTINGS = {
|
|
|
112
112
|
toolsWrap: true,
|
|
113
113
|
disabledPlugins: [],
|
|
114
114
|
reviewPrompt: "",
|
|
115
|
+
quickPhrases: [],
|
|
116
|
+
quickPhrasesEnabled: true,
|
|
115
117
|
};
|
|
116
118
|
// ---------------------------------------------------------------------------
|
|
117
119
|
// DshClientSession — 一个浏览器客户端
|
|
@@ -223,6 +225,8 @@ export class DshClientSession {
|
|
|
223
225
|
toolsWrap: savedSettings.toolsWrap,
|
|
224
226
|
disabledPlugins: savedSettings.disabledPlugins ?? [],
|
|
225
227
|
reviewPrompt: savedSettings.reviewPrompt,
|
|
228
|
+
quickPhrases: savedSettings.quickPhrases ?? [],
|
|
229
|
+
quickPhrasesEnabled: savedSettings.quickPhrasesEnabled ?? true,
|
|
226
230
|
};
|
|
227
231
|
}
|
|
228
232
|
// 第一个 conversation = 新会话(每客户端独立 sessionId,避免多标签页/多
|
|
@@ -2082,6 +2086,8 @@ export class DshClientSession {
|
|
|
2082
2086
|
subagentDefaultTemplates: [],
|
|
2083
2087
|
subagentDefaultModel: null,
|
|
2084
2088
|
subagentModels: [],
|
|
2089
|
+
quickPhrases: [...this.settings.quickPhrases],
|
|
2090
|
+
quickPhrasesEnabled: this.settings.quickPhrasesEnabled,
|
|
2085
2091
|
};
|
|
2086
2092
|
this.emit({ type: "settings_state", settings });
|
|
2087
2093
|
}
|
|
@@ -2110,6 +2116,15 @@ export class DshClientSession {
|
|
|
2110
2116
|
this.settings.disabledPlugins = partial.disabledPlugins;
|
|
2111
2117
|
if (partial.reviewPrompt !== undefined)
|
|
2112
2118
|
this.settings.reviewPrompt = partial.reviewPrompt;
|
|
2119
|
+
if (partial.quickPhrases !== undefined) {
|
|
2120
|
+
const seen = new Set();
|
|
2121
|
+
this.settings.quickPhrases = (Array.isArray(partial.quickPhrases) ? partial.quickPhrases : [])
|
|
2122
|
+
.map((p) => String(p).trim().slice(0, 200))
|
|
2123
|
+
.filter((p) => p && !seen.has(p) && (seen.add(p), true))
|
|
2124
|
+
.slice(0, 30);
|
|
2125
|
+
}
|
|
2126
|
+
if (partial.quickPhrasesEnabled !== undefined)
|
|
2127
|
+
this.settings.quickPhrasesEnabled = partial.quickPhrasesEnabled;
|
|
2113
2128
|
// 持久化(跨重连存活)。
|
|
2114
2129
|
this.stateStore.saveSettings(this.clientId, {
|
|
2115
2130
|
promptMode: this.settings.promptMode,
|
|
@@ -2124,6 +2139,8 @@ export class DshClientSession {
|
|
|
2124
2139
|
toolsWrap: this.settings.toolsWrap,
|
|
2125
2140
|
disabledPlugins: this.settings.disabledPlugins,
|
|
2126
2141
|
reviewPrompt: this.settings.reviewPrompt,
|
|
2142
|
+
quickPhrases: this.settings.quickPhrases,
|
|
2143
|
+
quickPhrasesEnabled: this.settings.quickPhrasesEnabled,
|
|
2127
2144
|
});
|
|
2128
2145
|
// 仅系统提示词变化才重启运行时(DSH_PERSONA 由 launcher env 注入);
|
|
2129
2146
|
// 其他设置(开关/隐藏插件等)只存不回写运行时。
|
|
@@ -1 +1 @@
|
|
|
1
|
-
[]
|
|
1
|
+
[]
|