ocuclaw 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1041 @@
1
+ import path from "node:path";
2
+
3
+ const GLOBAL_RUN_KEY = "__global__";
4
+ const DEFAULT_MAX_LABEL_CHARS = 120;
5
+ const TOOL_PREVIEW_CHARS = 30;
6
+
7
+ const REDACT_QUERY_KEYS = "(token|access_token|api_key|key|password|secret)";
8
+ const THINKING_SUMMARY_KEYS = ["summary", "thinkingSummary", "reasoningSummary", "intentLabel"];
9
+ const THINKING_DETAIL_KEYS = ["thinking", "reasoning", "thinkingText", "text", "analysis"];
10
+ const GENERIC_THINKING_LABEL = "Thinking...";
11
+ const ACTIVITY_INTENTS = new Set([
12
+ "thinking",
13
+ "thinking_summary",
14
+ "queued",
15
+ "fs.read",
16
+ "fs.write",
17
+ "fs.edit",
18
+ "search.files",
19
+ "search.web",
20
+ "browser.browse",
21
+ "browser.navigate",
22
+ "browser.fill",
23
+ "network.fetch",
24
+ "terminal.exec",
25
+ "terminal.git",
26
+ "agent.subtask",
27
+ "agent.coordinate",
28
+ "message.send",
29
+ "session.manage",
30
+ "canvas.edit",
31
+ "generic",
32
+ ]);
33
+
34
+ function isObject(value) {
35
+ return value && typeof value === "object" && !Array.isArray(value);
36
+ }
37
+
38
+ function asString(value) {
39
+ return typeof value === "string" ? value : null;
40
+ }
41
+
42
+ function isNullishToken(value) {
43
+ if (typeof value !== "string") return false;
44
+ const normalized = value.trim().toLowerCase();
45
+ return (
46
+ normalized === "null" ||
47
+ normalized === "undefined" ||
48
+ normalized === "(null)" ||
49
+ normalized === "(undefined)" ||
50
+ normalized === "none"
51
+ );
52
+ }
53
+
54
+ function normalizeLowerToken(value) {
55
+ const text = asString(value);
56
+ return text ? text.trim().toLowerCase() : "";
57
+ }
58
+
59
+ function pickString(obj, keys) {
60
+ const entry = pickStringEntry(obj, keys);
61
+ return entry ? entry.value : null;
62
+ }
63
+
64
+ function pickStringEntry(obj, keys) {
65
+ if (!isObject(obj)) return null;
66
+ for (const key of keys) {
67
+ const value = asString(obj[key]);
68
+ if (value && value.trim()) return { key, value };
69
+ }
70
+ return null;
71
+ }
72
+
73
+ function normalizeThinkingText(raw) {
74
+ const text = asString(raw);
75
+ if (!text) return null;
76
+ const cleaned = text.replace(/\*\*/g, "").trim();
77
+ return cleaned || null;
78
+ }
79
+
80
+ function extractFirstBoldThinkingSegment(raw) {
81
+ const text = asString(raw);
82
+ if (!text) return null;
83
+ const match = text.match(/\*\*([\s\S]+?)\*\*/);
84
+ if (!match) return null;
85
+ return normalizeThinkingText(match[1]);
86
+ }
87
+
88
+ function normalizeThinkingSummarySource(value) {
89
+ const normalized = normalizeLowerToken(value);
90
+ return (
91
+ normalized === "summary" ||
92
+ normalized === "bold" ||
93
+ normalized === "detail" ||
94
+ normalized === "generic"
95
+ )
96
+ ? normalized
97
+ : null;
98
+ }
99
+
100
+ function normalizeIntent(value) {
101
+ const normalized = normalizeLowerToken(value);
102
+ return ACTIVITY_INTENTS.has(normalized) ? normalized : null;
103
+ }
104
+
105
+ function parseArgs(raw) {
106
+ if (isObject(raw)) return raw;
107
+ if (typeof raw !== "string") return null;
108
+ try {
109
+ const parsed = JSON.parse(raw);
110
+ return isObject(parsed) ? parsed : null;
111
+ } catch {
112
+ return null;
113
+ }
114
+ }
115
+
116
+ function normalizeArgs(activity) {
117
+ const candidates = [
118
+ activity && activity.args,
119
+ activity && activity.arguments,
120
+ activity && activity.toolArgs,
121
+ activity && activity.input,
122
+ ];
123
+ for (const candidate of candidates) {
124
+ const parsed = parseArgs(candidate);
125
+ if (parsed) return parsed;
126
+ }
127
+ return null;
128
+ }
129
+
130
+ function pickThinkingSummary(activity) {
131
+ return normalizeThinkingText(pickString(activity, THINKING_SUMMARY_KEYS));
132
+ }
133
+
134
+ function pickThinkingDetail(activity) {
135
+ return normalizeThinkingText(pickString(activity, THINKING_DETAIL_KEYS));
136
+ }
137
+
138
+ function resolveThinkingContent(activity, existingLabel, existingDetail, includeThinking) {
139
+ const summaryEntry = pickStringEntry(activity, THINKING_SUMMARY_KEYS);
140
+ const detailEntry = pickStringEntry(activity, THINKING_DETAIL_KEYS);
141
+ const summaryText = normalizeThinkingText(summaryEntry && summaryEntry.value);
142
+ const detailText = normalizeThinkingText(detailEntry && detailEntry.value);
143
+ const boldLabelCandidate = extractFirstBoldThinkingSegment(detailEntry && detailEntry.value);
144
+ const explicitSource = normalizeThinkingSummarySource(
145
+ activity && activity.thinkingSummarySource
146
+ );
147
+ const existingLabelText = normalizeThinkingText(existingLabel);
148
+ const existingDetailText = normalizeThinkingText(existingDetail);
149
+
150
+ const candidates = [
151
+ { source: "summary", label: summaryText },
152
+ { source: "bold", label: boldLabelCandidate },
153
+ { source: "detail", label: detailText },
154
+ ];
155
+
156
+ let selected = null;
157
+ if (explicitSource && explicitSource !== "generic") {
158
+ const explicitLabel = (
159
+ candidates.find((candidate) => (
160
+ candidate.source === explicitSource &&
161
+ candidate.label
162
+ ))?.label ||
163
+ existingLabelText ||
164
+ summaryText ||
165
+ boldLabelCandidate ||
166
+ detailText
167
+ );
168
+ if (explicitLabel) {
169
+ selected = { source: explicitSource, label: explicitLabel };
170
+ }
171
+ }
172
+ if (!selected) {
173
+ selected = candidates.find((candidate) => candidate.label) || null;
174
+ }
175
+ if (!selected && existingLabelText) {
176
+ const normalizedExistingLabel = normalizeLowerToken(existingLabelText);
177
+ const inferredSource =
178
+ normalizedExistingLabel === "thinking" || normalizedExistingLabel === "thinking..."
179
+ ? "generic"
180
+ : "detail";
181
+ selected = { source: inferredSource, label: existingLabelText };
182
+ }
183
+ if (!selected && includeThinking) {
184
+ selected = { source: "generic", label: GENERIC_THINKING_LABEL };
185
+ }
186
+
187
+ return {
188
+ label: selected ? selected.label : null,
189
+ detail: detailText || existingDetailText || (
190
+ selected && selected.source !== "generic" ? selected.label : null
191
+ ),
192
+ thinkingSummarySource: selected ? selected.source : null,
193
+ };
194
+ }
195
+
196
+ function isThinkingActivity(activity, category) {
197
+ const state = normalizeLowerToken(activity && activity.state);
198
+ if (state !== "thinking" && state !== "queued") return false;
199
+ if (activity && activity.tool) return false;
200
+
201
+ if (normalizeLowerToken(category) === "thinking") return true;
202
+ if (normalizeLowerToken(activity && activity.category) === "thinking") return true;
203
+ if (normalizeLowerToken(activity && activity.origin) === "thinking") return true;
204
+ if (pickThinkingSummary(activity)) return true;
205
+ if (pickThinkingDetail(activity)) return true;
206
+ return !(activity && activity.tool);
207
+ }
208
+
209
+ function shortText(text, maxChars) {
210
+ if (!text) return "";
211
+ if (text.length <= maxChars) return text;
212
+ if (maxChars <= 3) return ".".repeat(Math.max(maxChars, 0));
213
+ return `${text.slice(0, maxChars - 3)}...`;
214
+ }
215
+
216
+ function lowercaseLeadingWord(rawText) {
217
+ if (typeof rawText !== "string" || rawText.length === 0) return rawText;
218
+ const match = rawText.match(/[A-Za-z][A-Za-z0-9_-]*/);
219
+ if (!match || match.index == null) return rawText;
220
+ const start = match.index;
221
+ const token = match[0];
222
+ const end = start + token.length;
223
+ return `${rawText.slice(0, start)}${token.toLowerCase()}${rawText.slice(end)}`;
224
+ }
225
+
226
+ function collapseWhitespace(text) {
227
+ return text.replace(/\s+/g, " ").trim();
228
+ }
229
+
230
+ function redactSecrets(rawText) {
231
+ if (!rawText) return "";
232
+ let text = String(rawText);
233
+
234
+ text = text.replace(
235
+ new RegExp(`([?&]${REDACT_QUERY_KEYS}=)[^&#\\s]+`, "gi"),
236
+ "$1[redacted]",
237
+ );
238
+ text = text.replace(
239
+ /((?:api[_-]?key|token|password|secret)\s*[=:]\s*)([^,\s"'`]+)/gi,
240
+ "$1[redacted]",
241
+ );
242
+ text = text.replace(/(authorization\s*:\s*bearer\s+)[^\s"'`]+/gi, "$1[redacted]");
243
+ text = text.replace(/\bBearer\s+[A-Za-z0-9._-]{8,}\b/g, "Bearer [redacted]");
244
+ text = text.replace(/\b(sk-[A-Za-z0-9]{16,}|ghp_[A-Za-z0-9]{20,}|xox[baprs]-[A-Za-z0-9-]{10,})\b/g, "[redacted]");
245
+
246
+ return text;
247
+ }
248
+
249
+ function sanitizeText(rawText, maxChars) {
250
+ const redacted = redactSecrets(rawText);
251
+ const collapsed = collapseWhitespace(redacted);
252
+ return shortText(collapsed, maxChars);
253
+ }
254
+
255
+ function hostFromUrl(urlString) {
256
+ if (!urlString) return null;
257
+ try {
258
+ const parsed = new URL(urlString);
259
+ return parsed.host || null;
260
+ } catch {
261
+ return null;
262
+ }
263
+ }
264
+
265
+ function extractFirstUrl(text) {
266
+ if (!text) return null;
267
+ const match = text.match(/https?:\/\/[^\s"'`]+/i);
268
+ return match ? match[0] : null;
269
+ }
270
+
271
+ function extractBrowserQueryFromCommand(command) {
272
+ const match = command.match(/[?&]q=([^&"'`\s]+)/i);
273
+ if (!match) return null;
274
+ try {
275
+ return decodeURIComponent(match[1].replace(/\+/g, " "));
276
+ } catch {
277
+ return match[1].replace(/\+/g, " ");
278
+ }
279
+ }
280
+
281
+ function stripQuotes(value) {
282
+ if (!value) return value;
283
+ return String(value).replace(/^['"]+|['"]+$/g, "");
284
+ }
285
+
286
+ function escapeRegex(value) {
287
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
288
+ }
289
+
290
+ function filenameFromPath(pathValue) {
291
+ if (!pathValue || typeof pathValue !== "string") return null;
292
+ const cleaned = stripQuotes(pathValue.trim());
293
+ if (!cleaned) return null;
294
+ const normalized = cleaned.replace(/[;,)]+$/g, "");
295
+ if (!normalized) return null;
296
+ if (isNullishToken(normalized)) return null;
297
+ // Ignore shell variable/subshell paths like "$f", "${file}", or "$(mktemp)".
298
+ if (/\$[({]?[A-Za-z_][A-Za-z0-9_]*[)}]?/.test(normalized) || /\$\(.+\)/.test(normalized)) {
299
+ return null;
300
+ }
301
+ if (/^(?:\/dev\/(?:null|stdout|stderr)|nul)$/i.test(normalized)) return null;
302
+ return path.basename(normalized);
303
+ }
304
+
305
+ function pickMktempTemplatePath(rawArgs) {
306
+ if (!rawArgs || typeof rawArgs !== "string") return null;
307
+ const tokens = rawArgs
308
+ .match(/"[^"]*"|'[^']*'|[^\s]+/g)
309
+ ?.map((token) => stripQuotes(token).trim())
310
+ ?.filter(Boolean);
311
+ if (!tokens || tokens.length === 0) return null;
312
+
313
+ for (let index = tokens.length - 1; index >= 0; index -= 1) {
314
+ const token = tokens[index];
315
+ if (!token || token === "mktemp" || token.startsWith("-")) continue;
316
+ if (isNullishToken(token)) continue;
317
+ return token;
318
+ }
319
+ return null;
320
+ }
321
+
322
+ function extractMktempBindings(command) {
323
+ if (!command || typeof command !== "string") return [];
324
+ const out = [];
325
+ const regex = /([A-Za-z_][A-Za-z0-9_]*)\s*=\s*\$\(\s*mktemp\b([^)]*)\)/g;
326
+ let match;
327
+ while ((match = regex.exec(command)) !== null) {
328
+ const varName = match[1];
329
+ const templatePath = pickMktempTemplatePath(match[2]);
330
+ const fileName = filenameFromPath(templatePath);
331
+ if (!fileName) continue;
332
+ out.push({ varName, fileName });
333
+ }
334
+ return out;
335
+ }
336
+
337
+ function commandRefsVarWithRedirect(command, varName, operator) {
338
+ if (!command || !varName) return false;
339
+ const varRef = `\\$\\{?${escapeRegex(varName)}\\}?`;
340
+ const op = escapeRegex(operator);
341
+ const regex = new RegExp(`(?:^|\\s)${op}\\s*(?:["']?${varRef}["']?)`);
342
+ return regex.test(command);
343
+ }
344
+
345
+ function commandReadsVarWithCat(command, varName) {
346
+ if (!command || !varName) return false;
347
+ const varRef = `\\$\\{?${escapeRegex(varName)}\\}?`;
348
+ const regex = new RegExp(`(?:^|\\s)cat\\s+(?:["']?${varRef}["']?)`);
349
+ return regex.test(command);
350
+ }
351
+
352
+ function categoryFromToolName(lowName) {
353
+ if (lowName.startsWith("browser") || lowName === "web" || lowName === "web.search") return "browser";
354
+ if (lowName === "read" || lowName === "write" || lowName === "edit" || lowName.startsWith("fs.")) return "filesystem";
355
+ if (lowName === "search" || lowName.startsWith("vector") || lowName === "grep" || lowName === "find") return "search";
356
+ if (lowName === "exec" || lowName === "bash" || lowName.startsWith("shell")) return "terminal";
357
+ return "generic";
358
+ }
359
+
360
+ function intentFromToolName(lowName, args) {
361
+ const query = pickString(args, ["query", "q", "term", "search"]);
362
+
363
+ switch (lowName) {
364
+ case "read":
365
+ case "fs.read":
366
+ return "fs.read";
367
+ case "write":
368
+ case "apply_patch":
369
+ case "fs.write":
370
+ return "fs.write";
371
+ case "edit":
372
+ case "fs.edit":
373
+ return "fs.edit";
374
+ case "search":
375
+ case "grep":
376
+ case "find":
377
+ return "search.files";
378
+ case "browser.search":
379
+ case "web.search":
380
+ return "search.web";
381
+ case "browser.click":
382
+ case "browser.navigate":
383
+ return "browser.navigate";
384
+ case "browser.fill":
385
+ return "browser.fill";
386
+ case "browser":
387
+ case "web":
388
+ return query ? "search.web" : "browser.browse";
389
+ case "exec":
390
+ case "bash":
391
+ return "terminal.exec";
392
+ case "git":
393
+ return "terminal.git";
394
+ case "llm_task":
395
+ return "agent.subtask";
396
+ case "agent_send":
397
+ return "agent.coordinate";
398
+ case "message":
399
+ return "message.send";
400
+ case "sessions_list":
401
+ case "sessions_read":
402
+ case "session_status":
403
+ return "session.manage";
404
+ case "canvas":
405
+ return "canvas.edit";
406
+ case "fetch":
407
+ return "network.fetch";
408
+ default:
409
+ break;
410
+ }
411
+
412
+ if (lowName.startsWith("browser")) {
413
+ if (lowName.includes("fill")) return "browser.fill";
414
+ if (lowName.includes("click") || lowName.includes("navigate")) return "browser.navigate";
415
+ if (lowName.includes("search")) return "search.web";
416
+ return "browser.browse";
417
+ }
418
+ if (lowName.startsWith("web")) {
419
+ return lowName.includes("search") ? "search.web" : "browser.browse";
420
+ }
421
+ if (lowName.includes("search")) {
422
+ return lowName.includes("web") || lowName.includes("browser")
423
+ ? "search.web"
424
+ : "search.files";
425
+ }
426
+ if (lowName.startsWith("fs.")) {
427
+ if (lowName.includes("read")) return "fs.read";
428
+ if (lowName.includes("edit")) return "fs.edit";
429
+ return "fs.write";
430
+ }
431
+ if (lowName.startsWith("session")) return "session.manage";
432
+ if (lowName.startsWith("http")) return "network.fetch";
433
+ if (lowName.startsWith("git")) return "terminal.git";
434
+ if (lowName.startsWith("shell")) return "terminal.exec";
435
+ return "generic";
436
+ }
437
+
438
+ function isExplanatoryThinkingLabel(label) {
439
+ const normalizedLabel = normalizeThinkingText(label);
440
+ if (!normalizedLabel) return false;
441
+ const normalizedToken = collapseWhitespace(normalizedLabel)
442
+ .toLowerCase()
443
+ .replace(/\u2026/g, "...");
444
+ return normalizedToken !== "thinking" && normalizedToken !== "thinking...";
445
+ }
446
+
447
+ function labelFromExecCommand(command) {
448
+ const raw = command ? String(command).trim() : "";
449
+ if (!raw) {
450
+ return {
451
+ label: "Running a command...",
452
+ category: "terminal",
453
+ intent: "terminal.exec",
454
+ };
455
+ }
456
+
457
+ if (raw.includes("agent-browser")) {
458
+ const query = extractBrowserQueryFromCommand(raw);
459
+ if (query) {
460
+ return {
461
+ label: `Searching "${shortText(sanitizeText(query, TOOL_PREVIEW_CHARS), TOOL_PREVIEW_CHARS)}"...`,
462
+ category: "browser",
463
+ intent: "search.web",
464
+ };
465
+ }
466
+ const browserUrl = extractFirstUrl(raw);
467
+ if (browserUrl) {
468
+ const host = hostFromUrl(browserUrl);
469
+ return {
470
+ label: host ? `Browsing ${host}...` : "Using browser...",
471
+ category: "browser",
472
+ intent: "browser.browse",
473
+ };
474
+ }
475
+ return {
476
+ label: "Using browser...",
477
+ category: "browser",
478
+ intent: "browser.browse",
479
+ };
480
+ }
481
+
482
+ if (/(^|\s)(curl|wget)\b/i.test(raw) || /https?:\/\//i.test(raw)) {
483
+ const url = extractFirstUrl(raw);
484
+ const host = hostFromUrl(url);
485
+ if (host) {
486
+ return {
487
+ label: `Fetching from ${host}...`,
488
+ category: "network",
489
+ intent: "network.fetch",
490
+ };
491
+ }
492
+ return {
493
+ label: "Fetching data...",
494
+ category: "network",
495
+ intent: "network.fetch",
496
+ };
497
+ }
498
+
499
+ const appendMatch = raw.match(/(?:^|\s)>>\s*([^\s]+)/);
500
+ if (appendMatch) {
501
+ const fileName = filenameFromPath(appendMatch[1]);
502
+ if (fileName) {
503
+ return {
504
+ label: `Appending to ${fileName}...`,
505
+ category: "filesystem",
506
+ intent: "fs.write",
507
+ };
508
+ }
509
+ }
510
+
511
+ const writeMatch = raw.match(/(?:^|\s)>\s*([^\s]+)/);
512
+ if (writeMatch) {
513
+ const fileName = filenameFromPath(writeMatch[1]);
514
+ if (fileName) {
515
+ return {
516
+ label: `Writing ${fileName}...`,
517
+ category: "filesystem",
518
+ intent: "fs.write",
519
+ };
520
+ }
521
+ }
522
+
523
+ const mktempBindings = extractMktempBindings(raw);
524
+ for (const binding of mktempBindings) {
525
+ if (commandRefsVarWithRedirect(raw, binding.varName, ">>")) {
526
+ return {
527
+ label: `Appending to ${binding.fileName}...`,
528
+ category: "filesystem",
529
+ intent: "fs.write",
530
+ };
531
+ }
532
+ }
533
+ for (const binding of mktempBindings) {
534
+ if (commandRefsVarWithRedirect(raw, binding.varName, ">")) {
535
+ return {
536
+ label: `Writing ${binding.fileName}...`,
537
+ category: "filesystem",
538
+ intent: "fs.write",
539
+ };
540
+ }
541
+ }
542
+ for (const binding of mktempBindings) {
543
+ if (commandReadsVarWithCat(raw, binding.varName)) {
544
+ return {
545
+ label: `Reading ${binding.fileName}...`,
546
+ category: "filesystem",
547
+ intent: "fs.read",
548
+ };
549
+ }
550
+ }
551
+
552
+ const catMatch = raw.match(/^cat\s+([^\s>]+)/);
553
+ if (catMatch) {
554
+ const fileName = filenameFromPath(catMatch[1]);
555
+ if (fileName) {
556
+ return {
557
+ label: `Reading ${fileName}...`,
558
+ category: "filesystem",
559
+ intent: "fs.read",
560
+ };
561
+ }
562
+ }
563
+
564
+ if (/^(grep|rg|find)\b/.test(raw)) {
565
+ return {
566
+ label: "Searching files...",
567
+ category: "search",
568
+ intent: "search.files",
569
+ };
570
+ }
571
+ if (/\bgit\s+/.test(raw)) {
572
+ return {
573
+ label: "Running git...",
574
+ category: "terminal",
575
+ intent: "terminal.git",
576
+ };
577
+ }
578
+
579
+ return {
580
+ label: `Running: ${shortText(sanitizeText(raw, TOOL_PREVIEW_CHARS), TOOL_PREVIEW_CHARS)}`,
581
+ category: "terminal",
582
+ intent: "terminal.exec",
583
+ };
584
+ }
585
+
586
+ function mapToolLabel(toolName, activityPath, args, options) {
587
+ const maxLabelChars = options.maxLabelChars;
588
+ const lowName = String(toolName || "").toLowerCase();
589
+ const rawPathValue = asString(activityPath) || pickString(args, [
590
+ "path",
591
+ "filePath",
592
+ "file_path",
593
+ "filepath",
594
+ "file",
595
+ "target",
596
+ "outputPath",
597
+ "output_path",
598
+ "output",
599
+ "destination",
600
+ "dest",
601
+ ]);
602
+ const pathValue = rawPathValue && !isNullishToken(rawPathValue) ? rawPathValue : null;
603
+ const fileName = filenameFromPath(pathValue);
604
+ const query = pickString(args, ["query", "q", "term", "search"]);
605
+ const url = pickString(args, ["url", "href", "uri"]);
606
+ const command = pickString(args, ["command", "cmd", "shell"]);
607
+
608
+ switch (lowName) {
609
+ case "write":
610
+ case "apply_patch":
611
+ case "fs.write":
612
+ return {
613
+ label: `Writing ${fileName || "file"}...`,
614
+ detail: pathValue || command || null,
615
+ category: "filesystem",
616
+ intent: "fs.write",
617
+ };
618
+ case "read":
619
+ case "fs.read":
620
+ return {
621
+ label: `Reading ${fileName || "file"}...`,
622
+ detail: pathValue || command || null,
623
+ category: "filesystem",
624
+ intent: "fs.read",
625
+ };
626
+ case "edit":
627
+ case "fs.edit":
628
+ return {
629
+ label: `Editing ${fileName || "file"}...`,
630
+ detail: pathValue || command || null,
631
+ category: "filesystem",
632
+ intent: "fs.edit",
633
+ };
634
+ case "search":
635
+ if (query) {
636
+ const shortQuery = shortText(sanitizeText(query, TOOL_PREVIEW_CHARS), TOOL_PREVIEW_CHARS);
637
+ return {
638
+ label: `Searching for "${shortQuery}"...`,
639
+ detail: query,
640
+ category: "search",
641
+ intent: "search.files",
642
+ };
643
+ }
644
+ return {
645
+ label: "Searching files...",
646
+ detail: pathValue || null,
647
+ category: "search",
648
+ intent: "search.files",
649
+ };
650
+ case "bash":
651
+ case "exec": {
652
+ const fromCommand = labelFromExecCommand(command);
653
+ return {
654
+ label: fromCommand.label,
655
+ detail: command || pathValue || null,
656
+ category: fromCommand.category,
657
+ intent: fromCommand.intent,
658
+ };
659
+ }
660
+ case "browser.search":
661
+ case "web.search":
662
+ if (query) {
663
+ const shortQuery = shortText(sanitizeText(query, TOOL_PREVIEW_CHARS), TOOL_PREVIEW_CHARS);
664
+ return {
665
+ label: `Searching the web for "${shortQuery}"...`,
666
+ detail: query,
667
+ category: "browser",
668
+ intent: "search.web",
669
+ };
670
+ }
671
+ if (url) {
672
+ return {
673
+ label: "Searching the web...",
674
+ detail: url,
675
+ category: "browser",
676
+ intent: "search.web",
677
+ };
678
+ }
679
+ return {
680
+ label: "Searching the web...",
681
+ detail: null,
682
+ category: "browser",
683
+ intent: "search.web",
684
+ };
685
+ case "browser":
686
+ case "web":
687
+ if (query) {
688
+ const shortQuery = shortText(sanitizeText(query, TOOL_PREVIEW_CHARS), TOOL_PREVIEW_CHARS);
689
+ return {
690
+ label: `Searching the web for "${shortQuery}"...`,
691
+ detail: query,
692
+ category: "browser",
693
+ intent: "search.web",
694
+ };
695
+ }
696
+ if (url) {
697
+ return {
698
+ label: "Browsing the web...",
699
+ detail: url,
700
+ category: "browser",
701
+ intent: "browser.browse",
702
+ };
703
+ }
704
+ return {
705
+ label: "Browsing the web...",
706
+ detail: null,
707
+ category: "browser",
708
+ intent: "browser.browse",
709
+ };
710
+ case "browser.click":
711
+ return {
712
+ label: "Navigating a webpage...",
713
+ detail: url || null,
714
+ category: "browser",
715
+ intent: "browser.navigate",
716
+ };
717
+ case "browser.fill":
718
+ return {
719
+ label: "Filling out a form...",
720
+ detail: url || null,
721
+ category: "browser",
722
+ intent: "browser.fill",
723
+ };
724
+ case "browser.navigate":
725
+ return {
726
+ label: "Opening a webpage...",
727
+ detail: url || null,
728
+ category: "browser",
729
+ intent: "browser.navigate",
730
+ };
731
+ case "llm_task":
732
+ return {
733
+ label: "Running a sub-task...",
734
+ detail: null,
735
+ category: "generic",
736
+ intent: "agent.subtask",
737
+ };
738
+ case "agent_send":
739
+ return {
740
+ label: "Coordinating with another agent...",
741
+ detail: null,
742
+ category: "generic",
743
+ intent: "agent.coordinate",
744
+ };
745
+ case "message":
746
+ return {
747
+ label: "Sending a message...",
748
+ detail: null,
749
+ category: "generic",
750
+ intent: "message.send",
751
+ };
752
+ case "sessions_list":
753
+ case "sessions_read":
754
+ case "session_status":
755
+ return {
756
+ label: "Checking sessions...",
757
+ detail: null,
758
+ category: "generic",
759
+ intent: "session.manage",
760
+ };
761
+ case "canvas":
762
+ return {
763
+ label: "Working on canvas...",
764
+ detail: null,
765
+ category: "generic",
766
+ intent: "canvas.edit",
767
+ };
768
+ default:
769
+ if (fileName) {
770
+ return {
771
+ label: `${toolName} ${fileName}...`,
772
+ detail: pathValue || null,
773
+ category: categoryFromToolName(lowName),
774
+ intent: intentFromToolName(lowName, args),
775
+ };
776
+ }
777
+ return {
778
+ label: `Using ${toolName}...`,
779
+ detail: query || url || command || null,
780
+ category: categoryFromToolName(lowName),
781
+ intent: intentFromToolName(lowName, args),
782
+ };
783
+ }
784
+ }
785
+
786
+ function normalizePhase(phase, state) {
787
+ const p = typeof phase === "string" ? phase.toLowerCase() : "";
788
+ if (p === "start" || p === "update" || p === "end") return p;
789
+ if (
790
+ p === "error" ||
791
+ p === "complete" ||
792
+ p === "completed" ||
793
+ p === "done" ||
794
+ p === "result" ||
795
+ p === "failed" ||
796
+ p === "finish" ||
797
+ p === "finished"
798
+ ) {
799
+ return "end";
800
+ }
801
+
802
+ const s = typeof state === "string" ? state.toLowerCase() : "";
803
+ if (s === "idle" || s === "error" || s === "done" || s === "cancelled" || s === "canceled") return "end";
804
+ if (s === "thinking" || s === "queued") return "update";
805
+ return "update";
806
+ }
807
+
808
+ function normalizeRunKey(runId) {
809
+ if (typeof runId === "string" && runId.trim()) return runId.trim();
810
+ return GLOBAL_RUN_KEY;
811
+ }
812
+
813
+ function sanitizeIdPart(value) {
814
+ return String(value || "")
815
+ .trim()
816
+ .replace(/[^a-zA-Z0-9._:-]+/g, "-")
817
+ .replace(/-+/g, "-")
818
+ .replace(/^-|-$/g, "");
819
+ }
820
+
821
+ function createActivityStatusAdapter(opts) {
822
+ const options = opts || {};
823
+ const enabled = options.enabled !== false;
824
+ const includeThinking = options.includeThinking !== false;
825
+ const maxLabelChars =
826
+ Number.isFinite(options.maxLabelChars) && options.maxLabelChars > 0
827
+ ? Math.floor(options.maxLabelChars)
828
+ : DEFAULT_MAX_LABEL_CHARS;
829
+
830
+ /** @type {Map<string, {seq: number, toolStartCount: number, currentActivityId: string|null, toolContextByActivityId: Map<string, {label: string, detail: string|null, category: string|null, intent: string|null}>}>} */
831
+ const runStates = new Map();
832
+
833
+ function getRunState(runKey) {
834
+ let state = runStates.get(runKey);
835
+ if (!state) {
836
+ state = {
837
+ seq: 0,
838
+ toolStartCount: 0,
839
+ currentActivityId: null,
840
+ toolContextByActivityId: new Map(),
841
+ };
842
+ runStates.set(runKey, state);
843
+ }
844
+ return state;
845
+ }
846
+
847
+ function reset() {
848
+ runStates.clear();
849
+ }
850
+
851
+ function augmentActivity(rawActivity) {
852
+ if (!isObject(rawActivity)) return rawActivity;
853
+
854
+ const activity = { ...rawActivity };
855
+ const runKey = normalizeRunKey(activity.runId);
856
+ const runState = getRunState(runKey);
857
+ const phase = normalizePhase(activity.phase, activity.state);
858
+
859
+ runState.seq += 1;
860
+ const seq = Number.isFinite(activity.seq) ? Math.floor(activity.seq) : runState.seq;
861
+
862
+ let activityId = asString(activity.activityId) && activity.activityId.trim()
863
+ ? activity.activityId.trim()
864
+ : null;
865
+
866
+ if (!activityId) {
867
+ if (phase === "start") {
868
+ if (activity.origin === "tool" || activity.tool) {
869
+ runState.toolStartCount += 1;
870
+ const toolName = sanitizeIdPart(activity.tool || "tool");
871
+ activityId = `${runKey}:tool:${toolName || "tool"}:${runState.toolStartCount}`;
872
+ } else if (activity.origin === "lifecycle") {
873
+ activityId = `${runKey}:lifecycle`;
874
+ } else {
875
+ activityId = `${runKey}:activity:${runState.seq}`;
876
+ }
877
+ } else {
878
+ activityId = runState.currentActivityId || `${runKey}:activity`;
879
+ }
880
+ }
881
+
882
+ if (phase === "start" || phase === "update") {
883
+ runState.currentActivityId = activityId;
884
+ }
885
+ if (phase === "end") {
886
+ runState.currentActivityId = null;
887
+ }
888
+
889
+ let label = asString(activity.label);
890
+ let detail = asString(activity.detail);
891
+ let category = asString(activity.category);
892
+ let thinkingSummarySource = null;
893
+ let suppressThinkingContent = false;
894
+ const args = normalizeArgs(activity) || {};
895
+ const previousToolContext = activity.tool && activityId
896
+ ? runState.toolContextByActivityId.get(activityId)
897
+ : null;
898
+ const hasCurrentToolContext =
899
+ !!asString(activity.path) ||
900
+ !!pickString(args, [
901
+ "path",
902
+ "filePath",
903
+ "file_path",
904
+ "filepath",
905
+ "file",
906
+ "target",
907
+ "outputPath",
908
+ "output_path",
909
+ "output",
910
+ "destination",
911
+ "dest",
912
+ "query",
913
+ "q",
914
+ "term",
915
+ "search",
916
+ "url",
917
+ "href",
918
+ "uri",
919
+ "command",
920
+ "cmd",
921
+ "shell",
922
+ ]);
923
+ const mappedTool = activity.tool
924
+ ? mapToolLabel(activity.tool, activity.path, args, {
925
+ maxLabelChars,
926
+ })
927
+ : null;
928
+ const isThinking = isThinkingActivity(activity, category);
929
+
930
+ if (isThinking) {
931
+ if (!category) category = "thinking";
932
+ const resolvedThinking = resolveThinkingContent(
933
+ activity,
934
+ label,
935
+ detail,
936
+ includeThinking,
937
+ );
938
+ thinkingSummarySource = resolvedThinking.thinkingSummarySource;
939
+ if (!label) {
940
+ label = resolvedThinking.label;
941
+ }
942
+ if (!detail) {
943
+ detail = resolvedThinking.detail;
944
+ }
945
+
946
+ if (!includeThinking) {
947
+ suppressThinkingContent = true;
948
+ label = null;
949
+ detail = null;
950
+ thinkingSummarySource = null;
951
+ }
952
+ } else if (!label && activity.tool) {
953
+ if (previousToolContext && !hasCurrentToolContext) {
954
+ label = previousToolContext.label;
955
+ if (!detail) detail = previousToolContext.detail;
956
+ if (!category) category = previousToolContext.category;
957
+ }
958
+ if (!label) {
959
+ const mapped = mappedTool;
960
+ label = mapped.label;
961
+ if (!detail) detail = mapped.detail;
962
+ if (!category) category = mapped.category;
963
+ }
964
+ }
965
+
966
+ let intent = normalizeIntent(activity.intent);
967
+ if (normalizeLowerToken(activity && activity.state) === "queued") {
968
+ intent = "queued";
969
+ } else if (activity.tool) {
970
+ intent = (
971
+ (!hasCurrentToolContext && previousToolContext && previousToolContext.intent) ||
972
+ (mappedTool && mappedTool.intent) ||
973
+ intentFromToolName(normalizeLowerToken(activity.tool), args)
974
+ );
975
+ } else if (isThinking) {
976
+ intent = isExplanatoryThinkingLabel(label) ? "thinking_summary" : "thinking";
977
+ }
978
+
979
+ if (activity.tool && activityId && label) {
980
+ runState.toolContextByActivityId.set(activityId, {
981
+ label,
982
+ detail: detail || null,
983
+ category: category || null,
984
+ intent: intent || null,
985
+ });
986
+ }
987
+
988
+ const result = {
989
+ ...activity,
990
+ activityId,
991
+ seq,
992
+ phase,
993
+ };
994
+
995
+ if (suppressThinkingContent) {
996
+ delete result.label;
997
+ delete result.detail;
998
+ }
999
+
1000
+ if (runKey !== GLOBAL_RUN_KEY && !result.runId) {
1001
+ result.runId = runKey;
1002
+ }
1003
+
1004
+ if (typeof activity.isError === "boolean") {
1005
+ result.isError = activity.isError;
1006
+ }
1007
+ if (Number.isFinite(activity.exitCode)) {
1008
+ result.exitCode = Math.trunc(activity.exitCode);
1009
+ }
1010
+ if (Number.isFinite(activity.durationMs) && activity.durationMs >= 0) {
1011
+ result.durationMs = Math.trunc(activity.durationMs);
1012
+ }
1013
+
1014
+ if (enabled) {
1015
+ if (label) result.label = sanitizeText(lowercaseLeadingWord(label), maxLabelChars);
1016
+ if (detail) result.detail = sanitizeText(detail, Math.max(maxLabelChars, 200));
1017
+ if (category) result.category = sanitizeIdPart(category.toLowerCase()) || "generic";
1018
+ if (thinkingSummarySource) result.thinkingSummarySource = thinkingSummarySource;
1019
+ const resultSummary = asString(activity.resultSummary);
1020
+ if (resultSummary) {
1021
+ result.resultSummary = sanitizeText(resultSummary, Math.max(maxLabelChars, 200));
1022
+ }
1023
+ }
1024
+ if (intent) {
1025
+ result.intent = intent;
1026
+ } else {
1027
+ delete result.intent;
1028
+ }
1029
+
1030
+ return result;
1031
+ }
1032
+
1033
+ return {
1034
+ augmentActivity,
1035
+ reset,
1036
+ };
1037
+ }
1038
+
1039
+ export {
1040
+ createActivityStatusAdapter,
1041
+ };