remcodex 0.1.0-beta.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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +331 -0
  3. package/dist/server/src/app.js +186 -0
  4. package/dist/server/src/cli.js +270 -0
  5. package/dist/server/src/controllers/codex-options.controller.js +199 -0
  6. package/dist/server/src/controllers/message.controller.js +21 -0
  7. package/dist/server/src/controllers/project.controller.js +44 -0
  8. package/dist/server/src/controllers/session.controller.js +175 -0
  9. package/dist/server/src/db/client.js +10 -0
  10. package/dist/server/src/db/migrations.js +32 -0
  11. package/dist/server/src/gateways/ws.gateway.js +60 -0
  12. package/dist/server/src/services/codex-app-server-runner.js +363 -0
  13. package/dist/server/src/services/codex-exec-runner.js +147 -0
  14. package/dist/server/src/services/codex-rollout-sync.js +977 -0
  15. package/dist/server/src/services/codex-runner.js +11 -0
  16. package/dist/server/src/services/codex-stream-events.js +478 -0
  17. package/dist/server/src/services/event-store.js +328 -0
  18. package/dist/server/src/services/project-manager.js +130 -0
  19. package/dist/server/src/services/pty-runner.js +72 -0
  20. package/dist/server/src/services/session-manager.js +1586 -0
  21. package/dist/server/src/services/session-timeline-service.js +181 -0
  22. package/dist/server/src/types/codex-launch.js +2 -0
  23. package/dist/server/src/types/models.js +37 -0
  24. package/dist/server/src/utils/ansi.js +143 -0
  25. package/dist/server/src/utils/codex-launch.js +102 -0
  26. package/dist/server/src/utils/codex-quota.js +179 -0
  27. package/dist/server/src/utils/codex-status.js +163 -0
  28. package/dist/server/src/utils/codex-ui-options.js +114 -0
  29. package/dist/server/src/utils/command.js +46 -0
  30. package/dist/server/src/utils/errors.js +16 -0
  31. package/dist/server/src/utils/ids.js +7 -0
  32. package/dist/server/src/utils/node-pty.js +29 -0
  33. package/package.json +36 -0
  34. package/scripts/fix-node-pty-helper.js +36 -0
  35. package/web/api.js +175 -0
  36. package/web/app.js +8082 -0
  37. package/web/components/composer.js +627 -0
  38. package/web/components/session-workbench.js +173 -0
  39. package/web/i18n/index.js +171 -0
  40. package/web/i18n/locales/de.js +50 -0
  41. package/web/i18n/locales/en.js +320 -0
  42. package/web/i18n/locales/es.js +50 -0
  43. package/web/i18n/locales/fr.js +50 -0
  44. package/web/i18n/locales/ja.js +50 -0
  45. package/web/i18n/locales/ko.js +50 -0
  46. package/web/i18n/locales/pt-BR.js +50 -0
  47. package/web/i18n/locales/ru.js +50 -0
  48. package/web/i18n/locales/zh-CN.js +320 -0
  49. package/web/i18n/locales/zh-Hant.js +53 -0
  50. package/web/index.html +23 -0
  51. package/web/message-rich-text.js +218 -0
  52. package/web/session-command-activity.js +980 -0
  53. package/web/session-event-adapter.js +826 -0
  54. package/web/session-timeline-reducer.js +728 -0
  55. package/web/session-timeline-renderer.js +656 -0
  56. package/web/session-ws.js +31 -0
  57. package/web/styles.css +5665 -0
  58. package/web/vendor/markdown-it.js +6969 -0
@@ -0,0 +1,980 @@
1
+ import { formatInlineList, t } from "./i18n/index.js";
2
+
3
+ function stripShellWrapper(command) {
4
+ let current = String(command || "").trim();
5
+
6
+ while (true) {
7
+ const match = current.match(
8
+ /^(?:\/bin\/)?(?:zsh|bash|sh)\s+-lc\s+(['"])([\s\S]*)\1$/,
9
+ );
10
+ if (!match) {
11
+ return current;
12
+ }
13
+ current = match[2].trim();
14
+ }
15
+ }
16
+
17
+ function tokenizeShell(command) {
18
+ const source = stripShellWrapper(command);
19
+ const tokens = [];
20
+ let current = "";
21
+ let quote = "";
22
+ let escapeNext = false;
23
+
24
+ for (let index = 0; index < source.length; index += 1) {
25
+ const char = source[index];
26
+
27
+ if (escapeNext) {
28
+ current += char;
29
+ escapeNext = false;
30
+ continue;
31
+ }
32
+
33
+ if (char === "\\") {
34
+ escapeNext = true;
35
+ continue;
36
+ }
37
+
38
+ if (quote) {
39
+ if (char === quote) {
40
+ quote = "";
41
+ } else {
42
+ current += char;
43
+ }
44
+ continue;
45
+ }
46
+
47
+ if (char === "'" || char === '"') {
48
+ quote = char;
49
+ continue;
50
+ }
51
+
52
+ if (/\s/.test(char)) {
53
+ if (current) {
54
+ tokens.push(current);
55
+ current = "";
56
+ }
57
+ continue;
58
+ }
59
+
60
+ current += char;
61
+ }
62
+
63
+ if (current) {
64
+ tokens.push(current);
65
+ }
66
+
67
+ return tokens;
68
+ }
69
+
70
+ function getCommandContext(command) {
71
+ const normalized = stripShellWrapper(command);
72
+ const tokens = tokenizeShell(command);
73
+ let offset = 0;
74
+
75
+ while (
76
+ offset < tokens.length &&
77
+ /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[offset] || "")
78
+ ) {
79
+ offset += 1;
80
+ }
81
+
82
+ if (tokens[offset] === "env") {
83
+ offset += 1;
84
+ while (
85
+ offset < tokens.length &&
86
+ /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[offset] || "")
87
+ ) {
88
+ offset += 1;
89
+ }
90
+ }
91
+
92
+ const primary = tokens[offset] || "";
93
+ const commandName = basename(primary);
94
+ const subcommand = commandName === "git" ? tokens[offset + 1] || "" : "";
95
+
96
+ return {
97
+ normalized,
98
+ tokens,
99
+ offset,
100
+ primary,
101
+ commandName,
102
+ subcommand,
103
+ };
104
+ }
105
+
106
+ function isOptionToken(token) {
107
+ return /^-/.test(token || "");
108
+ }
109
+
110
+ function isLikelyPathToken(token) {
111
+ if (!token || isOptionToken(token)) {
112
+ return false;
113
+ }
114
+
115
+ if (/^\d+$/.test(token)) {
116
+ return false;
117
+ }
118
+
119
+ if (
120
+ token === "|" ||
121
+ token === "||" ||
122
+ token === "&&" ||
123
+ token === ">" ||
124
+ token === ">>" ||
125
+ token === "<"
126
+ ) {
127
+ return false;
128
+ }
129
+
130
+ return true;
131
+ }
132
+
133
+ function pushUnique(list, value) {
134
+ if (!value || list.includes(value)) {
135
+ return;
136
+ }
137
+ list.push(value);
138
+ }
139
+
140
+ function basename(path) {
141
+ const value = String(path || "").replace(/\/+$/, "");
142
+ if (!value) {
143
+ return "";
144
+ }
145
+ const parts = value.split("/");
146
+ return parts[parts.length - 1] || value;
147
+ }
148
+
149
+ function extractSearchFiles(tokens, startIndex, options = {}) {
150
+ const consumingOptions = new Set(options.consumingOptions || []);
151
+ const patternOptions = new Set(options.patternOptions || []);
152
+ let patternConsumed = false;
153
+ const files = [];
154
+ let stopOptionParsing = false;
155
+
156
+ for (let index = startIndex; index < tokens.length; index += 1) {
157
+ const token = tokens[index];
158
+ if (!token) {
159
+ continue;
160
+ }
161
+
162
+ if (token === "--") {
163
+ stopOptionParsing = true;
164
+ continue;
165
+ }
166
+
167
+ if (!stopOptionParsing && isOptionToken(token)) {
168
+ const optionName = token.includes("=") ? token.split("=")[0] : token;
169
+ if (patternOptions.has(optionName)) {
170
+ patternConsumed = true;
171
+ }
172
+ if (consumingOptions.has(optionName) && !token.includes("=")) {
173
+ index += 1;
174
+ }
175
+ continue;
176
+ }
177
+
178
+ if (!patternConsumed) {
179
+ patternConsumed = true;
180
+ continue;
181
+ }
182
+ if (isLikelyPathToken(token)) {
183
+ pushUnique(files, token);
184
+ }
185
+ }
186
+
187
+ return files;
188
+ }
189
+
190
+ function extractBrowseFiles(context) {
191
+ const { primary, tokens, offset } = context;
192
+ const files = [];
193
+
194
+ if (["sed", "cat", "head", "tail", "nl", "wc", "stat"].includes(primary)) {
195
+ for (let index = tokens.length - 1; index > offset; index -= 1) {
196
+ const token = tokens[index];
197
+ if (isLikelyPathToken(token)) {
198
+ pushUnique(files, token);
199
+ break;
200
+ }
201
+ }
202
+ return files;
203
+ }
204
+
205
+ if (primary === "ls" || primary === "tree") {
206
+ for (let index = offset + 1; index < tokens.length; index += 1) {
207
+ const token = tokens[index];
208
+ if (isLikelyPathToken(token)) {
209
+ pushUnique(files, token);
210
+ }
211
+ }
212
+ return files;
213
+ }
214
+
215
+ return files;
216
+ }
217
+
218
+ function extractEditFiles(context) {
219
+ const { primary, tokens, offset } = context;
220
+ const files = [];
221
+
222
+ if (primary === "touch") {
223
+ for (let index = offset + 1; index < tokens.length; index += 1) {
224
+ const token = tokens[index];
225
+ if (isLikelyPathToken(token)) {
226
+ pushUnique(files, token);
227
+ }
228
+ }
229
+ return files;
230
+ }
231
+
232
+ if (primary === "sed" && tokens.includes("-i")) {
233
+ for (let index = tokens.length - 1; index > offset; index -= 1) {
234
+ const token = tokens[index];
235
+ if (isLikelyPathToken(token)) {
236
+ pushUnique(files, token);
237
+ break;
238
+ }
239
+ }
240
+ return files;
241
+ }
242
+
243
+ if (primary === "apply_patch") {
244
+ return files;
245
+ }
246
+
247
+ return files;
248
+ }
249
+
250
+ function countPatchStatsFromText(text) {
251
+ const lines = String(text || "").split("\n");
252
+ let added = 0;
253
+ let removed = 0;
254
+
255
+ lines.forEach((line) => {
256
+ if (line.startsWith("+++")) {
257
+ return;
258
+ }
259
+ if (line.startsWith("---")) {
260
+ return;
261
+ }
262
+ if (line.startsWith("+")) {
263
+ added += 1;
264
+ return;
265
+ }
266
+ if (line.startsWith("-")) {
267
+ removed += 1;
268
+ }
269
+ });
270
+
271
+ return { added, removed };
272
+ }
273
+
274
+ function collectPatchFileStats(item) {
275
+ const changes =
276
+ item?.changes && typeof item.changes === "object" ? item.changes : {};
277
+ const fileStats = [];
278
+
279
+ Object.entries(changes).forEach(([path, change]) => {
280
+ const added = Number(change?.added || 0);
281
+ const removed = Number(change?.removed || 0);
282
+ fileStats.push({
283
+ path,
284
+ added: Number.isFinite(added) ? added : 0,
285
+ removed: Number.isFinite(removed) ? removed : 0,
286
+ });
287
+ });
288
+
289
+ if (fileStats.length > 0) {
290
+ return fileStats;
291
+ }
292
+
293
+ const patchText = String(item?.patchText || "");
294
+ if (!patchText) {
295
+ return [];
296
+ }
297
+
298
+ const fileMap = new Map();
299
+ let currentPath = null;
300
+
301
+ function ensureFile(path) {
302
+ const normalizedPath = String(path || "").trim();
303
+ if (!normalizedPath || normalizedPath === "/dev/null") {
304
+ return null;
305
+ }
306
+ const existing = fileMap.get(normalizedPath) || {
307
+ path: normalizedPath,
308
+ added: 0,
309
+ removed: 0,
310
+ };
311
+ fileMap.set(normalizedPath, existing);
312
+ return existing;
313
+ }
314
+
315
+ patchText.split("\n").forEach((line) => {
316
+ const diffMatch = line.match(/^diff --git a\/(.+?) b\/(.+)$/);
317
+ if (diffMatch) {
318
+ currentPath = diffMatch[2];
319
+ ensureFile(currentPath);
320
+ return;
321
+ }
322
+
323
+ const nextFileMatch = line.match(/^\+\+\+ (?:b\/)?(.+)$/);
324
+ if (nextFileMatch && nextFileMatch[1] !== "/dev/null") {
325
+ currentPath = nextFileMatch[1];
326
+ ensureFile(currentPath);
327
+ return;
328
+ }
329
+
330
+ if (line.startsWith("@@")) {
331
+ ensureFile(currentPath);
332
+ return;
333
+ }
334
+
335
+ if (line.startsWith("+") && !line.startsWith("+++")) {
336
+ const current = ensureFile(currentPath);
337
+ if (current) {
338
+ current.added += 1;
339
+ }
340
+ return;
341
+ }
342
+
343
+ if (line.startsWith("-") && !line.startsWith("---")) {
344
+ const current = ensureFile(currentPath);
345
+ if (current) {
346
+ current.removed += 1;
347
+ }
348
+ }
349
+ });
350
+
351
+ if (fileMap.size === 0) {
352
+ currentPath = null;
353
+ patchText.split("\n").forEach((line) => {
354
+ const patchFileMatch = line.match(/^\*\*\* (?:Update|Add|Delete) File: (.+)$/);
355
+ if (patchFileMatch) {
356
+ currentPath = patchFileMatch[1].trim();
357
+ ensureFile(currentPath);
358
+ return;
359
+ }
360
+
361
+ if (!currentPath) {
362
+ return;
363
+ }
364
+
365
+ if (line.startsWith("*** ")) {
366
+ currentPath = null;
367
+ return;
368
+ }
369
+
370
+ if (line.startsWith("+")) {
371
+ const current = ensureFile(currentPath);
372
+ if (current) {
373
+ current.added += 1;
374
+ }
375
+ return;
376
+ }
377
+
378
+ if (line.startsWith("-")) {
379
+ const current = ensureFile(currentPath);
380
+ if (current) {
381
+ current.removed += 1;
382
+ }
383
+ }
384
+ });
385
+ }
386
+
387
+ return Array.from(fileMap.values());
388
+ }
389
+
390
+ function collectPatchFileStatsFromOutput(item) {
391
+ const output = String(item?.output || item?.stdout || "");
392
+ if (!output) {
393
+ return [];
394
+ }
395
+
396
+ const fileMap = new Map();
397
+ const lines = output.split("\n");
398
+ lines.forEach((line) => {
399
+ const match = line.match(/^[AMD]\s+(.+)$/);
400
+ if (!match) {
401
+ return;
402
+ }
403
+ const path = match[1].trim();
404
+ if (!path) {
405
+ return;
406
+ }
407
+ fileMap.set(path, {
408
+ path,
409
+ added: 0,
410
+ removed: 0,
411
+ });
412
+ });
413
+
414
+ return Array.from(fileMap.values());
415
+ }
416
+
417
+ function collectFileStatsForItem(item, classification) {
418
+ if (item?.type === "patch" || classification.kind === "edit") {
419
+ const fileStats = collectPatchFileStats(item);
420
+ if (fileStats.length > 0) {
421
+ return fileStats;
422
+ }
423
+ const outputFileStats = collectPatchFileStatsFromOutput(item);
424
+ if (outputFileStats.length > 0) {
425
+ return outputFileStats;
426
+ }
427
+ }
428
+
429
+ return (classification.files || []).map((path) => ({
430
+ path,
431
+ added: 0,
432
+ removed: 0,
433
+ }));
434
+ }
435
+
436
+ function getOutputLength(item) {
437
+ return [
438
+ item?.output,
439
+ item?.stdout,
440
+ item?.stderr,
441
+ item?.patchText,
442
+ ]
443
+ .map((value) => String(value || ""))
444
+ .join("")
445
+ .length;
446
+ }
447
+
448
+ function hasFailureLikeOutput(item) {
449
+ const output = [item?.output, item?.stdout, item?.stderr, item?.patchText]
450
+ .map((value) => String(value || ""))
451
+ .join("\n");
452
+
453
+ if (!output.trim()) {
454
+ return false;
455
+ }
456
+
457
+ return false;
458
+ }
459
+
460
+ function getDurationMs(item) {
461
+ const value = Number(item?.durationMs ?? item?.duration ?? 0);
462
+ if (!Number.isFinite(value) || value <= 0) {
463
+ return 0;
464
+ }
465
+ if (value >= 1000) {
466
+ return value;
467
+ }
468
+ return value * 1000;
469
+ }
470
+
471
+ export function extractCommandFiles(command) {
472
+ const context = getCommandContext(command);
473
+ const { primary, commandName, subcommand, offset, tokens } = context;
474
+
475
+ if (!primary) {
476
+ return [];
477
+ }
478
+
479
+ if (commandName === "git" && subcommand === "grep") {
480
+ return extractSearchFiles(tokens, offset + 2, {
481
+ consumingOptions: [
482
+ "-e",
483
+ "-f",
484
+ "-m",
485
+ "-A",
486
+ "-B",
487
+ "-C",
488
+ "--exclude",
489
+ "--exclude-from",
490
+ "--exclude-dir",
491
+ "--include",
492
+ ],
493
+ patternOptions: ["-e", "-f"],
494
+ });
495
+ }
496
+
497
+ if (["rg", "grep", "fd"].includes(commandName)) {
498
+ return extractSearchFiles(tokens, offset + 1, {
499
+ consumingOptions:
500
+ commandName === "fd"
501
+ ? ["-e", "-E", "-x", "-X", "-g", "-t", "-T", "--glob", "--type", "--exclude"]
502
+ : [
503
+ "-e",
504
+ "-f",
505
+ "-g",
506
+ "-t",
507
+ "-T",
508
+ "-m",
509
+ "-M",
510
+ "--glob",
511
+ "--type",
512
+ "--type-not",
513
+ "--max-count",
514
+ "--max-filesize",
515
+ "--ignore-file",
516
+ "--pre",
517
+ "--replace",
518
+ "--sort",
519
+ "--sortr",
520
+ "--colors",
521
+ ],
522
+ patternOptions:
523
+ commandName === "fd" ? ["-g", "--glob"] : ["-e", "-f"],
524
+ });
525
+ }
526
+
527
+ if (commandName === "find") {
528
+ for (let index = offset + 1; index < tokens.length; index += 1) {
529
+ const token = tokens[index];
530
+ if (isLikelyPathToken(token) && !isOptionToken(token)) {
531
+ return [token];
532
+ }
533
+ }
534
+ return [];
535
+ }
536
+
537
+ if (
538
+ ["sed", "cat", "head", "tail", "nl", "wc", "stat", "ls", "tree"].includes(commandName)
539
+ ) {
540
+ return extractBrowseFiles(context);
541
+ }
542
+
543
+ if (["touch"].includes(commandName) || (commandName === "sed" && tokens.includes("-i"))) {
544
+ return extractEditFiles(context);
545
+ }
546
+
547
+ return [];
548
+ }
549
+
550
+ export function classifyCommandActivity(item) {
551
+ if (!item) {
552
+ return {
553
+ kind: "unknown",
554
+ important: true,
555
+ files: [],
556
+ searchCount: 0,
557
+ browseCount: 0,
558
+ summaryLabel: "",
559
+ stats: { added: 0, removed: 0 },
560
+ };
561
+ }
562
+
563
+ if (item.type === "patch") {
564
+ const fileStats =
565
+ collectPatchFileStats(item).length > 0
566
+ ? collectPatchFileStats(item)
567
+ : collectPatchFileStatsFromOutput(item);
568
+ const files = fileStats.map((entry) => entry.path);
569
+ const stats = fileStats.reduce(
570
+ (acc, entry) => ({
571
+ added: acc.added + entry.added,
572
+ removed: acc.removed + entry.removed,
573
+ }),
574
+ { added: 0, removed: 0 },
575
+ );
576
+ const classification = {
577
+ kind: "edit",
578
+ important: false,
579
+ files,
580
+ searchCount: 0,
581
+ browseCount: 0,
582
+ summaryLabel:
583
+ files.length > 1
584
+ ? t("activity.edit.multiple", { count: files.length })
585
+ : t("activity.edit.single"),
586
+ stats,
587
+ };
588
+ classification.important = isImportantCommandActivity(item, classification);
589
+ return classification;
590
+ }
591
+
592
+ const context = getCommandContext(item.command || "");
593
+ const { primary, commandName, subcommand, normalized, tokens, offset } = context;
594
+ const files = extractCommandFiles(item.command || "");
595
+ let kind = "unknown";
596
+ let searchCount = 0;
597
+ let browseCount = 0;
598
+ let summaryLabel = "";
599
+ let stats = { added: 0, removed: 0 };
600
+
601
+ if (commandName === "git" && subcommand === "grep") {
602
+ kind = "search";
603
+ } else if (["rg", "grep", "fd"].includes(commandName)) {
604
+ kind = "search";
605
+ } else if (commandName === "find" && /(?:^|\s)find(?:\s|$)/.test(normalized)) {
606
+ kind = "search";
607
+ } else if (
608
+ ["sed", "cat", "head", "tail", "nl", "ls", "tree", "wc", "stat"].includes(commandName)
609
+ ) {
610
+ kind = "browse";
611
+ } else if (
612
+ (commandName === "node" && tokens.includes("--check")) ||
613
+ (commandName === "tsc" && tokens.includes("--noEmit")) ||
614
+ commandName === "eslint" ||
615
+ (commandName === "prettier" && tokens.includes("--check")) ||
616
+ commandName === "vitest" ||
617
+ commandName === "jest" ||
618
+ ((commandName === "npm" || commandName === "pnpm") && tokens[offset + 1] === "test")
619
+ ) {
620
+ kind = "validation";
621
+ } else if (commandName === "apply_patch") {
622
+ kind = "edit";
623
+ } else if (commandName === "touch" || (commandName === "sed" && tokens.includes("-i"))) {
624
+ kind = "edit";
625
+ } else if (commandName === "git") {
626
+ kind = "git";
627
+ } else if (primary) {
628
+ kind = "run";
629
+ }
630
+
631
+ if (kind === "search") {
632
+ searchCount = 1;
633
+ summaryLabel = t("activity.search");
634
+ } else if (kind === "browse") {
635
+ browseCount = files.length || 1;
636
+ summaryLabel =
637
+ files.length > 1
638
+ ? t("activity.browse.multiple", { count: files.length })
639
+ : t("activity.browse.single");
640
+ } else if (kind === "edit") {
641
+ const fileStats = collectFileStatsForItem(item, { kind, files });
642
+ files.splice(0, files.length, ...fileStats.map((entry) => entry.path));
643
+ stats = fileStats.reduce(
644
+ (acc, entry) => ({
645
+ added: acc.added + entry.added,
646
+ removed: acc.removed + entry.removed,
647
+ }),
648
+ { added: 0, removed: 0 },
649
+ );
650
+ summaryLabel =
651
+ files.length > 1
652
+ ? t("activity.edit.multiple", { count: files.length })
653
+ : t("activity.edit.single");
654
+ } else if (kind === "validation") {
655
+ summaryLabel = t("activity.validation.completed");
656
+ }
657
+
658
+ const classification = {
659
+ kind,
660
+ important: false,
661
+ files,
662
+ searchCount,
663
+ browseCount,
664
+ summaryLabel,
665
+ stats,
666
+ };
667
+ classification.important = isImportantCommandActivity(item, classification);
668
+ return classification;
669
+ }
670
+
671
+ export function isImportantCommandActivity(item, classification) {
672
+ if (!item) {
673
+ return true;
674
+ }
675
+
676
+ if (
677
+ item.status === "running" ||
678
+ item.outputStatus === "streaming" ||
679
+ item.status === "awaiting_approval"
680
+ ) {
681
+ return true;
682
+ }
683
+
684
+ if (
685
+ item.status === "failed" ||
686
+ item.status === "rejected" ||
687
+ item.success === false
688
+ ) {
689
+ return true;
690
+ }
691
+
692
+ if (Number.isFinite(Number(item.exitCode)) && Number(item.exitCode) !== 0) {
693
+ return true;
694
+ }
695
+
696
+ if (String(item.stderr || "").trim()) {
697
+ return true;
698
+ }
699
+
700
+ if (classification?.kind === "edit") {
701
+ return classification.files.length === 0;
702
+ }
703
+
704
+ if (getOutputLength(item) > 800) {
705
+ return true;
706
+ }
707
+
708
+ if (getDurationMs(item) > 8000) {
709
+ return true;
710
+ }
711
+
712
+ if (!classification || ["unknown", "run", "git"].includes(classification.kind)) {
713
+ return true;
714
+ }
715
+
716
+ return false;
717
+ }
718
+
719
+ function resolveDisplayState(item) {
720
+ if (
721
+ item?.status === "running" ||
722
+ item?.outputStatus === "streaming" ||
723
+ item?.status === "awaiting_approval"
724
+ ) {
725
+ return "running";
726
+ }
727
+
728
+ if (
729
+ item?.status === "failed" ||
730
+ item?.status === "rejected" ||
731
+ item?.success === false ||
732
+ hasFailureLikeOutput(item) ||
733
+ String(item?.stderr || "").trim() ||
734
+ (Number.isFinite(Number(item?.exitCode)) && Number(item.exitCode) !== 0)
735
+ ) {
736
+ return "failed";
737
+ }
738
+
739
+ return "completed";
740
+ }
741
+
742
+ function summarizePaths(paths) {
743
+ const values = Array.isArray(paths) ? paths.filter(Boolean) : [];
744
+ if (values.length === 0) {
745
+ return "";
746
+ }
747
+ if (values.length === 1) {
748
+ return values[0];
749
+ }
750
+ return `${values[0]} ${t("timeline.summary.moreItems", { count: values.length })}`;
751
+ }
752
+
753
+ export function resolveActivityDisplay(item, classification) {
754
+ const kind = classification?.kind || "unknown";
755
+ const state = resolveDisplayState(item);
756
+ const titleMap = {
757
+ edit: {
758
+ running: t("activity.running.edit"),
759
+ failed: t("activity.failed.edit"),
760
+ completed: t("activity.completed.edit"),
761
+ },
762
+ search: {
763
+ running: t("activity.running.search"),
764
+ failed: t("activity.failed.search"),
765
+ completed: t("activity.completed.search"),
766
+ },
767
+ browse: {
768
+ running: t("activity.running.browse"),
769
+ failed: t("activity.failed.browse"),
770
+ completed: t("activity.completed.browse"),
771
+ },
772
+ validation: {
773
+ running: t("activity.running.validation"),
774
+ failed: t("activity.failed.validation"),
775
+ completed: t("activity.completed.validation"),
776
+ },
777
+ git: {
778
+ running: t("activity.running.git"),
779
+ failed: t("activity.failed.git"),
780
+ completed: t("activity.completed.git"),
781
+ },
782
+ run: {
783
+ running: t("activity.running.run"),
784
+ failed: t("activity.failed.run"),
785
+ completed: t("activity.completed.run"),
786
+ },
787
+ unknown: {
788
+ running: t("activity.running.run"),
789
+ failed: t("activity.failed.run"),
790
+ completed: t("activity.completed.run"),
791
+ },
792
+ };
793
+
794
+ let subtitle = "";
795
+ if (kind === "search") {
796
+ subtitle = summarizePaths(classification?.files || []) || item?.cwd || "";
797
+ } else if (kind === "browse" || kind === "edit") {
798
+ subtitle = summarizePaths(classification?.files || []);
799
+ } else if (kind === "validation") {
800
+ subtitle = item?.cwd || "";
801
+ }
802
+
803
+ return {
804
+ title: titleMap[kind]?.[state] || titleMap.unknown[state],
805
+ subtitle,
806
+ showRawCommandAsBody: item?.type === "command",
807
+ };
808
+ }
809
+
810
+ function createActivityGroup(item, classification) {
811
+ return {
812
+ groupType: classification.kind === "edit" ? "file_change_summary" : "activity_summary",
813
+ turnId: item.turnId || null,
814
+ seq: item.seq,
815
+ timestamp: item.timestamp,
816
+ rawItems: [],
817
+ browseFiles: [],
818
+ browseCommandCount: 0,
819
+ searchTargets: [],
820
+ searchCount: 0,
821
+ validationCount: 0,
822
+ commandsCount: 0,
823
+ files: [],
824
+ fileMap: new Map(),
825
+ };
826
+ }
827
+
828
+ function addToActivityGroup(group, item, classification) {
829
+ group.rawItems.push(item);
830
+ group.commandsCount += 1;
831
+
832
+ if (group.groupType === "activity_summary") {
833
+ if (classification.kind === "browse") {
834
+ group.browseCommandCount += 1;
835
+ classification.files.forEach((path) => pushUnique(group.browseFiles, path));
836
+ }
837
+ if (classification.kind === "search") {
838
+ group.searchCount += Math.max(1, classification.searchCount || 0);
839
+ const targets =
840
+ classification.files.length > 0
841
+ ? classification.files
842
+ : item.cwd
843
+ ? [item.cwd]
844
+ : [];
845
+ targets.forEach((path) => pushUnique(group.searchTargets, path));
846
+ }
847
+ if (classification.kind === "validation") {
848
+ group.validationCount += 1;
849
+ if (item.cwd) {
850
+ pushUnique(group.searchTargets, item.cwd);
851
+ }
852
+ }
853
+ return;
854
+ }
855
+
856
+ collectFileStatsForItem(item, classification).forEach((entry) => {
857
+ const current = group.fileMap.get(entry.path) || {
858
+ path: entry.path,
859
+ added: 0,
860
+ removed: 0,
861
+ };
862
+ current.added += entry.added;
863
+ current.removed += entry.removed;
864
+ group.fileMap.set(entry.path, current);
865
+ if (!group.files.includes(entry.path)) {
866
+ group.files.push(entry.path);
867
+ }
868
+ });
869
+ }
870
+
871
+ function buildActivitySummaryItem(group) {
872
+ const browseCount = group.browseFiles.length || group.browseCommandCount;
873
+ const titleParts = [];
874
+ if (browseCount > 0) {
875
+ titleParts.push(t("timeline.browse.completed", { count: browseCount }));
876
+ }
877
+ if (group.searchCount > 0) {
878
+ titleParts.push(t("timeline.search.completed", { count: group.searchCount }));
879
+ }
880
+ if (group.validationCount > 0) {
881
+ titleParts.push(t("timeline.validation.completed", { count: group.validationCount }));
882
+ }
883
+ let title = formatInlineList(titleParts);
884
+ if (!title) {
885
+ title = t("timeline.executedActivities", { count: group.commandsCount });
886
+ }
887
+
888
+ return {
889
+ id: `activity-summary:${group.turnId || "none"}:${group.seq}`,
890
+ type: "activity_summary",
891
+ turnId: group.turnId,
892
+ seq: group.seq,
893
+ timestamp: group.timestamp,
894
+ summary: {
895
+ browseFiles: group.browseFiles,
896
+ browseCount,
897
+ searchTargets: group.searchTargets,
898
+ searchCount: group.searchCount,
899
+ validationCount: group.validationCount,
900
+ commandsCount: group.commandsCount,
901
+ title,
902
+ },
903
+ rawItems: group.rawItems,
904
+ };
905
+ }
906
+
907
+ function buildFileChangeSummaryItem(group) {
908
+ const files = group.files
909
+ .map((path) => group.fileMap.get(path))
910
+ .filter(Boolean);
911
+
912
+ return {
913
+ id: `file-change-summary:${group.turnId || "none"}:${group.seq}`,
914
+ type: "file_change_summary",
915
+ turnId: group.turnId,
916
+ seq: group.seq,
917
+ timestamp: group.timestamp,
918
+ files,
919
+ title: t("timeline.edit.completed", { count: files.length }),
920
+ rawItems: group.rawItems,
921
+ };
922
+ }
923
+
924
+ function flushGroup(result, group) {
925
+ if (!group || group.rawItems.length === 0) {
926
+ return;
927
+ }
928
+
929
+ if (group.groupType === "activity_summary") {
930
+ result.push(buildActivitySummaryItem(group));
931
+ return;
932
+ }
933
+
934
+ result.push(buildFileChangeSummaryItem(group));
935
+ }
936
+
937
+ export function groupTimelineActivities(items) {
938
+ if (!Array.isArray(items) || items.length === 0) {
939
+ return [];
940
+ }
941
+
942
+ const result = [];
943
+ let group = null;
944
+
945
+ items.forEach((item) => {
946
+ if (item?.type !== "command" && item?.type !== "patch") {
947
+ flushGroup(result, group);
948
+ group = null;
949
+ result.push(item);
950
+ return;
951
+ }
952
+
953
+ const classification = classifyCommandActivity(item);
954
+ if (classification.important) {
955
+ flushGroup(result, group);
956
+ group = null;
957
+ result.push(item);
958
+ return;
959
+ }
960
+
961
+ const nextGroupType =
962
+ classification.kind === "edit" ? "file_change_summary" : "activity_summary";
963
+
964
+ if (
965
+ !group ||
966
+ group.groupType !== nextGroupType ||
967
+ group.turnId !== (item.turnId || null)
968
+ ) {
969
+ flushGroup(result, group);
970
+ group = createActivityGroup(item, classification);
971
+ }
972
+
973
+ addToActivityGroup(group, item, classification);
974
+ });
975
+
976
+ flushGroup(result, group);
977
+ return result;
978
+ }
979
+
980
+ export { basename };