chatccc 0.2.203 → 0.2.205
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/README.md +14 -10
- package/config.sample.json +8 -5
- package/package.json +1 -1
- package/src/__tests__/agent-activity.test.ts +76 -0
- package/src/__tests__/codex-adapter.test.ts +7 -7
- package/src/__tests__/config-reload.test.ts +19 -6
- package/src/__tests__/config-sample.test.ts +10 -1
- package/src/__tests__/cursor-adapter.test.ts +5 -5
- package/src/__tests__/session.test.ts +97 -17
- package/src/__tests__/startup-lifecycle.test.ts +98 -0
- package/src/__tests__/web-ui.test.ts +59 -0
- package/src/adapters/codex-adapter.ts +1 -0
- package/src/adapters/cursor-adapter.ts +1 -0
- package/src/agent-activity.ts +170 -0
- package/src/config.ts +19 -11
- package/src/index.ts +59 -19
- package/src/orchestrator.ts +11 -4
- package/src/session-chat-binding.ts +6 -4
- package/src/session.ts +89 -60
- package/src/startup-lifecycle.ts +96 -0
- package/src/stream-state.ts +13 -7
- package/src/web-ui.ts +185 -127
package/src/session.ts
CHANGED
|
@@ -21,7 +21,12 @@ import {
|
|
|
21
21
|
toolDisplayName,
|
|
22
22
|
ts,
|
|
23
23
|
} from "./config.ts";
|
|
24
|
-
import { buildProgressCard, getToolEmoji, isCodeBlockOpen, truncateContent } from "./cards.ts";
|
|
24
|
+
import { buildProgressCard, getToolEmoji, isCodeBlockOpen, truncateContent } from "./cards.ts";
|
|
25
|
+
import {
|
|
26
|
+
createAgentActivityTracker,
|
|
27
|
+
formatAgentActivityTitle,
|
|
28
|
+
updateAgentActivity,
|
|
29
|
+
} from "./agent-activity.ts";
|
|
25
30
|
import { simplifyToolUse, simplifyToolResult } from "./simplify.ts";
|
|
26
31
|
import { logTrace } from "./trace.ts";
|
|
27
32
|
import type { UnifiedBlock } from "./adapters/adapter-interface.ts";
|
|
@@ -82,18 +87,19 @@ async function sendFinalReplyTextOnce(
|
|
|
82
87
|
return sent;
|
|
83
88
|
}
|
|
84
89
|
|
|
85
|
-
async function createVisibleProgressCard(
|
|
90
|
+
async function createVisibleProgressCard(
|
|
86
91
|
platform: PlatformAdapter,
|
|
87
92
|
chatId: string,
|
|
88
93
|
sessionId: string,
|
|
89
|
-
turnCount: number,
|
|
90
|
-
notifyFailureText?: string,
|
|
91
|
-
|
|
94
|
+
turnCount: number,
|
|
95
|
+
notifyFailureText?: string,
|
|
96
|
+
headerTitle = "正在启动 Agent · 0秒",
|
|
97
|
+
): Promise<string | null> {
|
|
92
98
|
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
93
99
|
let cardId: string | null = null;
|
|
94
100
|
try {
|
|
95
|
-
cardId = await platform.cardCreate(
|
|
96
|
-
buildProgressCard("", { showStop: true, headerTitle
|
|
101
|
+
cardId = await platform.cardCreate(
|
|
102
|
+
buildProgressCard("等待 Agent 输出...", { showStop: true, headerTitle }),
|
|
97
103
|
);
|
|
98
104
|
if (!cardId) throw new Error("empty card id");
|
|
99
105
|
await platform.cardSend(chatId, cardId);
|
|
@@ -1115,29 +1121,33 @@ export async function runAgentSession(
|
|
|
1115
1121
|
}
|
|
1116
1122
|
|
|
1117
1123
|
// 初始化 stream-state.json
|
|
1118
|
-
const initialState = createEmptyStreamState(sessionId, cwd, tool, nextTurnCount);
|
|
1119
|
-
|
|
1124
|
+
const initialState = createEmptyStreamState(sessionId, cwd, tool, nextTurnCount);
|
|
1125
|
+
const activityTracker = createAgentActivityTracker(initialState.activity?.startedAt ?? Date.now());
|
|
1126
|
+
await writeStreamState(initialState);
|
|
1120
1127
|
|
|
1121
1128
|
// 为新 turn 创建第一张展示卡片,同时注册到 turn-cards 持久化。
|
|
1122
1129
|
// 统一 display loop 始终运行,卡片创建后下一个 tick 即自动开始更新。
|
|
1123
1130
|
const displayChatIdForNew = pickDisplayChat(sessionId);
|
|
1124
|
-
if (displayChatIdForNew) {
|
|
1125
|
-
const ppNew = platformForChat(displayChatIdForNew);
|
|
1126
|
-
if (ppNew && ppNew.kind !== "wechat") {
|
|
1127
|
-
const
|
|
1131
|
+
if (displayChatIdForNew) {
|
|
1132
|
+
const ppNew = platformForChat(displayChatIdForNew);
|
|
1133
|
+
if (ppNew && ppNew.kind !== "wechat") {
|
|
1134
|
+
const initialHeaderTitle = formatAgentActivityTitle(activityTracker.activity);
|
|
1135
|
+
const cardId = await createVisibleProgressCard(
|
|
1128
1136
|
ppNew,
|
|
1129
1137
|
displayChatIdForNew,
|
|
1130
1138
|
sessionId,
|
|
1131
|
-
nextTurnCount,
|
|
1132
|
-
"生成中卡片发送失败,结果将以文本形式发送。",
|
|
1139
|
+
nextTurnCount,
|
|
1140
|
+
"生成中卡片发送失败,结果将以文本形式发送。",
|
|
1141
|
+
initialHeaderTitle,
|
|
1133
1142
|
);
|
|
1134
1143
|
if (cardId) {
|
|
1135
1144
|
displayCards.set(displayChatIdForNew, {
|
|
1136
1145
|
cardId,
|
|
1137
1146
|
sequence: 1,
|
|
1138
1147
|
cardBusy: false,
|
|
1139
|
-
cardCreatedAt: Date.now(),
|
|
1140
|
-
lastSentContent: "",
|
|
1148
|
+
cardCreatedAt: Date.now(),
|
|
1149
|
+
lastSentContent: "",
|
|
1150
|
+
lastSentHeaderTitle: initialHeaderTitle,
|
|
1141
1151
|
streamErrorNotified: false,
|
|
1142
1152
|
sessionId,
|
|
1143
1153
|
turnCount: nextTurnCount,
|
|
@@ -1179,7 +1189,7 @@ export async function runAgentSession(
|
|
|
1179
1189
|
let streamErrored = false;
|
|
1180
1190
|
|
|
1181
1191
|
try {
|
|
1182
|
-
for await (const unifiedMsg of adapter.prompt(sessionId, userTextWithCapabilities, cwd, controller.signal, {
|
|
1192
|
+
for await (const unifiedMsg of adapter.prompt(sessionId, userTextWithCapabilities, cwd, controller.signal, {
|
|
1183
1193
|
onProcessStart: (processInfo) => {
|
|
1184
1194
|
startPromptProcessMonitor(sessionId, processInfo);
|
|
1185
1195
|
if (processInfo.pid !== undefined) registerProcess(processInfo.pid, sessionId);
|
|
@@ -1193,8 +1203,10 @@ export async function runAgentSession(
|
|
|
1193
1203
|
if (prompt) prompt.closeSession = closeSession;
|
|
1194
1204
|
},
|
|
1195
1205
|
})) {
|
|
1196
|
-
|
|
1197
|
-
|
|
1206
|
+
let activityChanged = false;
|
|
1207
|
+
for (const block of unifiedMsg.blocks) {
|
|
1208
|
+
if (updateAgentActivity(activityTracker, block)) activityChanged = true;
|
|
1209
|
+
accumulateBlockContent(block, state, toolCallMap);
|
|
1198
1210
|
|
|
1199
1211
|
if (block.type === "compact_boundary" && block.post_tokens) {
|
|
1200
1212
|
for (const cid of getChatsForSession(sessionId)) {
|
|
@@ -1213,13 +1225,14 @@ export async function runAgentSession(
|
|
|
1213
1225
|
|
|
1214
1226
|
// 定时写入文件
|
|
1215
1227
|
const now2 = Date.now();
|
|
1216
|
-
if (now2 - lastFileWrite >= FILE_WRITE_INTERVAL_MS) {
|
|
1217
|
-
lastFileWrite = now2;
|
|
1218
|
-
await writeStreamState({
|
|
1228
|
+
if (activityChanged || now2 - lastFileWrite >= FILE_WRITE_INTERVAL_MS) {
|
|
1229
|
+
lastFileWrite = now2;
|
|
1230
|
+
await writeStreamState({
|
|
1219
1231
|
sessionId,
|
|
1220
1232
|
status: "running",
|
|
1221
|
-
accumulatedContent: state.accumulatedContent,
|
|
1222
|
-
finalReply: pickFinalReply(state),
|
|
1233
|
+
accumulatedContent: state.accumulatedContent,
|
|
1234
|
+
finalReply: pickFinalReply(state),
|
|
1235
|
+
activity: activityTracker.activity,
|
|
1223
1236
|
chunkCount: state.chunkCount,
|
|
1224
1237
|
turnCount: nextTurnCount,
|
|
1225
1238
|
contextTokens: existingInfo?.lastContextTokens ?? 0,
|
|
@@ -1267,8 +1280,9 @@ export async function runAgentSession(
|
|
|
1267
1280
|
await writeStreamState({
|
|
1268
1281
|
sessionId,
|
|
1269
1282
|
status: finalStatus,
|
|
1270
|
-
accumulatedContent: state.accumulatedContent,
|
|
1271
|
-
finalReply: finalReplyToWrite,
|
|
1283
|
+
accumulatedContent: state.accumulatedContent,
|
|
1284
|
+
finalReply: finalReplyToWrite,
|
|
1285
|
+
activity: activityTracker.activity,
|
|
1272
1286
|
chunkCount: state.chunkCount,
|
|
1273
1287
|
turnCount: nextTurnCount,
|
|
1274
1288
|
contextTokens: existingInfo?.lastContextTokens ?? 0,
|
|
@@ -1553,14 +1567,16 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
1553
1567
|
}
|
|
1554
1568
|
} else {
|
|
1555
1569
|
// 非 WeChat: 卡片流程
|
|
1556
|
-
if (display.turnCount !== state.turnCount) {
|
|
1570
|
+
if (display.turnCount !== state.turnCount) {
|
|
1557
1571
|
console.log(`[${ts()}] [DISPLAY] turn mismatch for ${chatId}: display.turnCount=${display.turnCount} state.turnCount=${state.turnCount}, resetting`);
|
|
1558
1572
|
finalizeTurnCards(sessionId, display.turnCount, "done").catch(() => {});
|
|
1559
1573
|
displayCards.delete(chatId);
|
|
1560
|
-
continue;
|
|
1561
|
-
}
|
|
1562
|
-
|
|
1563
|
-
|
|
1574
|
+
continue;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
const activityHeaderTitle = formatAgentActivityTitle(state.activity, Date.now());
|
|
1578
|
+
|
|
1579
|
+
// 卡片轮转
|
|
1564
1580
|
if (Date.now() - display.cardCreatedAt > CARD_ROTATE_MS) {
|
|
1565
1581
|
display.cardBusy = true;
|
|
1566
1582
|
try {
|
|
@@ -1568,8 +1584,9 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
1568
1584
|
p,
|
|
1569
1585
|
chatId,
|
|
1570
1586
|
sessionId,
|
|
1571
|
-
display.turnCount,
|
|
1572
|
-
display.streamErrorNotified ? undefined : "生成中卡片发送失败,结果将继续更新在上一张卡片中。",
|
|
1587
|
+
display.turnCount,
|
|
1588
|
+
display.streamErrorNotified ? undefined : "生成中卡片发送失败,结果将继续更新在上一张卡片中。",
|
|
1589
|
+
activityHeaderTitle,
|
|
1573
1590
|
);
|
|
1574
1591
|
if (!newCardId) {
|
|
1575
1592
|
display.streamErrorNotified = true;
|
|
@@ -1577,7 +1594,7 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
1577
1594
|
}
|
|
1578
1595
|
const oldSeqBase = display.sequence;
|
|
1579
1596
|
const oldContent = state.accumulatedContent + state.finalReply;
|
|
1580
|
-
const oldCard = buildProgressCard(truncateContent(oldContent) || " ", { showStop: false, headerTitle: "
|
|
1597
|
+
const oldCard = buildProgressCard(truncateContent(oldContent) || " ", { showStop: false, headerTitle: "上一阶段记录" });
|
|
1581
1598
|
await p.cardUpdate(display.cardId, oldCard, oldSeqBase + 1).then(() => {
|
|
1582
1599
|
display.sequence = oldSeqBase + 1;
|
|
1583
1600
|
}).catch(err => {
|
|
@@ -1588,9 +1605,10 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
1588
1605
|
display.sequence = 1;
|
|
1589
1606
|
display.cardCreatedAt = Date.now();
|
|
1590
1607
|
display.rotationAccLen = state.accumulatedContent.length;
|
|
1591
|
-
display.rotationFinalReply = state.finalReply;
|
|
1592
|
-
display.lastSentContent = "";
|
|
1593
|
-
display.
|
|
1608
|
+
display.rotationFinalReply = state.finalReply;
|
|
1609
|
+
display.lastSentContent = "";
|
|
1610
|
+
display.lastSentHeaderTitle = activityHeaderTitle;
|
|
1611
|
+
display.streamErrorNotified = false;
|
|
1594
1612
|
} catch (err) {
|
|
1595
1613
|
console.error(`[${ts()}] [CARDIKT] rotation FAIL for ${chatId}: ${(err as Error).message}`);
|
|
1596
1614
|
} finally {
|
|
@@ -1608,17 +1626,24 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
1608
1626
|
replyDelta = state.finalReply.slice(rotReply.length);
|
|
1609
1627
|
} else {
|
|
1610
1628
|
replyDelta = state.finalReply;
|
|
1611
|
-
}
|
|
1612
|
-
const delta = (accDelta + replyDelta).trim();
|
|
1613
|
-
|
|
1614
|
-
display.dotCount = (display.dotCount % 9) + 1;
|
|
1615
|
-
let deltaBase =
|
|
1616
|
-
if (isCodeBlockOpen(deltaBase)) deltaBase += "\n```";
|
|
1617
|
-
const displayContent = deltaBase + "\n" + "。"
|
|
1618
|
-
if (
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1629
|
+
}
|
|
1630
|
+
const delta = (accDelta + replyDelta).trim();
|
|
1631
|
+
|
|
1632
|
+
display.dotCount = (display.dotCount % 9) + 1;
|
|
1633
|
+
let deltaBase = delta;
|
|
1634
|
+
if (isCodeBlockOpen(deltaBase)) deltaBase += "\n```";
|
|
1635
|
+
const displayContent = deltaBase + "\n" + "。".repeat(display.dotCount);
|
|
1636
|
+
if (
|
|
1637
|
+
displayContent === display.lastSentContent
|
|
1638
|
+
&& activityHeaderTitle === display.lastSentHeaderTitle
|
|
1639
|
+
) continue;
|
|
1640
|
+
|
|
1641
|
+
display.lastSentContent = displayContent;
|
|
1642
|
+
display.lastSentHeaderTitle = activityHeaderTitle;
|
|
1643
|
+
const deltaCard = buildProgressCard(truncateContent(displayContent) || "等待 Agent 输出...", {
|
|
1644
|
+
showStop: true,
|
|
1645
|
+
headerTitle: activityHeaderTitle,
|
|
1646
|
+
});
|
|
1622
1647
|
display.cardBusy = true;
|
|
1623
1648
|
const mySeq = display.sequence + 1;
|
|
1624
1649
|
try {
|
|
@@ -1637,20 +1662,24 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
1637
1662
|
display.cardBusy = false;
|
|
1638
1663
|
}
|
|
1639
1664
|
continue;
|
|
1640
|
-
}
|
|
1641
|
-
|
|
1642
|
-
display.dotCount = (display.dotCount % 9) + 1;
|
|
1643
|
-
let contentBase = state.accumulatedContent + state.finalReply;
|
|
1644
|
-
if (isCodeBlockOpen(contentBase)) contentBase += "\n```";
|
|
1645
|
-
const fullContent = contentBase + "\n" + "。".repeat(display.dotCount);
|
|
1646
|
-
if (
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
display.dotCount = (display.dotCount % 9) + 1;
|
|
1668
|
+
let contentBase = state.accumulatedContent + state.finalReply;
|
|
1669
|
+
if (isCodeBlockOpen(contentBase)) contentBase += "\n```";
|
|
1670
|
+
const fullContent = contentBase + "\n" + "。".repeat(display.dotCount);
|
|
1671
|
+
if (
|
|
1672
|
+
fullContent === display.lastSentContent
|
|
1673
|
+
&& activityHeaderTitle === display.lastSentHeaderTitle
|
|
1674
|
+
) continue;
|
|
1675
|
+
|
|
1676
|
+
display.lastSentContent = fullContent;
|
|
1677
|
+
display.lastSentHeaderTitle = activityHeaderTitle;
|
|
1678
|
+
const cardContent = truncateContent(fullContent) || "等待 Agent 输出...";
|
|
1650
1679
|
display.cardBusy = true;
|
|
1651
1680
|
const mySeq = display.sequence + 1;
|
|
1652
1681
|
try {
|
|
1653
|
-
const card = buildProgressCard(cardContent, { showStop: true, headerTitle:
|
|
1682
|
+
const card = buildProgressCard(cardContent, { showStop: true, headerTitle: activityHeaderTitle });
|
|
1654
1683
|
await p.cardUpdate(display.cardId, card, mySeq);
|
|
1655
1684
|
display.sequence = mySeq;
|
|
1656
1685
|
} catch (err) {
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ChatCCC 自己拉起替代进程时使用的内部标记。
|
|
5
|
+
*
|
|
6
|
+
* 不能用“是否已有配置”判断是否打开控制台:首次配置和日常直接启动都应该
|
|
7
|
+
* 打开,而 `/restart`、`/update` 和 Web UI 重启都不应该打扰用户。环境变量
|
|
8
|
+
* 会自然穿过 cmd/bash/npx 这几层启动器,因此也适用于 Windows 与 Linux。
|
|
9
|
+
*/
|
|
10
|
+
export const INTERNAL_RESTART_ENV_VAR = "CHATCCC_INTERNAL_RESTART";
|
|
11
|
+
|
|
12
|
+
type Environment = Record<string, string | undefined>;
|
|
13
|
+
|
|
14
|
+
export function createInternalRestartEnv(
|
|
15
|
+
inherited: Environment = process.env,
|
|
16
|
+
): NodeJS.ProcessEnv {
|
|
17
|
+
return {
|
|
18
|
+
...inherited,
|
|
19
|
+
[INTERNAL_RESTART_ENV_VAR]: "1",
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** 用户直接启动时打开;ChatCCC 内部重启产生的替代进程不打开。 */
|
|
24
|
+
interface AutoOpenWebUiOptions {
|
|
25
|
+
env?: Environment;
|
|
26
|
+
openOnStart?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function shouldAutoOpenWebUi(options: AutoOpenWebUiOptions = {}): boolean {
|
|
30
|
+
const env = options.env ?? process.env;
|
|
31
|
+
return options.openOnStart !== false && env[INTERNAL_RESTART_ENV_VAR] !== "1";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Web UI 始终使用 localhost,并跟随实际配置端口。 */
|
|
35
|
+
export function buildWebUiUrl(port: number): string {
|
|
36
|
+
return `http://localhost:${port}/`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface OpenBrowserDeps {
|
|
40
|
+
platform?: NodeJS.Platform;
|
|
41
|
+
env?: Environment;
|
|
42
|
+
spawnImpl?: typeof spawn;
|
|
43
|
+
onError?: (message: string) => void;
|
|
44
|
+
onInfo?: (message: string) => void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 调用操作系统默认浏览器打开 Web UI,与 Chrome CDP 守护功能完全独立。
|
|
49
|
+
* 返回值仅表示打开请求是否成功发起;浏览器是否复用标签页由系统浏览器决定。
|
|
50
|
+
*/
|
|
51
|
+
export function openWebUiInDefaultBrowser(
|
|
52
|
+
port: number,
|
|
53
|
+
deps: OpenBrowserDeps = {},
|
|
54
|
+
): boolean {
|
|
55
|
+
const url = buildWebUiUrl(port);
|
|
56
|
+
const platform = deps.platform ?? process.platform;
|
|
57
|
+
const env = deps.env ?? process.env;
|
|
58
|
+
const spawnImpl = deps.spawnImpl ?? spawn;
|
|
59
|
+
const onError = deps.onError ?? ((message: string) => console.error(message));
|
|
60
|
+
const onInfo = deps.onInfo ?? ((message: string) => console.log(message));
|
|
61
|
+
|
|
62
|
+
// Linux 服务器通常没有图形会话。此时调用 xdg-open 只会制造噪音;
|
|
63
|
+
// 直接给出可复制的 SSH 隧道命令,让用户从自己的电脑访问本地 Web UI。
|
|
64
|
+
if (platform === "linux" && !env.DISPLAY && !env.WAYLAND_DISPLAY) {
|
|
65
|
+
onInfo(
|
|
66
|
+
`[WEB-UI] 未检测到 Linux 图形桌面,跳过自动打开浏览器。` +
|
|
67
|
+
`可在本机执行 ssh -L ${port}:127.0.0.1:${port} <user>@<server>,` +
|
|
68
|
+
`然后访问 ${url}`,
|
|
69
|
+
);
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
let child: ChildProcess;
|
|
75
|
+
if (platform === "win32") {
|
|
76
|
+
// `start` 会把第一个带引号的参数当窗口标题,空字符串是必要占位符。
|
|
77
|
+
child = spawnImpl("cmd.exe", ["/c", "start", "", url], {
|
|
78
|
+
detached: true,
|
|
79
|
+
stdio: "ignore",
|
|
80
|
+
windowsHide: true,
|
|
81
|
+
});
|
|
82
|
+
} else if (platform === "darwin") {
|
|
83
|
+
child = spawnImpl("open", [url], { detached: true, stdio: "ignore" });
|
|
84
|
+
} else {
|
|
85
|
+
child = spawnImpl("xdg-open", [url], { detached: true, stdio: "ignore" });
|
|
86
|
+
}
|
|
87
|
+
child.on("error", (err) => {
|
|
88
|
+
onError(`[WEB-UI] 自动打开浏览器失败: ${err.message}`);
|
|
89
|
+
});
|
|
90
|
+
child.unref();
|
|
91
|
+
return true;
|
|
92
|
+
} catch (err) {
|
|
93
|
+
onError(`[WEB-UI] 自动打开浏览器失败: ${(err as Error).message}`);
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
package/src/stream-state.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { readFile, writeFile, mkdir, rename, unlink } from "node:fs/promises";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
|
|
4
|
-
import { USER_DATA_DIR, ts } from "./config.ts";
|
|
4
|
+
import { USER_DATA_DIR, ts } from "./config.ts";
|
|
5
|
+
import { createAgentActivityTracker } from "./agent-activity.ts";
|
|
6
|
+
import type { AgentActivity } from "./agent-activity.ts";
|
|
5
7
|
|
|
6
8
|
// ---------------------------------------------------------------------------
|
|
7
9
|
// stream-state.json — 每个 session 的流式输出持久化文件
|
|
@@ -16,7 +18,9 @@ export interface StreamState {
|
|
|
16
18
|
/** 本轮会话中 LLM 输出的全部文本内容(所有 text block 的累加)。
|
|
17
19
|
* 命名含 "final" 但实为"全部累积文本",并非仅"最终一段回复"。
|
|
18
20
|
* 参见 session.ts 的 AccumulatorState 注释。 */
|
|
19
|
-
finalReply: string;
|
|
21
|
+
finalReply: string;
|
|
22
|
+
/** Current user-visible work phase for running progress cards. */
|
|
23
|
+
activity?: AgentActivity;
|
|
20
24
|
/** The turn whose terminal text reply has already been delivered to IM. */
|
|
21
25
|
finalReplySentTurn?: number;
|
|
22
26
|
finalReplySentAt?: number;
|
|
@@ -122,16 +126,18 @@ export async function markFinalReplySent(sessionId: string, turnCount: number, s
|
|
|
122
126
|
await writeStreamState(state);
|
|
123
127
|
}
|
|
124
128
|
|
|
125
|
-
export function createEmptyStreamState(sessionId: string, cwd: string, tool: string, turnCount: number): StreamState {
|
|
126
|
-
|
|
129
|
+
export function createEmptyStreamState(sessionId: string, cwd: string, tool: string, turnCount: number): StreamState {
|
|
130
|
+
const now = Date.now();
|
|
131
|
+
return {
|
|
127
132
|
sessionId,
|
|
128
133
|
status: "running",
|
|
129
|
-
accumulatedContent: "",
|
|
130
|
-
finalReply: "",
|
|
134
|
+
accumulatedContent: "",
|
|
135
|
+
finalReply: "",
|
|
136
|
+
activity: createAgentActivityTracker(now).activity,
|
|
131
137
|
chunkCount: 0,
|
|
132
138
|
turnCount,
|
|
133
139
|
contextTokens: 0,
|
|
134
|
-
updatedAt:
|
|
140
|
+
updatedAt: now,
|
|
135
141
|
cwd,
|
|
136
142
|
tool,
|
|
137
143
|
};
|