nolo-cli 0.1.41 → 0.1.43

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.
Files changed (136) hide show
  1. package/README.md +27 -12
  2. package/agent-runtime/agentThread.ts +222 -0
  3. package/agent-runtime/agentThreadAdmission.ts +74 -0
  4. package/agent-runtime/dialogWritePlan.ts +42 -0
  5. package/agent-runtime/hostAdapter.ts +1 -0
  6. package/agent-runtime/index.ts +43 -0
  7. package/agent-runtime/localLoop.ts +31 -2
  8. package/agent-runtime/localToolPolicy.ts +1 -0
  9. package/agent-runtime/localWorkspaceTools.ts +1258 -78
  10. package/agent-runtime/noloWorkspaceTools.ts +20 -1
  11. package/agent-runtime/runtimeToolPolicy.ts +1 -5
  12. package/agent-runtime/runtimeToolSurface.ts +1 -0
  13. package/agentAliases.ts +29 -9
  14. package/agentInternalCommandEntries.ts +11 -3
  15. package/agentMachineCommands.ts +1 -0
  16. package/agentRecordCommands.ts +2 -2
  17. package/agentRecordHelpers.ts +59 -6
  18. package/agentRunCommand.ts +84 -19
  19. package/ai/agent/buildSystemPrompt.ts +14 -0
  20. package/ai/agent/cliExecutor.ts +202 -43
  21. package/ai/agent/createAgentSchema.ts +2 -2
  22. package/ai/agent/liveAudioModel.ts +1 -1
  23. package/ai/agent/streamAgentChatTurn.ts +154 -20
  24. package/ai/agent/streamAgentChatTurnUtils.ts +63 -0
  25. package/ai/ai.locale.ts +70 -0
  26. package/ai/chat/inlineImageUrlsForCustomProvider.ts +7 -2
  27. package/ai/chat/sendOpenAICompletionsRequest.ts +25 -1
  28. package/ai/llm/kimi.ts +0 -1
  29. package/ai/llm/openrouterModels.ts +27 -2
  30. package/ai/memory/policy.ts +49 -0
  31. package/ai/memory/queryShared.ts +10 -4
  32. package/ai/memory/recentRelationshipRecap.ts +4 -0
  33. package/ai/memory/remember.ts +48 -104
  34. package/ai/memory/scope.ts +74 -0
  35. package/ai/memory/types.ts +6 -0
  36. package/ai/memory/understanding.ts +10 -18
  37. package/ai/policy/runtimePolicy.ts +95 -23
  38. package/ai/tools/agent/agentTools.ts +0 -21
  39. package/ai/tools/agent/startAgentDialogTool.ts +2 -1
  40. package/ai/tools/index.ts +2 -20
  41. package/ai/tools/prepareTools.ts +171 -1
  42. package/ai/tools/readXhsProfileTool.ts +80 -69
  43. package/ai/tools/table/createTableTool.ts +11 -0
  44. package/ai/tools/toolPacks.ts +19 -0
  45. package/ai/types.ts +3 -0
  46. package/app/favorite/deletedFavoriteProjection.ts +31 -0
  47. package/app/hooks/deleteDbKey.ts +24 -3
  48. package/app/i18n/translations/interface.locale.ts +48 -16
  49. package/app/types/appSummary.ts +4 -0
  50. package/app/utils/desktopAgentRuntimeTurnClient.ts +2 -0
  51. package/auth/adminPermissions.ts +64 -4
  52. package/auth/routes.ts +5 -0
  53. package/chat/chat.locale.ts +4 -0
  54. package/chat/dialog/actions/handleSendMessageAction.ts +7 -2
  55. package/chat/dialog/dialogAttachmentCleanup.ts +135 -0
  56. package/chat/dialog/dialogSlice.ts +117 -4
  57. package/chat/messages/activityCompletion.ts +97 -0
  58. package/chat/messages/imagePayloadPersistence.ts +22 -0
  59. package/chat/messages/messageContent.ts +20 -16
  60. package/chat/messages/messageSlice.ts +20 -1
  61. package/chat/messages/toolThunks.ts +32 -11
  62. package/chat/messages/types.ts +8 -1
  63. package/chat/messages/web/toolDisplayName.ts +686 -0
  64. package/cli/agentAliases.ts +29 -9
  65. package/cli/agentInternalCommandEntries.ts +11 -3
  66. package/cli/agentMachineCommands.ts +1 -0
  67. package/cli/agentRecordCommands.ts +2 -2
  68. package/cli/agentRecordHelpers.ts +59 -6
  69. package/cli/agentRunCommand.ts +84 -19
  70. package/cli/client/agentRun.ts +65 -253
  71. package/cli/client/localRuntimeAdapter.ts +179 -53
  72. package/cli/commandRegistry.ts +7 -4
  73. package/cli/dialogAttachmentCleanup.ts +151 -0
  74. package/cli/dialogCommands.ts +163 -61
  75. package/cli/dialogInternalCommandEntries.ts +1 -1
  76. package/cli/docCreateCommands.ts +345 -0
  77. package/cli/docInternalCommandEntries.ts +11 -0
  78. package/cli/docPageHelpers.ts +80 -0
  79. package/cli/docSkillHelpers.ts +67 -0
  80. package/cli/docSpaceHelpers.ts +46 -0
  81. package/cli/internalCommandEntries.ts +4 -0
  82. package/cli/machineCommands.ts +18 -34
  83. package/cli/memoryCommands.ts +253 -0
  84. package/cli/memoryInternalCommandEntries.ts +9 -0
  85. package/cli/qoderUsageProbe.ts +252 -0
  86. package/cli/scriptCommandEntries.ts +0 -3
  87. package/cli/secretScan.ts +31 -0
  88. package/cli/tableCommands.ts +248 -13
  89. package/cli/workflowInternalCommandEntries.ts +2 -3
  90. package/client/agentRun.test.ts +68 -84
  91. package/client/agentRun.ts +65 -253
  92. package/client/localRuntimeAdapter.test.ts +505 -74
  93. package/client/localRuntimeAdapter.ts +179 -53
  94. package/commandRegistry.ts +7 -4
  95. package/connector-experimental/capabilities.ts +10 -9
  96. package/create/space/sidebarVisibleTypes.ts +19 -0
  97. package/dialogAttachmentCleanup.ts +151 -0
  98. package/dialogCommands.ts +163 -61
  99. package/dialogInternalCommandEntries.ts +1 -1
  100. package/docCreateCommands.ts +345 -0
  101. package/docInternalCommandEntries.ts +11 -0
  102. package/docPageHelpers.ts +80 -0
  103. package/docSkillHelpers.ts +67 -0
  104. package/docSpaceHelpers.ts +46 -0
  105. package/integrations/google/models.ts +4 -4
  106. package/integrations/openai/generateOpenAIRequestBody.ts +0 -3
  107. package/integrations/xhs-reader/types.ts +36 -3
  108. package/integrations/xhs-reader/url.ts +24 -1
  109. package/internalCommandEntries.ts +4 -0
  110. package/machineCommands.ts +18 -34
  111. package/memoryCommands.ts +253 -0
  112. package/memoryInternalCommandEntries.ts +9 -0
  113. package/package.json +141 -114
  114. package/qoderUsageProbe.ts +252 -0
  115. package/render/contentIcon/types.ts +167 -0
  116. package/render/page/docSlice.ts +30 -3
  117. package/render/page/types.ts +2 -0
  118. package/render/table/createTableAction.ts +4 -0
  119. package/render/table/tableSlice.ts +61 -0
  120. package/render/table/types.ts +10 -0
  121. package/scriptCommandEntries.ts +0 -3
  122. package/secretScan.ts +31 -0
  123. package/server/handlers/agentRun/types.ts +24 -23
  124. package/tableCommands.ts +248 -13
  125. package/workflowInternalCommandEntries.ts +2 -3
  126. package/ai/tools/agent/taskRunTool.ts +0 -112
  127. package/cli/client/taskRunPrompt.ts +0 -27
  128. package/cli/taskRunCommand.ts +0 -265
  129. package/client/taskRunPrompt.ts +0 -27
  130. package/integrations/xhs-reader/analyze/profileAnalyzer.ts +0 -205
  131. package/integrations/xhs-reader/backends/playwrightProfileCollector.ts +0 -624
  132. package/integrations/xhs-reader/bridge/readXhsProfileWithBridge.ts +0 -227
  133. package/integrations/xhs-reader/client/xhsWebApiClient.ts +0 -229
  134. package/integrations/xhs-reader/normalize.ts +0 -390
  135. package/integrations/xhs-reader/orchestrator.ts +0 -178
  136. package/taskRunCommand.ts +0 -265
@@ -0,0 +1,686 @@
1
+ export function resolveToolDisplayName(
2
+ toolName: string | undefined,
3
+ translate: (key: string, fallback: string) => string
4
+ ) {
5
+ const normalized = typeof toolName === "string" ? toolName.trim() : "";
6
+ if (!normalized) return translate("toolNames.tool", "Tool");
7
+ return translate(`toolNames.${normalized}`, normalized);
8
+ }
9
+
10
+ export function formatToolInvocationSummary(
11
+ messages: Array<{ toolName?: string }>,
12
+ translate: (key: string, fallback: string) => string
13
+ ) {
14
+ const counts = new Map<string, number>();
15
+ for (const msg of messages) {
16
+ const name = msg.toolName || "tool";
17
+ counts.set(name, (counts.get(name) || 0) + 1);
18
+ }
19
+ return Array.from(counts.entries())
20
+ .map(([name, count]) => `${resolveToolDisplayName(name, translate)} × ${count}`)
21
+ .join("、");
22
+ }
23
+
24
+ // ─── Activity line extraction & fallback ─────────────────────────────
25
+
26
+ export type ToolActivityRef =
27
+ | { type: "file"; path: string }
28
+ | { type: "terminal"; id?: string; label?: string }
29
+ | { type: "url"; url: string; label?: string };
30
+
31
+ export type ToolActivityKind =
32
+ | "read"
33
+ | "write"
34
+ | "edit"
35
+ | "search"
36
+ | "terminal"
37
+ | "version"
38
+ | "test"
39
+ | "build"
40
+ | "preview"
41
+ | "other";
42
+
43
+ export type ToolActivityStatus = "pending" | "running" | "success" | "failed";
44
+
45
+ export type ToolActivityAction = {
46
+ title: string;
47
+ kind?: ToolActivityKind;
48
+ detail?: string;
49
+ refs?: ToolActivityRef[];
50
+ };
51
+
52
+ export type ToolActivityPhase = {
53
+ id: string;
54
+ title: string;
55
+ index?: number;
56
+ total?: number;
57
+ status?: ToolActivityStatus;
58
+ };
59
+
60
+ export type ActivityPlanPhase = {
61
+ id: string;
62
+ title: string;
63
+ index?: number;
64
+ status?: ToolActivityStatus;
65
+ };
66
+
67
+ export type ActivityPlan = {
68
+ title?: string;
69
+ phases: ActivityPlanPhase[];
70
+ };
71
+
72
+ export type ToolActivity = ToolActivityAction & {
73
+ phase?: ToolActivityPhase;
74
+ action?: ToolActivityAction;
75
+ plan?: ActivityPlan;
76
+ };
77
+
78
+ type ActivitySignal = {
79
+ phase?: ToolActivityPhase;
80
+ action?: ToolActivityAction;
81
+ plan?: ActivityPlan;
82
+ };
83
+
84
+ export type ActivityTimelineAction = ToolActivityAction & {
85
+ id: string;
86
+ label: string;
87
+ status: ToolActivityStatus;
88
+ message: unknown;
89
+ };
90
+
91
+ export type ActivityTimelinePhase = {
92
+ id: string;
93
+ title: string;
94
+ index?: number;
95
+ total?: number;
96
+ status: ToolActivityStatus;
97
+ actions: ActivityTimelineAction[];
98
+ };
99
+
100
+ export type ActivityTimeline = {
101
+ phases: ActivityTimelinePhase[];
102
+ completedPhases: number;
103
+ totalPhases: number;
104
+ };
105
+
106
+ export type BuildActivityTimelineOptions = {
107
+ includePlan?: boolean;
108
+ };
109
+
110
+ const SHELL_COMMAND_CLASSIFIERS: Array<{
111
+ pattern: RegExp;
112
+ label: (cmd: string) => string;
113
+ }> = [
114
+ {
115
+ pattern: /\bgit\s+(status)\b/,
116
+ label: () => "用版本管理检查改动",
117
+ },
118
+ {
119
+ pattern: /\bgit\s+diff\b/,
120
+ label: () => "用版本管理查看改动",
121
+ },
122
+ {
123
+ pattern: /\bgit\s+log\b/,
124
+ label: () => "用版本管理查看历史",
125
+ },
126
+ {
127
+ pattern: /\bgit\s+add\b/,
128
+ label: () => "用版本管理暂存改动",
129
+ },
130
+ {
131
+ pattern: /\bgit\s+commit\b/,
132
+ label: () => "用版本管理保存改动",
133
+ },
134
+ {
135
+ pattern: /\bgit\s+push\b/,
136
+ label: () => "用版本管理同步改动",
137
+ },
138
+ {
139
+ pattern: /\b(rg|grep)\b/,
140
+ label: (cmd) => {
141
+ const match = cmd.match(/(?:rg|grep)\s+(?:-[^\s]*\s+)*['"]?([^\s'"]+)['"]?/);
142
+ return match ? `搜索 "${match[1]}"` : "搜索代码";
143
+ },
144
+ },
145
+ {
146
+ pattern: /\b(bun\s+test|jest|vitest|mocha|pytest|go\s+test)\b/,
147
+ label: () => "运行测试",
148
+ },
149
+ {
150
+ pattern: /\b(bun\s+run\s+build|npm\s+run\s+build|yarn\s+build|make\s+build)\b/,
151
+ label: () => "构建项目",
152
+ },
153
+ {
154
+ pattern: /\b(bun\s+install|npm\s+install|yarn\s+install|pnpm\s+install)\b/,
155
+ label: () => "安装依赖",
156
+ },
157
+ {
158
+ pattern: /\b(bun\s+run\s+dev|npm\s+run\s+dev|yarn\s+dev)\b/,
159
+ label: () => "启动开发服务器",
160
+ },
161
+ {
162
+ pattern: /\bcurl\b/,
163
+ label: (cmd) => {
164
+ const match = cmd.match(/curl\s+(?:-[^\s]*\s+)*(https?:\/\/[^\s'"]+)/);
165
+ return match ? `请求 ${match[1]}` : "发送 HTTP 请求";
166
+ },
167
+ },
168
+ {
169
+ pattern: /\blsof\b/,
170
+ label: () => "查看端口占用",
171
+ },
172
+ ];
173
+
174
+ function classifyShellCommand(command: string): string {
175
+ const trimmed = command.trim();
176
+ for (const { pattern, label } of SHELL_COMMAND_CLASSIFIERS) {
177
+ if (pattern.test(trimmed)) return label(trimmed);
178
+ }
179
+ return "执行终端命令";
180
+ }
181
+
182
+ /**
183
+ * Build a fallback activity line from tool call arguments when the model
184
+ * did not provide an explicit `_activity` field.
185
+ */
186
+ export function buildFallbackActivity(
187
+ toolName: string | undefined,
188
+ args: Record<string, unknown> | undefined
189
+ ): ToolActivity | undefined {
190
+ const normalized = typeof toolName === "string" ? toolName.trim() : "";
191
+ if (!normalized || !args) return undefined;
192
+
193
+ switch (normalized) {
194
+ case "readFile": {
195
+ const path = typeof args.path === "string" ? args.path.trim() : "";
196
+ return path ? { title: "查看相关文件", refs: [{ type: "file", path }] } : undefined;
197
+ }
198
+ case "writeFile": {
199
+ const path = typeof args.path === "string" ? args.path.trim() : "";
200
+ return path ? { title: "写入文件", refs: [{ type: "file", path }] } : undefined;
201
+ }
202
+ case "editFile": {
203
+ const path = typeof args.path === "string" ? args.path.trim() : "";
204
+ return path ? { title: "修改文件", refs: [{ type: "file", path }] } : undefined;
205
+ }
206
+ case "searchFiles": {
207
+ const query = typeof args.query === "string" ? args.query.trim() : "";
208
+ return query ? { title: "在代码里找线索", detail: query } : undefined;
209
+ }
210
+ case "globFiles": {
211
+ const pattern = typeof args.pattern === "string"
212
+ ? args.pattern.trim()
213
+ : typeof args.glob === "string"
214
+ ? args.glob.trim()
215
+ : "";
216
+ return pattern ? { title: "查找相关文件", detail: pattern } : undefined;
217
+ }
218
+ case "execShell": {
219
+ const command =
220
+ typeof args.cmd === "string"
221
+ ? args.cmd.trim()
222
+ : typeof args.command === "string"
223
+ ? args.command.trim()
224
+ : "";
225
+ if (!command) return undefined;
226
+ const label = classifyShellCommand(command);
227
+ return { title: label, detail: command.length <= 120 ? command : `${command.slice(0, 117)}...` };
228
+ }
229
+ default:
230
+ return undefined;
231
+ }
232
+ }
233
+
234
+ function normalizeString(value: unknown): string | undefined {
235
+ if (typeof value !== "string") return undefined;
236
+ const trimmed = value.trim();
237
+ return trimmed || undefined;
238
+ }
239
+
240
+ function normalizeNumber(value: unknown): number | undefined {
241
+ if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
242
+ return value;
243
+ }
244
+
245
+ function normalizeStatus(value: unknown): ToolActivityStatus | undefined {
246
+ return value === "pending" ||
247
+ value === "running" ||
248
+ value === "success" ||
249
+ value === "failed"
250
+ ? value
251
+ : undefined;
252
+ }
253
+
254
+ function normalizeKind(value: unknown): ToolActivityKind | undefined {
255
+ return value === "read" ||
256
+ value === "write" ||
257
+ value === "edit" ||
258
+ value === "search" ||
259
+ value === "terminal" ||
260
+ value === "version" ||
261
+ value === "test" ||
262
+ value === "build" ||
263
+ value === "preview" ||
264
+ value === "other"
265
+ ? value
266
+ : undefined;
267
+ }
268
+
269
+ function normalizeRefs(value: unknown): ToolActivityRef[] | undefined {
270
+ if (!Array.isArray(value)) return undefined;
271
+ const refs = value.flatMap((item): ToolActivityRef[] => {
272
+ if (!item || typeof item !== "object" || Array.isArray(item)) return [];
273
+ const raw = item as Record<string, unknown>;
274
+ if (raw.type === "file") {
275
+ const path = normalizeString(raw.path);
276
+ return path ? [{ type: "file", path }] : [];
277
+ }
278
+ if (raw.type === "terminal") {
279
+ const id = normalizeString(raw.id);
280
+ const label = normalizeString(raw.label);
281
+ return id || label ? [{ type: "terminal", ...(id ? { id } : {}), ...(label ? { label } : {}) }] : [];
282
+ }
283
+ if (raw.type === "url") {
284
+ const url = normalizeString(raw.url);
285
+ const label = normalizeString(raw.label);
286
+ return url ? [{ type: "url", url, ...(label ? { label } : {}) }] : [];
287
+ }
288
+ return [];
289
+ });
290
+ return refs.length ? refs : undefined;
291
+ }
292
+
293
+ function normalizeActivityAction(value: unknown): ToolActivityAction | undefined {
294
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
295
+ const raw = value as Record<string, unknown>;
296
+ const title = normalizeString(raw.title);
297
+ if (!title) return undefined;
298
+ const detail = normalizeString(raw.detail);
299
+ const kind = normalizeKind(raw.kind);
300
+ const refs = normalizeRefs(raw.refs);
301
+ return {
302
+ title,
303
+ ...(kind ? { kind } : {}),
304
+ ...(detail ? { detail } : {}),
305
+ ...(refs ? { refs } : {}),
306
+ };
307
+ }
308
+
309
+ function normalizeActivityPhase(value: unknown): ToolActivityPhase | undefined {
310
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
311
+ const raw = value as Record<string, unknown>;
312
+ const title = normalizeString(raw.title);
313
+ if (!title) return undefined;
314
+ const id = normalizeString(raw.id) || title.toLowerCase().replace(/\s+/g, "-");
315
+ const index = normalizeNumber(raw.index);
316
+ const total = normalizeNumber(raw.total);
317
+ const status = normalizeStatus(raw.status);
318
+ return {
319
+ id,
320
+ title,
321
+ ...(index !== undefined ? { index } : {}),
322
+ ...(total !== undefined ? { total } : {}),
323
+ ...(status ? { status } : {}),
324
+ };
325
+ }
326
+
327
+ function normalizeActivityPlan(value: unknown): ActivityPlan | undefined {
328
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
329
+ const raw = value as Record<string, unknown>;
330
+ if (!Array.isArray(raw.phases)) return undefined;
331
+ const phases = raw.phases.flatMap((item, index): ActivityPlanPhase[] => {
332
+ const phase = normalizeActivityPhase(item);
333
+ if (!phase) return [];
334
+ return [{
335
+ id: phase.id,
336
+ title: phase.title,
337
+ index: phase.index ?? index + 1,
338
+ ...(phase.status ? { status: phase.status } : {}),
339
+ }];
340
+ });
341
+ if (phases.length === 0) return undefined;
342
+ const title = normalizeString(raw.title);
343
+ return {
344
+ ...(title ? { title } : {}),
345
+ phases,
346
+ };
347
+ }
348
+
349
+ function normalizeActivitySignal(value: unknown): ActivitySignal | undefined {
350
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
351
+ const raw = value as Record<string, unknown>;
352
+ const nestedAction = normalizeActivityAction(raw.action);
353
+ const legacyAction = normalizeActivityAction(raw);
354
+ const action = nestedAction || legacyAction;
355
+ const phase = normalizeActivityPhase(raw.phase);
356
+ const plan = normalizeActivityPlan(raw.plan);
357
+ if (!action && !phase && !plan) return undefined;
358
+ return {
359
+ ...(action ? { action } : {}),
360
+ ...(phase ? { phase } : {}),
361
+ ...(plan ? { plan } : {}),
362
+ };
363
+ }
364
+
365
+ export function normalizeToolActivity(value: unknown): ToolActivity | undefined {
366
+ const signal = normalizeActivitySignal(value);
367
+ if (!signal?.action) return undefined;
368
+ return {
369
+ ...signal.action,
370
+ ...(signal.phase ? { phase: signal.phase } : {}),
371
+ ...(signal.action ? { action: signal.action } : {}),
372
+ ...(signal.plan ? { plan: signal.plan } : {}),
373
+ };
374
+ }
375
+
376
+ function formatActivityAction(activity: ToolActivityAction): string {
377
+ const firstRef = activity.refs?.[0];
378
+ const refLabel =
379
+ firstRef?.type === "file"
380
+ ? firstRef.path
381
+ : firstRef?.type === "terminal"
382
+ ? firstRef.label || firstRef.id
383
+ : firstRef?.type === "url"
384
+ ? firstRef.label || firstRef.url
385
+ : undefined;
386
+ const suffix = refLabel || activity.detail;
387
+ return suffix ? `${activity.title} · ${suffix}` : activity.title;
388
+ }
389
+
390
+ function getMessageStatus(msg: any): ToolActivityStatus {
391
+ const rawData = typeof msg?.content === "string" ? safeParseObject(msg.content) : msg?.content;
392
+ const isError =
393
+ msg?.toolPayload?.status === "failed" ||
394
+ !!msg?.toolPayload?.error ||
395
+ !!rawData?.error;
396
+ if (msg?.isStreaming || msg?.toolPayload?.status === "running") return "running";
397
+ if (isError) return "failed";
398
+ if (msg?.toolPayload?.status === "pending") return "pending";
399
+ return "success";
400
+ }
401
+
402
+ function mergeStatuses(current: ToolActivityStatus, next: ToolActivityStatus): ToolActivityStatus {
403
+ if (current === "running" || next === "running") return "running";
404
+ if (current === "failed" || next === "failed") return "failed";
405
+ if (current === "pending" || next === "pending") return "pending";
406
+ return "success";
407
+ }
408
+
409
+ function readMessageActivity(msg: {
410
+ toolName?: string;
411
+ metadata?: Record<string, unknown>;
412
+ content?: string;
413
+ toolPayload?: Record<string, unknown>;
414
+ }): ToolActivity | undefined {
415
+ const meta = msg.metadata as Record<string, unknown> | undefined;
416
+ const explicit = normalizeToolActivity(meta?.activity) ||
417
+ normalizeToolActivity(msg.toolPayload?.activity);
418
+ if (explicit) return explicit;
419
+ const parsedArgs = tryParseToolArgs(msg);
420
+ const fallback = buildFallbackActivity(msg.toolName, parsedArgs);
421
+ return fallback ? normalizeToolActivity(fallback) : undefined;
422
+ }
423
+
424
+ function readMessageActivitySignal(msg: {
425
+ toolName?: string;
426
+ metadata?: Record<string, unknown>;
427
+ content?: string;
428
+ toolPayload?: Record<string, unknown>;
429
+ }): ActivitySignal | undefined {
430
+ const meta = msg.metadata as Record<string, unknown> | undefined;
431
+ const explicit = normalizeActivitySignal(meta?.activity) ||
432
+ normalizeActivitySignal(msg.toolPayload?.activity);
433
+ if (explicit) return explicit;
434
+ const parsedArgs = tryParseToolArgs(msg);
435
+ const fallback = buildFallbackActivity(msg.toolName, parsedArgs);
436
+ const fallbackActivity = fallback ? normalizeToolActivity(fallback) : undefined;
437
+ return fallbackActivity
438
+ ? {
439
+ action: fallbackActivity.action || fallbackActivity,
440
+ ...(fallbackActivity.phase ? { phase: fallbackActivity.phase } : {}),
441
+ ...(fallbackActivity.plan ? { plan: fallbackActivity.plan } : {}),
442
+ }
443
+ : undefined;
444
+ }
445
+
446
+ function readMessageActivityPlan(msg: {
447
+ metadata?: Record<string, unknown>;
448
+ toolPayload?: Record<string, unknown>;
449
+ }): ActivityPlan | undefined {
450
+ const meta = msg.metadata as Record<string, unknown> | undefined;
451
+ const metadataActivity =
452
+ meta?.activity && typeof meta.activity === "object" && !Array.isArray(meta.activity)
453
+ ? meta.activity as Record<string, unknown>
454
+ : undefined;
455
+ const payloadActivity =
456
+ msg.toolPayload?.activity &&
457
+ typeof msg.toolPayload.activity === "object" &&
458
+ !Array.isArray(msg.toolPayload.activity)
459
+ ? msg.toolPayload.activity as Record<string, unknown>
460
+ : undefined;
461
+ return normalizeActivitySignal(metadataActivity)?.plan ||
462
+ normalizeActivitySignal(payloadActivity)?.plan;
463
+ }
464
+
465
+ function safeParseObject(content: string): Record<string, unknown> | undefined {
466
+ if (!content.trim()) return undefined;
467
+ try {
468
+ const parsed = JSON.parse(content);
469
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
470
+ ? (parsed as Record<string, unknown>)
471
+ : undefined;
472
+ } catch {
473
+ return undefined;
474
+ }
475
+ }
476
+
477
+ export function buildActivityTimeline(
478
+ messages: Array<{
479
+ id?: string;
480
+ dbKey?: string;
481
+ tool_call_id?: string;
482
+ toolCallId?: string;
483
+ toolName?: string;
484
+ metadata?: Record<string, unknown>;
485
+ content?: string;
486
+ toolPayload?: Record<string, unknown>;
487
+ isStreaming?: boolean;
488
+ }>,
489
+ activityPlan?: unknown,
490
+ options: BuildActivityTimelineOptions = {}
491
+ ): ActivityTimeline {
492
+ const phases: ActivityTimelinePhase[] = [];
493
+ const phaseById = new Map<string, ActivityTimelinePhase>();
494
+ let implicitPhase: ActivityTimelinePhase | undefined;
495
+ let declaredTotal = 0;
496
+ const includePlan = options.includePlan !== false;
497
+ const declaredPlan = includePlan
498
+ ? normalizeActivityPlan(activityPlan) ||
499
+ messages.map(readMessageActivityPlan).find(Boolean)
500
+ : undefined;
501
+
502
+ const ensurePlanPhase = (phaseDef: ActivityPlanPhase): ActivityTimelinePhase => {
503
+ let phase = phaseById.get(phaseDef.id);
504
+ if (!phase) {
505
+ phase = {
506
+ id: phaseDef.id,
507
+ title: phaseDef.title,
508
+ ...(phaseDef.index !== undefined ? { index: phaseDef.index } : {}),
509
+ ...(declaredPlan ? { total: declaredPlan.phases.length } : {}),
510
+ status: phaseDef.status || "pending",
511
+ actions: [],
512
+ };
513
+ phaseById.set(phaseDef.id, phase);
514
+ phases.push(phase);
515
+ }
516
+ return phase;
517
+ };
518
+
519
+ if (declaredPlan) {
520
+ declaredTotal = declaredPlan.phases.length;
521
+ for (const phaseDef of declaredPlan.phases) {
522
+ ensurePlanPhase(phaseDef);
523
+ }
524
+ }
525
+
526
+ for (const msg of messages) {
527
+ const signal = readMessageActivitySignal(msg);
528
+ if (!signal) continue;
529
+ if (includePlan && !declaredPlan && signal.plan) {
530
+ declaredTotal = Math.max(declaredTotal, signal.plan.phases.length);
531
+ for (const phaseDef of signal.plan.phases) {
532
+ ensurePlanPhase(phaseDef);
533
+ }
534
+ }
535
+
536
+ const status = getMessageStatus(msg);
537
+ const activityAction = signal.action;
538
+ const phaseDef = signal.phase;
539
+ if (!activityAction && !phaseDef) continue;
540
+ const phaseId = phaseDef?.id || "__implicit_tools__";
541
+ const phaseTitle = phaseDef?.title || "执行工具步骤";
542
+ const phaseStatus = phaseDef?.status || (activityAction ? status : "pending");
543
+ let phase = phaseById.get(phaseId);
544
+
545
+ if (!phase) {
546
+ phase = {
547
+ id: phaseId,
548
+ title: phaseTitle,
549
+ ...(phaseDef?.index !== undefined ? { index: phaseDef.index } : {}),
550
+ ...(phaseDef?.total !== undefined ? { total: phaseDef.total } : {}),
551
+ status: phaseStatus,
552
+ actions: [],
553
+ };
554
+ phaseById.set(phaseId, phase);
555
+ phases.push(phase);
556
+ if (!phaseDef) implicitPhase = phase;
557
+ } else {
558
+ phase.status = mergeStatuses(phase.status, phaseStatus);
559
+ if (phase.status === "pending") {
560
+ phase.status = phaseStatus;
561
+ }
562
+ }
563
+
564
+ if (phaseDef?.total && phaseDef.total > declaredTotal) {
565
+ declaredTotal = phaseDef.total;
566
+ }
567
+
568
+ if (activityAction) {
569
+ const action: ActivityTimelineAction = {
570
+ ...activityAction,
571
+ id:
572
+ normalizeString(msg.id) ||
573
+ normalizeString(msg.dbKey) ||
574
+ normalizeString(msg.toolCallId) ||
575
+ normalizeString(msg.tool_call_id) ||
576
+ `tool-action-${phase.actions.length + 1}`,
577
+ label: formatActivityAction(activityAction),
578
+ status,
579
+ message: msg,
580
+ };
581
+ phase.actions.push(action);
582
+ }
583
+ }
584
+
585
+ if (implicitPhase && phases.length > 1 && implicitPhase.actions.length === 0) {
586
+ phaseById.delete(implicitPhase.id);
587
+ }
588
+
589
+ const completedPhases = phases.filter((phase) => phase.status === "success").length;
590
+ return {
591
+ phases,
592
+ completedPhases,
593
+ totalPhases: declaredTotal || phases.length,
594
+ };
595
+ }
596
+
597
+ /**
598
+ * Extract activity lines from an array of tool messages for collapsed-group display.
599
+ * Returns at most `maxLines` entries. Each entry is a displayable string.
600
+ */
601
+ export function extractActivityLines(
602
+ messages: Array<{
603
+ toolName?: string;
604
+ metadata?: Record<string, unknown>;
605
+ content?: string;
606
+ tool_call_id?: string;
607
+ toolPayload?: Record<string, unknown>;
608
+ }>,
609
+ maxLines: number = 5
610
+ ): { lines: string[]; overflow: number } {
611
+ const lines: string[] = [];
612
+ for (const msg of messages) {
613
+ if (lines.length >= maxLines) break;
614
+
615
+ // 1. Try explicit activity from metadata
616
+ const meta = msg.metadata as Record<string, unknown> | undefined;
617
+ const activity = normalizeToolActivity(meta?.activity) ||
618
+ normalizeToolActivity(msg.toolPayload?.activity);
619
+
620
+ if (activity) {
621
+ lines.push(formatActivityAction(activity.action || activity));
622
+ continue;
623
+ }
624
+
625
+ // 2. Fallback: derive from tool name + content/args
626
+ const parsedArgs = tryParseToolArgs(msg);
627
+ const fallback = buildFallbackActivity(msg.toolName, parsedArgs);
628
+ if (fallback) {
629
+ lines.push(formatActivityAction(fallback));
630
+ }
631
+ }
632
+ const overflow = Math.max(0, messages.length - maxLines);
633
+ return { lines, overflow };
634
+ }
635
+
636
+ function tryParseToolArgs(
637
+ msg: { content?: string; toolPayload?: Record<string, unknown>; metadata?: Record<string, unknown> }
638
+ ): Record<string, unknown> | undefined {
639
+ // 1. Standard: toolPayload.input (web runtime)
640
+ const payload = msg.toolPayload;
641
+ if (payload && typeof payload.input === "object" && payload.input !== null) {
642
+ return payload.input as Record<string, unknown>;
643
+ }
644
+
645
+ // 2. Desktop projected messages: derive from metadata fields
646
+ const meta = msg.metadata;
647
+ if (meta) {
648
+ const derived: Record<string, unknown> = {};
649
+ let hasField = false;
650
+ if (typeof meta.path === "string" && meta.path.trim()) {
651
+ derived.path = meta.path.trim();
652
+ hasField = true;
653
+ }
654
+ if (typeof meta.command === "string" && meta.command.trim()) {
655
+ derived.command = meta.command.trim();
656
+ hasField = true;
657
+ }
658
+ if (typeof meta.cmd === "string" && meta.cmd.trim()) {
659
+ derived.cmd = meta.cmd.trim();
660
+ hasField = true;
661
+ }
662
+ if (typeof meta.query === "string" && meta.query.trim()) {
663
+ derived.query = meta.query.trim();
664
+ hasField = true;
665
+ }
666
+ if (typeof meta.pattern === "string" && meta.pattern.trim()) {
667
+ derived.pattern = meta.pattern.trim();
668
+ hasField = true;
669
+ }
670
+ if (hasField) return derived;
671
+ }
672
+
673
+ // 3. Last resort: try parsing content as JSON tool args
674
+ if (typeof msg.content === "string" && msg.content.trim()) {
675
+ try {
676
+ const parsed = JSON.parse(msg.content);
677
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
678
+ return parsed as Record<string, unknown>;
679
+ }
680
+ } catch {
681
+ // not JSON, skip
682
+ }
683
+ }
684
+
685
+ return undefined;
686
+ }