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,656 @@
1
+ import { escapeHtml, renderRichText } from "./message-rich-text.js";
2
+ import { formatInlineList, t } from "./i18n/index.js";
3
+ import {
4
+ basename,
5
+ classifyCommandActivity,
6
+ resolveActivityDisplay,
7
+ } from "./session-command-activity.js";
8
+
9
+ function getCompactDisplayPaths(paths, limit = 2) {
10
+ const values = Array.isArray(paths) ? paths.filter(Boolean) : [];
11
+ if (values.length === 0) {
12
+ return { preview: [], remainingCount: 0 };
13
+ }
14
+
15
+ const basenameCounts = new Map();
16
+ values.forEach((path) => {
17
+ const key = basename(path) || path;
18
+ basenameCounts.set(key, (basenameCounts.get(key) || 0) + 1);
19
+ });
20
+
21
+ const displayPaths = values.map((path) => {
22
+ const key = basename(path) || path;
23
+ return (basenameCounts.get(key) || 0) > 1 ? path : key;
24
+ });
25
+
26
+ return {
27
+ preview: displayPaths.slice(0, limit),
28
+ remainingCount: Math.max(0, displayPaths.length - limit),
29
+ };
30
+ }
31
+
32
+ function compactCommandPreview(command, maxLength = 64) {
33
+ const source = String(command || "").trim();
34
+ if (!source) {
35
+ return "";
36
+ }
37
+
38
+ const unwrapped = source.replace(
39
+ /^(?:\/bin\/)?(?:zsh|bash|sh)\s+-lc\s+(['"])([\s\S]*)\1$/,
40
+ "$2",
41
+ );
42
+ const normalized = unwrapped.replace(/\s+/g, " ").trim();
43
+ if (normalized.length <= maxLength) {
44
+ return normalized;
45
+ }
46
+ return `${normalized.slice(0, maxLength - 1)}…`;
47
+ }
48
+
49
+ function localizeApprovalTitle(title) {
50
+ const normalized = String(title || "").trim();
51
+ if (!normalized) {
52
+ return t("approval.required");
53
+ }
54
+
55
+ if (
56
+ normalized === "命令执行需要授权" ||
57
+ normalized === "Command execution requires approval"
58
+ ) {
59
+ return t("approval.commandRequired");
60
+ }
61
+
62
+ if (
63
+ normalized === "文件修改需要授权" ||
64
+ normalized === "File changes require approval"
65
+ ) {
66
+ return t("approval.fileChangeRequired");
67
+ }
68
+
69
+ if (
70
+ normalized === "额外权限需要授权" ||
71
+ normalized === "Extra permissions require approval"
72
+ ) {
73
+ return t("approval.extraPermissionRequired");
74
+ }
75
+
76
+ if (
77
+ normalized === "操作需要授权" ||
78
+ normalized === "Approval required" ||
79
+ normalized === "Approval required for operation"
80
+ ) {
81
+ return t("approval.required");
82
+ }
83
+
84
+ return normalized;
85
+ }
86
+
87
+ function renderInlineActivityDetail({
88
+ shell = "",
89
+ output = "",
90
+ error = "",
91
+ patchText = "",
92
+ }) {
93
+ const hasBody = shell || output || error || patchText;
94
+ if (!hasBody) {
95
+ return { hasDetail: false, bodyHtml: "" };
96
+ }
97
+
98
+ return {
99
+ hasDetail: true,
100
+ bodyHtml: `
101
+ <div class="assistant-command-content">
102
+ ${shell ? `<pre class="assistant-command-shell">${escapeHtml(shell)}</pre>` : ""}
103
+ ${patchText ? `<pre class="assistant-command-output">${escapeHtml(patchText)}</pre>` : ""}
104
+ ${output ? `<pre class="assistant-command-output">${escapeHtml(output)}</pre>` : ""}
105
+ ${error ? `<pre class="assistant-command-error-output">${escapeHtml(error)}</pre>` : ""}
106
+ </div>
107
+ `,
108
+ };
109
+ }
110
+
111
+ function renderRawActivityItems(items) {
112
+ const list = Array.isArray(items) ? items : [];
113
+ if (list.length === 0) {
114
+ return "";
115
+ }
116
+
117
+ const blocks = list
118
+ .map((item) => {
119
+ if (!item || typeof item !== "object") {
120
+ return "";
121
+ }
122
+ if (item.type === "patch") {
123
+ return renderInlineActivityDetail({
124
+ patchText: item.patchText || "",
125
+ output: item.output || "",
126
+ error: item.stderr || "",
127
+ }).bodyHtml;
128
+ }
129
+ if (item.type === "command") {
130
+ return renderInlineActivityDetail({
131
+ shell: item.command || "",
132
+ output: item.output || item.stdout || "",
133
+ error: item.stderr || "",
134
+ }).bodyHtml;
135
+ }
136
+ return "";
137
+ })
138
+ .filter(Boolean);
139
+
140
+ return blocks.join("");
141
+ }
142
+
143
+ function renderInlineActivityRow({
144
+ rowClass = "",
145
+ itemId = "",
146
+ label = "",
147
+ meta = "",
148
+ detail = null,
149
+ open = false,
150
+ }) {
151
+ if (detail?.hasDetail) {
152
+ return `
153
+ <div class="transcript-row transcript-row-inline-activity timeline-row ${escapeHtml(rowClass)}" data-timeline-id="${escapeHtml(itemId)}">
154
+ <div class="timeline-inline-step">
155
+ <details class="timeline-inline-detail-row" ${open ? "open" : ""}>
156
+ <summary class="task-step-row task-step-item-status">
157
+ <span class="task-step-label">${escapeHtml(label)}</span>
158
+ ${meta ? `<span class="task-step-meta">${escapeHtml(meta)}</span>` : ""}
159
+ </summary>
160
+ ${detail.bodyHtml}
161
+ </details>
162
+ </div>
163
+ </div>
164
+ `;
165
+ }
166
+
167
+ return `
168
+ <div class="transcript-row transcript-row-inline-activity timeline-row ${escapeHtml(rowClass)}" data-timeline-id="${escapeHtml(itemId)}">
169
+ <div class="timeline-inline-step">
170
+ <div class="task-step-row task-step-item-status">
171
+ <span class="task-step-label">${escapeHtml(label)}</span>
172
+ ${meta ? `<span class="task-step-meta">${escapeHtml(meta)}</span>` : ""}
173
+ </div>
174
+ </div>
175
+ </div>
176
+ `;
177
+ }
178
+
179
+ function renderInlinePatchMeta(item, classification, display) {
180
+ const changeEntries = Object.entries(item.changes || {});
181
+ if (changeEntries.length > 0) {
182
+ const basenameCounts = new Map();
183
+ changeEntries.forEach(([path]) => {
184
+ const key = basename(path) || path;
185
+ basenameCounts.set(key, (basenameCounts.get(key) || 0) + 1);
186
+ });
187
+ const preview = changeEntries.slice(0, 2).map(([path, change]) => {
188
+ const compact = basename(path) || path;
189
+ const displayPath = (basenameCounts.get(compact) || 0) > 1 ? path : compact;
190
+ return `${displayPath} +${Number(change?.added || 0)} -${Number(change?.removed || 0)}`;
191
+ });
192
+ if (changeEntries.length > 2) {
193
+ preview.push(t("timeline.summary.moreItems", { count: changeEntries.length }));
194
+ }
195
+ return preview.join("、");
196
+ }
197
+
198
+ if (classification.files.length > 0) {
199
+ const firstPath = classification.files[0];
200
+ return `${basename(firstPath) || firstPath} +${Number(classification.stats?.added || 0)} -${Number(classification.stats?.removed || 0)}`;
201
+ }
202
+
203
+ return display.subtitle || "";
204
+ }
205
+
206
+ function getBubbleWidthClass(text) {
207
+ const source = String(text || "").trim();
208
+ if (!source) {
209
+ return "";
210
+ }
211
+
212
+ return source.includes("\n") ? "" : " msg-bubble-fluid";
213
+ }
214
+
215
+ function renderChanges(changes) {
216
+ const entries = Object.entries(changes || {});
217
+ if (entries.length === 0) {
218
+ return "";
219
+ }
220
+
221
+ return `
222
+ <ul class="timeline-file-list">
223
+ ${entries
224
+ .map(
225
+ ([file, change]) => `
226
+ <li class="timeline-file-item">
227
+ <span class="timeline-file-op">${escapeHtml(change?.type || "?")}</span>
228
+ <span class="timeline-file-path">${escapeHtml(file)}</span>
229
+ </li>
230
+ `,
231
+ )
232
+ .join("")}
233
+ </ul>
234
+ `;
235
+ }
236
+
237
+ function renderPlainStreamingText(text) {
238
+ return `<div class="timeline-streaming-plain">${escapeHtml(String(text || "")).replace(/\n/g, "<br>")}</div>`;
239
+ }
240
+
241
+ function renderStreamingAwareRichText(text, options = {}) {
242
+ const source = String(text || "");
243
+ const body = renderRichText(source, options);
244
+ if (body) {
245
+ return body;
246
+ }
247
+
248
+ if (source) {
249
+ return renderPlainStreamingText(source);
250
+ }
251
+
252
+ return "";
253
+ }
254
+
255
+ export function renderTimelineList(items, options = {}) {
256
+ const body =
257
+ Array.isArray(items) && items.length > 0
258
+ ? items.map((item) => renderTimelineItem(item, options)).join("")
259
+ : `<div class="event-empty">${escapeHtml(t("timeline.empty"))}</div>`;
260
+
261
+ return `
262
+ <div id="event-list" class="event-list timeline-list event-list--flex">
263
+ ${body}
264
+ </div>
265
+ `;
266
+ }
267
+
268
+ export function renderTimeline(items, options = {}) {
269
+ return `
270
+ <div class="session-stream-shell">
271
+ <div class="session-stream-main">
272
+ ${renderTimelineList(items, options)}
273
+ </div>
274
+ </div>
275
+ `;
276
+ }
277
+
278
+ export function renderTimelineItem(item, options = {}) {
279
+ switch (item.type) {
280
+ case "user":
281
+ return renderUserMessage(item, options);
282
+ case "assistant_commentary":
283
+ return renderAssistantCommentary(item, options);
284
+ case "assistant_final":
285
+ return renderAssistantFinal(item, options);
286
+ case "reasoning":
287
+ return renderReasoningItem(item, options);
288
+ case "command":
289
+ return renderCommandItem(item, options);
290
+ case "patch":
291
+ return renderPatchItem(item, options);
292
+ case "activity_summary":
293
+ return renderActivitySummaryItem(item, options);
294
+ case "file_change_summary":
295
+ return renderFileChangeSummaryItem(item, options);
296
+ case "approval":
297
+ return renderApprovalItem(item, options);
298
+ case "system":
299
+ return renderSystemItem(item, options);
300
+ default:
301
+ return "";
302
+ }
303
+ }
304
+
305
+ export function renderUserMessage(item) {
306
+ return `
307
+ <div class="transcript-row transcript-row-user timeline-row timeline-row-user" data-timeline-id="${escapeHtml(item.id || "")}">
308
+ <article class="msg-bubble msg-user msg-user-soft${getBubbleWidthClass(item.text)}" aria-label="${escapeHtml(t("timeline.userMessage"))}">
309
+ <div class="msg-bubble-body">${renderRichText(item.text || "")}</div>
310
+ </article>
311
+ </div>
312
+ `;
313
+ }
314
+
315
+ export function renderAssistantCommentary(item) {
316
+ const body = renderStreamingAwareRichText(item.text || "", {
317
+ streaming: item.status === "streaming",
318
+ });
319
+ if (!body) {
320
+ return "";
321
+ }
322
+ return `
323
+ <div class="transcript-row transcript-row-assistant timeline-row timeline-row-commentary" data-timeline-id="${escapeHtml(item.id || "")}">
324
+ <article class="msg-bubble msg-assistant turn-assistant-bubble timeline-commentary-bubble${getBubbleWidthClass(item.text)} ${item.status === "streaming" ? "timeline-assistant-streaming" : ""}" aria-label="${escapeHtml(t("timeline.assistantCommentary"))}">
325
+ <div class="msg-bubble-body msg-md timeline-commentary-body">
326
+ ${body}
327
+ </div>
328
+ </article>
329
+ </div>
330
+ `;
331
+ }
332
+
333
+ export function renderAssistantFinal(item) {
334
+ const body = renderStreamingAwareRichText(item.text || "", {
335
+ streaming: item.status === "streaming",
336
+ });
337
+ if (!body) {
338
+ return "";
339
+ }
340
+ return `
341
+ <div class="transcript-row transcript-row-assistant timeline-row timeline-row-final" data-timeline-id="${escapeHtml(item.id || "")}">
342
+ <article class="msg-bubble msg-assistant turn-assistant-bubble${getBubbleWidthClass(item.text)} ${item.status === "streaming" ? "timeline-assistant-streaming" : ""}" aria-label="${escapeHtml(t("timeline.assistant"))}">
343
+ <div class="msg-bubble-body msg-md">
344
+ ${body}
345
+ </div>
346
+ </article>
347
+ </div>
348
+ `;
349
+ }
350
+
351
+ export function renderReasoningItem(item, options = {}) {
352
+ const activeElapsedLabel =
353
+ item.status === "thinking" ? String(options.activeElapsedLabel || "").trim() : "";
354
+ const reasoningText = String(item.summary || item.text || "").trim();
355
+ if (!reasoningText && !item.synthetic) {
356
+ return "";
357
+ }
358
+ return `
359
+ <div class="transcript-row transcript-row-assistant timeline-row timeline-row-reasoning" data-timeline-id="${escapeHtml(item.id || "")}">
360
+ <div class="timeline-reasoning ${item.status === "thinking" ? "timeline-reasoning-thinking" : ""}">
361
+ <div class="assistant-thinking-row">
362
+ <span class="assistant-thinking">${escapeHtml(reasoningText || t("timeline.thinking"))}</span>
363
+ ${
364
+ activeElapsedLabel
365
+ ? `<span class="assistant-thinking-elapsed" data-active-elapsed="true">${escapeHtml(activeElapsedLabel)}</span>`
366
+ : ""
367
+ }
368
+ </div>
369
+ </div>
370
+ </div>
371
+ `;
372
+ }
373
+
374
+ export function renderCommandItem(item) {
375
+ const classification = classifyCommandActivity(item);
376
+ const display = resolveActivityDisplay(item, classification);
377
+ const inlineOutput = String(item.output || item.stdout || "");
378
+ const isRunning =
379
+ item.status === "running" ||
380
+ item.outputStatus === "streaming" ||
381
+ item.status === "awaiting_approval";
382
+ const isFailed =
383
+ item.status === "failed" ||
384
+ item.status === "rejected" ||
385
+ (item.exitCode !== null &&
386
+ item.exitCode !== undefined &&
387
+ Number.isFinite(Number(item.exitCode)) &&
388
+ Number(item.exitCode) !== 0) ||
389
+ Boolean(String(item.stderr || "").trim());
390
+ const canRenderInline =
391
+ isRunning ||
392
+ (!isFailed && !["unknown"].includes(classification.kind));
393
+ const shouldRenderCard = !canRenderInline && !isFailed;
394
+ const summary = [];
395
+ const pushSummary = (value) => {
396
+ if (!value || summary.includes(value)) {
397
+ return;
398
+ }
399
+ summary.push(value);
400
+ };
401
+ pushSummary(display.subtitle);
402
+ pushSummary(item.cwd ? `cwd: ${item.cwd}` : "");
403
+ pushSummary(
404
+ item.exitCode !== null && item.exitCode !== undefined ? `exit ${item.exitCode}` : "",
405
+ );
406
+ pushSummary(item.status);
407
+ const shouldOpen = item.status === "running" || item.outputStatus === "streaming";
408
+
409
+ if (!shouldRenderCard) {
410
+ const inlineMeta = display.subtitle || compactCommandPreview(item.command || "");
411
+ const detail = renderInlineActivityDetail({
412
+ shell: item.command || "",
413
+ output: inlineOutput,
414
+ error: item.stderr || "",
415
+ });
416
+ return renderInlineActivityRow({
417
+ rowClass: "timeline-row-command timeline-row-inline-command",
418
+ itemId: item.id || "",
419
+ label: display.title || t("timeline.command"),
420
+ meta: inlineMeta,
421
+ detail,
422
+ open: isRunning || isFailed,
423
+ });
424
+ }
425
+
426
+ return `
427
+ <div class="transcript-row transcript-row-assistant timeline-row timeline-row-command" data-timeline-id="${escapeHtml(item.id || "")}">
428
+ <div class="timeline-card timeline-card-command timeline-card-${escapeHtml(item.status || "pending")}">
429
+ <details ${shouldOpen ? "open" : ""}>
430
+ <summary>
431
+ <span class="timeline-card-title">${escapeHtml(display.title || t("timeline.command"))}</span>
432
+ <span class="timeline-card-meta">${escapeHtml(summary.join(" · "))}</span>
433
+ </summary>
434
+ <div class="timeline-card-body">
435
+ ${
436
+ display.showRawCommandAsBody && item.command
437
+ ? `<pre class="timeline-card-pre">${escapeHtml(item.command)}</pre>`
438
+ : ""
439
+ }
440
+ ${
441
+ item.output
442
+ ? `<pre class="timeline-card-pre">${escapeHtml(item.output)}</pre>`
443
+ : item.stdout
444
+ ? `<pre class="timeline-card-pre">${escapeHtml(item.stdout)}</pre>`
445
+ : item.status === "running"
446
+ ? renderStreamingPlaceholder(t("timeline.commandStreaming"))
447
+ : ""
448
+ }
449
+ ${item.stderr ? `<pre class="timeline-card-pre timeline-card-pre-error">${escapeHtml(item.stderr)}</pre>` : ""}
450
+ </div>
451
+ </details>
452
+ </div>
453
+ </div>
454
+ `;
455
+ }
456
+
457
+ export function renderPatchItem(item) {
458
+ const classification = classifyCommandActivity(item);
459
+ const display = resolveActivityDisplay(item, classification);
460
+ const isRunning =
461
+ item.status === "running" ||
462
+ item.outputStatus === "streaming" ||
463
+ item.status === "awaiting_approval";
464
+ const looksFailed =
465
+ item.success === false ||
466
+ Boolean(String(item.stderr || "").trim()) ||
467
+ /verification failed/i.test(String(item.output || "")) ||
468
+ /failed to find expected lines/i.test(String(item.output || ""));
469
+ const shouldRenderCard =
470
+ !looksFailed && !isRunning && classification.files.length === 0;
471
+ const meta = [];
472
+ const pushMeta = (value) => {
473
+ if (!value || meta.includes(value)) {
474
+ return;
475
+ }
476
+ meta.push(value);
477
+ };
478
+ pushMeta(display.subtitle);
479
+ pushMeta(item.status || "pending");
480
+ if (item.success === true) {
481
+ pushMeta("success");
482
+ } else if (item.success === false) {
483
+ pushMeta("failed");
484
+ }
485
+ const shouldOpen = item.status === "running" || item.outputStatus === "streaming";
486
+
487
+ if (!shouldRenderCard) {
488
+ const detail = renderInlineActivityDetail({
489
+ patchText: item.patchText || "",
490
+ output: item.output || "",
491
+ error: item.stderr || "",
492
+ });
493
+ return renderInlineActivityRow({
494
+ rowClass: "timeline-row-patch timeline-row-inline-patch",
495
+ itemId: item.id || "",
496
+ label: display.title || t("timeline.patch"),
497
+ meta: renderInlinePatchMeta(item, classification, display),
498
+ detail,
499
+ open: isRunning || looksFailed,
500
+ });
501
+ }
502
+
503
+ return `
504
+ <div class="transcript-row transcript-row-assistant timeline-row timeline-row-patch" data-timeline-id="${escapeHtml(item.id || "")}">
505
+ <div class="timeline-card timeline-card-patch timeline-card-${escapeHtml(item.status || "pending")}">
506
+ <details ${shouldOpen ? "open" : ""}>
507
+ <summary>
508
+ <span class="timeline-card-title">${escapeHtml(display.title || t("timeline.patch"))}</span>
509
+ <span class="timeline-card-meta">${escapeHtml(meta.join(" · "))}</span>
510
+ </summary>
511
+ <div class="timeline-card-body">
512
+ ${item.patchText ? `<pre class="timeline-card-pre">${escapeHtml(item.patchText)}</pre>` : ""}
513
+ ${renderChanges(item.changes)}
514
+ ${
515
+ item.output
516
+ ? `<pre class="timeline-card-pre">${escapeHtml(item.output)}</pre>`
517
+ : item.status === "running"
518
+ ? renderStreamingPlaceholder(t("timeline.patchStreaming"))
519
+ : ""
520
+ }
521
+ ${item.stderr ? `<pre class="timeline-card-pre timeline-card-pre-error">${escapeHtml(item.stderr)}</pre>` : ""}
522
+ </div>
523
+ </details>
524
+ </div>
525
+ </div>
526
+ `;
527
+ }
528
+
529
+ export function renderActivitySummaryItem(item) {
530
+ const summary = item.summary || {};
531
+ const browseFiles = Array.isArray(summary.browseFiles) ? summary.browseFiles : [];
532
+ const searchTargets = Array.isArray(summary.searchTargets) ? summary.searchTargets : [];
533
+ const metaParts = [];
534
+ const browsePreview = getCompactDisplayPaths(browseFiles, 2);
535
+ const searchPreview = getCompactDisplayPaths(searchTargets, 2);
536
+
537
+ if (browsePreview.preview.length > 0) {
538
+ metaParts.push(formatInlineList(browsePreview.preview));
539
+ }
540
+ if (browsePreview.remainingCount > 0) {
541
+ metaParts.push(t("timeline.summary.moreFiles", { count: browsePreview.remainingCount }));
542
+ }
543
+ if (summary.searchCount > 0 && searchPreview.preview.length > 0) {
544
+ metaParts.push(
545
+ browsePreview.preview.length > 0
546
+ ? t("timeline.summary.searchAt", { value: formatInlineList(searchPreview.preview) })
547
+ : formatInlineList(searchPreview.preview),
548
+ );
549
+ }
550
+ if (
551
+ summary.validationCount > 0 &&
552
+ searchPreview.preview.length > 0 &&
553
+ summary.searchCount === 0
554
+ ) {
555
+ metaParts.push(formatInlineList(searchPreview.preview));
556
+ }
557
+ if (
558
+ summary.searchCount > 0 &&
559
+ searchPreview.remainingCount > 0 &&
560
+ browsePreview.preview.length === 0
561
+ ) {
562
+ metaParts.push(t("timeline.summary.moreLocations", { count: searchPreview.remainingCount }));
563
+ }
564
+ if (
565
+ summary.validationCount > 0 &&
566
+ searchPreview.remainingCount > 0 &&
567
+ summary.searchCount === 0
568
+ ) {
569
+ metaParts.push(t("timeline.summary.moreLocations", { count: searchPreview.remainingCount }));
570
+ }
571
+ if (summary.commandsCount > 0 && metaParts.length === 0) {
572
+ metaParts.push(t("timeline.summary.activities", { count: summary.commandsCount }));
573
+ }
574
+
575
+ const detail = {
576
+ hasDetail: Array.isArray(item.rawItems) && item.rawItems.length > 0,
577
+ bodyHtml: renderRawActivityItems(item.rawItems),
578
+ };
579
+
580
+ return renderInlineActivityRow({
581
+ rowClass: "timeline-row-activity-summary",
582
+ itemId: item.id || "",
583
+ label: summary.title || t("timeline.activitySummary"),
584
+ meta: metaParts.join(" · "),
585
+ detail,
586
+ });
587
+ }
588
+
589
+ export function renderFileChangeSummaryItem(item) {
590
+ const files = Array.isArray(item.files) ? item.files : [];
591
+ const basenameCounts = new Map();
592
+ files.forEach((file) => {
593
+ const key = basename(file.path || "") || file.path || "";
594
+ basenameCounts.set(key, (basenameCounts.get(key) || 0) + 1);
595
+ });
596
+
597
+ const preview = files.slice(0, 2).map((file) => {
598
+ const path = file.path || t("timeline.file.untitled");
599
+ const compact = basename(path) || path;
600
+ const displayPath = (basenameCounts.get(compact) || 0) > 1 ? path : compact;
601
+ return `${displayPath} +${Number(file.added || 0)} -${Number(file.removed || 0)}`;
602
+ });
603
+ if (files.length > 2) {
604
+ preview.push(t("timeline.summary.moreItems", { count: files.length }));
605
+ }
606
+ const detail = {
607
+ hasDetail: Array.isArray(item.rawItems) && item.rawItems.length > 0,
608
+ bodyHtml: renderRawActivityItems(item.rawItems),
609
+ };
610
+
611
+ return renderInlineActivityRow({
612
+ rowClass: "timeline-row-file-change-summary",
613
+ itemId: item.id || "",
614
+ label: item.title || t("timeline.fileChanges"),
615
+ meta: formatInlineList(preview),
616
+ detail,
617
+ });
618
+ }
619
+
620
+ export function renderApprovalItem(item) {
621
+ const metaParts = [];
622
+ if (item.status === "rejected") {
623
+ metaParts.push(t("approval.deny"));
624
+ } else if (item.status === "resolved") {
625
+ if (item.decision === "acceptForSession") {
626
+ metaParts.push(t("approval.allowForTurn"));
627
+ } else {
628
+ metaParts.push(t("approval.allowOnce"));
629
+ }
630
+ } else {
631
+ metaParts.push(t("approval.pending"));
632
+ }
633
+
634
+ if (item.reason) {
635
+ metaParts.push(item.reason);
636
+ } else if (item.command) {
637
+ metaParts.push(compactCommandPreview(item.command, 88));
638
+ }
639
+
640
+ return renderInlineActivityRow({
641
+ rowClass: "timeline-row-approval-history",
642
+ itemId: item.id || "",
643
+ label: localizeApprovalTitle(item.title),
644
+ meta: metaParts.join(" · "),
645
+ });
646
+ }
647
+
648
+ export function renderSystemItem(item) {
649
+ return `
650
+ <div class="transcript-row transcript-row-assistant timeline-row timeline-row-system" data-timeline-id="${escapeHtml(item.id || "")}">
651
+ <div class="timeline-system timeline-system-${escapeHtml(item.status || item.subtype || "neutral")}">
652
+ ${escapeHtml(item.text || item.subtype || t("timeline.system"))}
653
+ </div>
654
+ </div>
655
+ `;
656
+ }
@@ -0,0 +1,31 @@
1
+ export function connectSessionSocket(sessionId, handlers) {
2
+ const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
3
+ const ws = new WebSocket(`${protocol}//${window.location.host}/ws/sessions/${sessionId}`);
4
+
5
+ ws.addEventListener("open", () => {
6
+ handlers.onStateChange?.("open");
7
+ });
8
+
9
+ ws.addEventListener("close", () => {
10
+ handlers.onStateChange?.("closed");
11
+ });
12
+
13
+ ws.addEventListener("error", () => {
14
+ handlers.onStateChange?.("error");
15
+ });
16
+
17
+ ws.addEventListener("message", (event) => {
18
+ try {
19
+ const payload = JSON.parse(event.data);
20
+ handlers.onEvent?.(payload);
21
+ } catch (error) {
22
+ handlers.onStateChange?.(error instanceof Error ? error.message : "parse_error");
23
+ }
24
+ });
25
+
26
+ return {
27
+ close() {
28
+ ws.close();
29
+ },
30
+ };
31
+ }