@parallelworks/ui 0.1.0 → 0.2.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.
Files changed (102) hide show
  1. package/dist/assets/editor.worker-CuBRIwi-.js +14776 -0
  2. package/dist/assets/json.worker-8ytS02vz.js +22161 -0
  3. package/dist/assets/yaml.worker-DJC-VUcv.js +48367 -0
  4. package/dist/chunks/Indicator-fF8AVbjQ.js +158 -0
  5. package/dist/chunks/Loader-DQiZ75fa.js +25 -0
  6. package/dist/chunks/MarkdownImpl-Bq4QhrtA.js +33 -0
  7. package/dist/chunks/Monaco-CP_cL9gp.js +745 -0
  8. package/dist/chunks/Provider-C2VtPnh5.js +151 -0
  9. package/dist/chunks/SwitchToggleSmall-FcsaYSTs.js +14 -0
  10. package/dist/chunks/Table-DWuRINhu.js +855 -0
  11. package/dist/chunks/Tooltip-WgbiC_nd.js +71 -0
  12. package/dist/chunks/lib-DTgL8klR.js +191 -0
  13. package/dist/chunks/workers-Cjfjb8E7.js +119 -0
  14. package/dist/components/CollapsiblePanel.d.ts +27 -0
  15. package/dist/components/CopyToClipboard.d.ts +17 -0
  16. package/dist/components/Dropdown.d.ts +24 -0
  17. package/dist/components/Indicator.d.ts +4 -0
  18. package/dist/components/Listbox.d.ts +18 -0
  19. package/dist/components/Markdown.d.ts +7 -0
  20. package/dist/components/MarkdownImpl.d.ts +2 -0
  21. package/dist/components/MultiSelect.d.ts +19 -0
  22. package/dist/components/Overview.d.ts +12 -0
  23. package/dist/components/Provider.d.ts +145 -0
  24. package/dist/components/SectionHeader.d.ts +6 -0
  25. package/dist/components/SwitchToggle.d.ts +12 -0
  26. package/dist/components/SwitchToggleSmall.d.ts +6 -0
  27. package/dist/components/dropdownUtils.d.ts +18 -0
  28. package/dist/editor/Editor.d.ts +2 -0
  29. package/dist/editor/EditorField.d.ts +11 -0
  30. package/dist/editor/FormCodePanel.d.ts +8 -0
  31. package/dist/editor/Monaco.d.ts +31 -0
  32. package/dist/editor/index.d.ts +6 -0
  33. package/dist/editor/index.js +219 -0
  34. package/dist/editor/themes.d.ts +4 -0
  35. package/dist/editor/workers.d.ts +6 -0
  36. package/dist/form/FieldWrapper.d.ts +10 -0
  37. package/dist/form/Fields/BooleanField.d.ts +12 -0
  38. package/dist/form/Fields/BucketReadOnlyField.d.ts +7 -0
  39. package/dist/form/Fields/CheckboxGroupField.d.ts +18 -0
  40. package/dist/form/Fields/ColorField.d.ts +8 -0
  41. package/dist/form/Fields/DropdownField.d.ts +24 -0
  42. package/dist/form/Fields/DurationField.d.ts +13 -0
  43. package/dist/form/Fields/LabelField.d.ts +9 -0
  44. package/dist/form/Fields/MultiDropdownField.d.ts +18 -0
  45. package/dist/form/Fields/NumberField.d.ts +13 -0
  46. package/dist/form/Fields/ObjectField.d.ts +8 -0
  47. package/dist/form/Fields/RadioField.d.ts +9 -0
  48. package/dist/form/Fields/RangeField.d.ts +11 -0
  49. package/dist/form/Fields/ResourceOptionIcon.d.ts +11 -0
  50. package/dist/form/Fields/StepField.d.ts +3 -0
  51. package/dist/form/Fields/StringField.d.ts +17 -0
  52. package/dist/form/Fields/TextAreaField.d.ts +8 -0
  53. package/dist/form/Fields/durationValidity.d.ts +2 -0
  54. package/dist/form/Form.d.ts +105 -0
  55. package/dist/form/SpecialTypes/DropdownWrapper.d.ts +14 -0
  56. package/dist/form/SpecialTypes/FormikCustomDropdown.d.ts +53 -0
  57. package/dist/form/SpecialTypes/FormikCustomInput.d.ts +17 -0
  58. package/dist/form/SpecialTypes/FormikMultiSelectDropdown.d.ts +13 -0
  59. package/dist/form/SpecialTypes/FormikNumericInput.d.ts +9 -0
  60. package/dist/form/SpecialTypes/index.d.ts +3 -0
  61. package/dist/form/Wizard/WizardContainer.d.ts +5 -0
  62. package/dist/form/Wizard/WizardNavigation.d.ts +2 -0
  63. package/dist/form/Wizard/WizardStepContent.d.ts +8 -0
  64. package/dist/form/Wizard/WizardStepIndicator.d.ts +2 -0
  65. package/dist/form/Wizard/types.d.ts +171 -0
  66. package/dist/form/Wizard/useWizardState.d.ts +28 -0
  67. package/dist/form/Wizard/utils.d.ts +2 -0
  68. package/dist/form/fieldRegistry.d.ts +7 -0
  69. package/dist/form/index.d.ts +16 -0
  70. package/dist/form/index.js +2317 -0
  71. package/dist/form/lib.d.ts +15 -0
  72. package/dist/form/types/fieldComponentTypes.d.ts +32 -0
  73. package/dist/form/types/fieldTypes.d.ts +80 -0
  74. package/dist/form/types/workflow-schema.d.ts +1107 -0
  75. package/dist/form/useParsedOpts.d.ts +5 -0
  76. package/dist/form/usePrevious.d.ts +1 -0
  77. package/dist/form/utils/checkShowIfObject.d.ts +1 -0
  78. package/dist/form/utils/getParentValue.d.ts +1 -0
  79. package/dist/form/utils/getPlaceholder.d.ts +2 -0
  80. package/dist/form/utils/getValueUsingPath.d.ts +1 -0
  81. package/dist/form/utils/pwResolver.d.ts +18 -0
  82. package/dist/form/utils/secondaryField.d.ts +8 -0
  83. package/dist/form/utils/slurmDuration.d.ts +15 -0
  84. package/dist/icons.d.ts +1 -1
  85. package/dist/icons.js +2 -2
  86. package/dist/index.d.ts +17 -0
  87. package/dist/index.js +107 -323
  88. package/dist/logviewer/LogViewer.d.ts +18 -0
  89. package/dist/logviewer/index.d.ts +1 -0
  90. package/dist/logviewer/index.js +534 -0
  91. package/dist/logviewer/logviewer.css +70 -0
  92. package/dist/styles.css +1 -1
  93. package/dist/workflow/AnnotationBanner.d.ts +5 -0
  94. package/dist/workflow/Collapse.d.ts +6 -0
  95. package/dist/workflow/DependencyGraph.d.ts +15 -0
  96. package/dist/workflow/JobSummary.d.ts +38 -0
  97. package/dist/workflow/MatrixGroup.d.ts +41 -0
  98. package/dist/workflow/Reveal.d.ts +7 -0
  99. package/dist/workflow/TreeView.d.ts +42 -0
  100. package/dist/workflow/index.d.ts +1 -0
  101. package/dist/workflow/index.js +2330 -0
  102. package/package.json +60 -6
@@ -0,0 +1,534 @@
1
+ import { ArrowDownIcon, ArrowUpIcon, CheckIcon, ChevronDownIcon, ChevronRightIcon, CopyIcon, DownloadIcon, ExpandIcon, ShrinkIcon, XIcon } from "../icons.js";
2
+ import { o as useStrings, r as useNotify } from "../chunks/Provider-C2VtPnh5.js";
3
+ import cx from "classnames";
4
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
+ import { Fragment as Fragment$1, memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
6
+ import { highlightSegments, parseAnsiSegments, plainSegments, processLinesWithCommands } from "@parallelworks/workflow-parser";
7
+ //#region src/logviewer/LogViewer.tsx
8
+ function LogSegments({ segments }) {
9
+ return segments.map((segment, i) => segment.highlighted ? /* @__PURE__ */ jsx("mark", {
10
+ className: cx("search-highlight", segment.className),
11
+ children: segment.content
12
+ }, i) : segment.className ? /* @__PURE__ */ jsx("span", {
13
+ className: segment.className,
14
+ children: segment.content
15
+ }, i) : /* @__PURE__ */ jsx(Fragment$1, { children: segment.content }, i));
16
+ }
17
+ var LogLine = memo(function LogLine({ index, segments, lineNumbers, isSelected, lineHeight, onLineClick, searchTerm, inline = false, command, groupDepth = 0, isGroupCollapsed, onGroupToggle, annotationLabels }) {
18
+ const displaySegments = useMemo(() => highlightSegments(command?.message ? plainSegments(command.message) : segments, searchTerm), [
19
+ segments,
20
+ searchTerm,
21
+ command
22
+ ]);
23
+ const annotationLabel = useMemo(() => {
24
+ if (command?.type === "error") return /* @__PURE__ */ jsx("span", {
25
+ className: "text-red-500 font-semibold mr-1",
26
+ children: annotationLabels.error
27
+ });
28
+ if (command?.type === "warning") return /* @__PURE__ */ jsx("span", {
29
+ className: "text-yellow-500 font-semibold mr-1",
30
+ children: annotationLabels.warning
31
+ });
32
+ if (command?.type === "notice") return /* @__PURE__ */ jsx("span", {
33
+ className: "text-blue-500 font-semibold mr-1",
34
+ children: annotationLabels.notice
35
+ });
36
+ return null;
37
+ }, [command?.type, annotationLabels]);
38
+ return /* @__PURE__ */ jsxs("div", {
39
+ "data-line-number": index + 1,
40
+ className: cx("flex items-start text-sm font-mono group", isSelected ? "bg-(--theme-link)/15" : "hover:bg-(--theme-hover)", "text-(--theme-app)", command?.type === "error" && "bg-red-500/10", command?.type === "warning" && "bg-yellow-500/10", command?.type === "notice" && "bg-blue-500/10", command?.type === "debug" && "opacity-60", command?.type === "group" && "bg-(--theme-hover) cursor-pointer"),
41
+ style: {
42
+ minHeight: `${lineHeight}px`,
43
+ lineHeight: `${lineHeight}px`,
44
+ paddingLeft: groupDepth > 0 ? `${groupDepth * 16}px` : void 0
45
+ },
46
+ onClick: command?.type === "group" && onGroupToggle ? () => onGroupToggle(index) : void 0,
47
+ children: [lineNumbers && /* @__PURE__ */ jsx("span", {
48
+ onClick: (e) => onLineClick(index + 1, e),
49
+ className: cx("inline-flex items-center justify-end w-14 pr-3 shrink-0 cursor-pointer select-none", "hover:underline hover:text-(--theme-link) transition-colors", "text-(--theme-muted-text-color)", isSelected && "text-(--theme-link)"),
50
+ children: index + 1
51
+ }), /* @__PURE__ */ jsxs("pre", {
52
+ className: cx("grow text-left whitespace-pre m-0 py-0.5 pr-2 flex items-center", lineNumbers && "pl-2 border-l border-(--theme-border)", command?.type === "error" && "border-l-red-500 border-l-2", command?.type === "warning" && "border-l-yellow-500 border-l-2", command?.type === "notice" && "border-l-blue-500 border-l-2", command?.type === "group" && "font-bold"),
53
+ children: [
54
+ command?.type === "group" && /* @__PURE__ */ jsx("span", {
55
+ className: "mr-1 shrink-0 inline-flex items-center",
56
+ children: isGroupCollapsed ? /* @__PURE__ */ jsx(ChevronRightIcon, { className: "w-3 h-3" }) : /* @__PURE__ */ jsx(ChevronDownIcon, { className: "w-3 h-3" })
57
+ }),
58
+ annotationLabel,
59
+ /* @__PURE__ */ jsx("span", { children: /* @__PURE__ */ jsx(LogSegments, { segments: displaySegments }) })
60
+ ]
61
+ })]
62
+ });
63
+ });
64
+ function LogViewer({ log, height = "unset", width, roundedBottom = false, lineNumbers = true, additionalLeftBarComponents = null, additionalRightBarComponents = null, additionalBottomLeftBarComponents = null, additionalBottomRightBarComponents = null, hideExpand = false, inline = false, contentClassName, enableWorkflowCommands = false }) {
65
+ const notify = useNotify();
66
+ const { common: tCommon, logviewer: t } = useStrings();
67
+ const annotationLabels = useMemo(() => ({
68
+ error: t.error,
69
+ warning: t.warning,
70
+ notice: t.notice
71
+ }), [t]);
72
+ const [fullscreen, setFullscreen] = useState(false);
73
+ const [follow, _setFollow] = useState(true);
74
+ const followRef = useRef(true);
75
+ const updateFollow = useCallback((value) => {
76
+ followRef.current = value;
77
+ _setFollow(value);
78
+ }, []);
79
+ const [logUrl, setLogUrl] = useState("");
80
+ const [fullParsedLog, setFullParsedLog] = useState([]);
81
+ const [processedLines, setProcessedLines] = useState([]);
82
+ const [rawLines, setRawLines] = useState([]);
83
+ const [showDebug, setShowDebug] = useState(false);
84
+ const [collapsedGroups, setCollapsedGroups] = useState(/* @__PURE__ */ new Set());
85
+ const [filter, setFilter] = useState("");
86
+ const [visibleStart, setVisibleStart] = useState(0);
87
+ const [visibleEnd, setVisibleEnd] = useState(0);
88
+ const [selectedLines, setSelectedLines] = useState(/* @__PURE__ */ new Set());
89
+ const [lastClickedLine, setLastClickedLine] = useState(null);
90
+ const [copied, setCopied] = useState(false);
91
+ const [highlightCopyPosition, setHighlightCopyPosition] = useState(null);
92
+ const [highlightedText, setHighlightedText] = useState("");
93
+ const logContainerRef = useRef(null);
94
+ const ansiCache = useRef({});
95
+ const scrollAnimationRef = useRef(null);
96
+ const prevLogLengthRef = useRef(0);
97
+ const userScrollUpRef = useRef(false);
98
+ const LINE_HEIGHT = 20;
99
+ const OVERSCAN = 20;
100
+ const handleLineClick = useCallback((lineNum, e) => {
101
+ e.preventDefault();
102
+ updateFollow(false);
103
+ if (e.shiftKey && lastClickedLine !== null) {
104
+ const start = Math.min(lastClickedLine, lineNum);
105
+ const end = Math.max(lastClickedLine, lineNum);
106
+ const newSelected = /* @__PURE__ */ new Set();
107
+ for (let i = start; i <= end; i++) newSelected.add(i);
108
+ setSelectedLines(newSelected);
109
+ } else if (selectedLines.has(lineNum) && selectedLines.size === 1) setSelectedLines(/* @__PURE__ */ new Set());
110
+ else {
111
+ setSelectedLines(new Set([lineNum]));
112
+ setLastClickedLine(lineNum);
113
+ }
114
+ }, [
115
+ lastClickedLine,
116
+ selectedLines,
117
+ updateFollow
118
+ ]);
119
+ const copyAllLogs = useCallback(async () => {
120
+ const text = rawLines.join("\n");
121
+ try {
122
+ await navigator.clipboard.writeText(text);
123
+ setCopied(true);
124
+ setTimeout(() => setCopied(false), 2e3);
125
+ } catch (_err) {
126
+ notify.error("Failed to copy logs to clipboard");
127
+ }
128
+ }, [rawLines]);
129
+ const handleTextMouseUp = useCallback((e) => {
130
+ const selectedText = window.getSelection()?.toString().trim();
131
+ if (selectedText && selectedText.length > 0) {
132
+ setHighlightCopyPosition({
133
+ x: e.clientX,
134
+ y: e.clientY
135
+ });
136
+ setHighlightedText(selectedText);
137
+ } else {
138
+ setHighlightCopyPosition(null);
139
+ setHighlightedText("");
140
+ }
141
+ }, []);
142
+ const copyHighlightedText = useCallback(async () => {
143
+ if (!highlightedText) return;
144
+ try {
145
+ await navigator.clipboard.writeText(highlightedText);
146
+ notify.success("Text copied to clipboard");
147
+ window.getSelection()?.removeAllRanges();
148
+ setHighlightCopyPosition(null);
149
+ setHighlightedText("");
150
+ } catch (_err) {
151
+ notify.error("Failed to copy text to clipboard");
152
+ }
153
+ }, [highlightedText]);
154
+ useEffect(() => {
155
+ const handleClickOutside = (e) => {
156
+ if (e.target.closest("[data-highlight-copy-button]")) return;
157
+ setTimeout(() => {
158
+ setHighlightCopyPosition(null);
159
+ setHighlightedText("");
160
+ }, 100);
161
+ };
162
+ const handleKeyDown = (e) => {
163
+ if (e.key === "Escape") {
164
+ setHighlightCopyPosition(null);
165
+ setHighlightedText("");
166
+ }
167
+ };
168
+ if (highlightCopyPosition) {
169
+ document.addEventListener("mousedown", handleClickOutside);
170
+ document.addEventListener("keydown", handleKeyDown);
171
+ }
172
+ return () => {
173
+ document.removeEventListener("mousedown", handleClickOutside);
174
+ document.removeEventListener("keydown", handleKeyDown);
175
+ };
176
+ }, [highlightCopyPosition]);
177
+ const copySelectedLines = useCallback(async () => {
178
+ if (selectedLines.size === 0) return;
179
+ const text = Array.from(selectedLines).sort((a, b) => a - b).map((ln) => rawLines[ln - 1] || "").join("\n");
180
+ try {
181
+ await navigator.clipboard.writeText(text);
182
+ setCopied(true);
183
+ setTimeout(() => setCopied(false), 2e3);
184
+ } catch (_err) {
185
+ notify.error("Failed to copy selected logs to clipboard");
186
+ }
187
+ }, [selectedLines, rawLines]);
188
+ const handleGroupToggle = useCallback((lineIndex) => {
189
+ setCollapsedGroups((prev) => {
190
+ const next = new Set(prev);
191
+ if (next.has(lineIndex)) next.delete(lineIndex);
192
+ else next.add(lineIndex);
193
+ return next;
194
+ });
195
+ }, []);
196
+ const parsedLog = useMemo(() => {
197
+ let filtered = enableWorkflowCommands ? processedLines : fullParsedLog;
198
+ if (enableWorkflowCommands) filtered = filtered.filter((item) => {
199
+ if (item.command?.type === "endgroup") return false;
200
+ if (!showDebug && item.command?.type === "debug") return false;
201
+ if (item.groupId !== void 0 && item.command?.type !== "group" && collapsedGroups.has(item.groupId)) return false;
202
+ return true;
203
+ });
204
+ if (filter !== "") {
205
+ const normalizedFilter = filter.toLowerCase();
206
+ filtered = filtered.filter((item) => {
207
+ const command = enableWorkflowCommands ? item.command : void 0;
208
+ return (command?.message ? command.message : item.segments.map((segment) => segment.content).join("")).toLowerCase().includes(normalizedFilter);
209
+ });
210
+ }
211
+ return filtered;
212
+ }, [
213
+ fullParsedLog,
214
+ processedLines,
215
+ filter,
216
+ enableWorkflowCommands,
217
+ showDebug,
218
+ collapsedGroups
219
+ ]);
220
+ const smoothScrollToBottom = useCallback(() => {
221
+ if (!logContainerRef.current) return;
222
+ if (scrollAnimationRef.current) {
223
+ cancelAnimationFrame(scrollAnimationRef.current);
224
+ scrollAnimationRef.current = null;
225
+ }
226
+ const container = logContainerRef.current;
227
+ const targetScroll = container.scrollHeight - container.clientHeight;
228
+ const currentScroll = container.scrollTop;
229
+ const distance = targetScroll - currentScroll;
230
+ if (Math.abs(distance) < 5) {
231
+ container.scrollTop = targetScroll;
232
+ return;
233
+ }
234
+ const startTime = performance.now();
235
+ const duration = 150;
236
+ const animate = (currentTime) => {
237
+ const elapsed = currentTime - startTime;
238
+ const progress = Math.min(elapsed / duration, 1);
239
+ container.scrollTop = currentScroll + distance * (1 - (1 - progress) ** 3);
240
+ if (progress < 1) scrollAnimationRef.current = requestAnimationFrame(animate);
241
+ else scrollAnimationRef.current = null;
242
+ };
243
+ scrollAnimationRef.current = requestAnimationFrame(animate);
244
+ }, []);
245
+ const scrollLogViewer = useCallback((location, ref) => {
246
+ if (!ref?.current) return;
247
+ if (location === "top") ref.current.scroll({
248
+ top: 0,
249
+ left: 0,
250
+ behavior: "smooth"
251
+ });
252
+ else smoothScrollToBottom();
253
+ }, [smoothScrollToBottom]);
254
+ useEffect(() => {
255
+ const container = logContainerRef.current;
256
+ if (!container || inline) return;
257
+ const onWheel = (e) => {
258
+ if (e.deltaY < 0) userScrollUpRef.current = true;
259
+ if (scrollAnimationRef.current) {
260
+ cancelAnimationFrame(scrollAnimationRef.current);
261
+ scrollAnimationRef.current = null;
262
+ }
263
+ };
264
+ const onTouchStart = () => {
265
+ userScrollUpRef.current = true;
266
+ if (scrollAnimationRef.current) {
267
+ cancelAnimationFrame(scrollAnimationRef.current);
268
+ scrollAnimationRef.current = null;
269
+ }
270
+ };
271
+ container.addEventListener("wheel", onWheel, { passive: true });
272
+ container.addEventListener("touchstart", onTouchStart, { passive: true });
273
+ return () => {
274
+ container.removeEventListener("wheel", onWheel);
275
+ container.removeEventListener("touchstart", onTouchStart);
276
+ };
277
+ }, [inline]);
278
+ const handleLogScroll = (event) => {
279
+ const { scrollTop, clientHeight, scrollHeight } = event.currentTarget;
280
+ const isAnimating = scrollAnimationRef.current !== null;
281
+ const atBottom = scrollHeight - clientHeight - scrollTop < 2;
282
+ if (!isAnimating) {
283
+ if (atBottom && !follow) updateFollow(true);
284
+ else if (userScrollUpRef.current && follow) updateFollow(false);
285
+ userScrollUpRef.current = false;
286
+ }
287
+ if (!followRef.current) {
288
+ const visibleCount = Math.ceil(clientHeight / LINE_HEIGHT) + OVERSCAN * 2;
289
+ const maxStart = Math.max(0, parsedLog.length - visibleCount);
290
+ const startIndex = Math.min(maxStart, Math.max(0, Math.floor(scrollTop / LINE_HEIGHT) - OVERSCAN));
291
+ setVisibleStart(startIndex);
292
+ setVisibleEnd(startIndex + visibleCount);
293
+ }
294
+ };
295
+ useEffect(() => {
296
+ if (!log) return;
297
+ const lines = (typeof log === "object" ? JSON.stringify(log) : log.toString()).split("\n");
298
+ const cleanedLines = lines[lines.length - 1] === "" ? lines.slice(0, -1) : lines;
299
+ const isAppending = cleanedLines.length > prevLogLengthRef.current;
300
+ prevLogLengthRef.current = cleanedLines.length;
301
+ setRawLines(cleanedLines);
302
+ const processed = cleanedLines.map((line, index) => {
303
+ if (ansiCache.current[line] === void 0) ansiCache.current[line] = parseAnsiSegments(line);
304
+ return {
305
+ index,
306
+ segments: ansiCache.current[line]
307
+ };
308
+ });
309
+ setFullParsedLog(processed);
310
+ if (enableWorkflowCommands) setProcessedLines(processLinesWithCommands(processed, cleanedLines));
311
+ if (followRef.current && logContainerRef.current) {
312
+ setVisibleEnd((prev) => Math.max(prev, cleanedLines.length));
313
+ if (isAppending) requestAnimationFrame(() => {
314
+ requestAnimationFrame(() => {
315
+ smoothScrollToBottom();
316
+ });
317
+ });
318
+ }
319
+ const file = new Blob([log], { type: "text/plaintext" });
320
+ const url = URL.createObjectURL(file);
321
+ setLogUrl(url);
322
+ return () => {
323
+ URL.revokeObjectURL(url);
324
+ if (scrollAnimationRef.current) cancelAnimationFrame(scrollAnimationRef.current);
325
+ };
326
+ }, [
327
+ log,
328
+ smoothScrollToBottom,
329
+ enableWorkflowCommands
330
+ ]);
331
+ const parsedLogLengthRef = useRef(parsedLog.length);
332
+ parsedLogLengthRef.current = parsedLog.length;
333
+ useEffect(() => {
334
+ if (!logContainerRef.current) return;
335
+ const observer = new ResizeObserver(() => {
336
+ if (!logContainerRef.current) return;
337
+ if (parsedLogLengthRef.current === 0) return;
338
+ const container = logContainerRef.current;
339
+ const clientHeight = container.clientHeight;
340
+ const visibleCount = Math.ceil(clientHeight / LINE_HEIGHT) + OVERSCAN * 2;
341
+ if (followRef.current) {
342
+ const minStart = Math.max(0, parsedLogLengthRef.current - visibleCount);
343
+ setVisibleStart((prev) => Math.min(prev, minStart));
344
+ setVisibleEnd(parsedLogLengthRef.current);
345
+ requestAnimationFrame(() => {
346
+ requestAnimationFrame(() => {
347
+ smoothScrollToBottom();
348
+ });
349
+ });
350
+ } else {
351
+ const maxStart = Math.max(0, parsedLogLengthRef.current - visibleCount);
352
+ const startIndex = Math.min(maxStart, Math.max(0, Math.floor(container.scrollTop / LINE_HEIGHT) - OVERSCAN));
353
+ setVisibleStart(startIndex);
354
+ setVisibleEnd(startIndex + visibleCount);
355
+ }
356
+ });
357
+ observer.observe(logContainerRef.current);
358
+ return () => observer.disconnect();
359
+ }, [smoothScrollToBottom]);
360
+ const handleFilterChange = (event) => {
361
+ const newFilter = event.target.value;
362
+ setFilter(newFilter);
363
+ updateFollow(false);
364
+ if (logContainerRef.current) {
365
+ const clientHeight = logContainerRef.current.clientHeight;
366
+ const visibleCount = Math.ceil(clientHeight / LINE_HEIGHT) + OVERSCAN * 2;
367
+ setVisibleStart(0);
368
+ setVisibleEnd(Math.min(parsedLog.length, visibleCount));
369
+ scrollLogViewer("top", logContainerRef);
370
+ } else {
371
+ setVisibleStart(0);
372
+ setVisibleEnd(50);
373
+ }
374
+ };
375
+ const ExpandButtonIcon = fullscreen ? ShrinkIcon : ExpandIcon;
376
+ const expandButtonText = fullscreen ? "Collapse" : "Expand";
377
+ const paddingTop = visibleStart * LINE_HEIGHT;
378
+ const paddingBottom = Math.max(0, parsedLog.length - visibleEnd) * LINE_HEIGHT;
379
+ const visibleLogs = inline ? parsedLog : parsedLog.slice(visibleStart, Math.min(parsedLog.length, visibleEnd));
380
+ return /* @__PURE__ */ jsxs("div", {
381
+ className: cx("flex flex-col grow", roundedBottom && "rounded-lg"),
382
+ style: !fullscreen ? {
383
+ height,
384
+ width
385
+ } : {
386
+ height: "calc(100% - 50px)",
387
+ width: "100%",
388
+ position: "fixed",
389
+ left: 0,
390
+ top: 50,
391
+ zIndex: 9999
392
+ },
393
+ children: [
394
+ /* @__PURE__ */ jsxs("div", {
395
+ className: cx("flex select-none items-center gap-x-2 p-2", inline ? "bg-[var(--theme-panel-bg)] border-b border-[var(--theme-border)]" : "theme-panel"),
396
+ children: [/* @__PURE__ */ jsxs("div", {
397
+ className: cx("w-full flex flex-row gap-x-2 items-center", hideExpand && "justify-between"),
398
+ children: [
399
+ /* @__PURE__ */ jsxs("div", {
400
+ className: "flex gap-x-2 w-full",
401
+ children: [
402
+ !inline && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("button", {
403
+ type: "button",
404
+ className: "btn btn-neutral",
405
+ onClick: () => {
406
+ scrollLogViewer("top", logContainerRef);
407
+ updateFollow(false);
408
+ },
409
+ children: [/* @__PURE__ */ jsx(ArrowUpIcon, {}), /* @__PURE__ */ jsx("span", { children: t.top })]
410
+ }), /* @__PURE__ */ jsxs("button", {
411
+ type: "button",
412
+ className: "btn btn-neutral",
413
+ onClick: () => {
414
+ if (!follow) {
415
+ updateFollow(true);
416
+ setVisibleEnd((prev) => Math.max(prev, parsedLog.length));
417
+ requestAnimationFrame(() => {
418
+ smoothScrollToBottom();
419
+ });
420
+ } else updateFollow(false);
421
+ },
422
+ children: [/* @__PURE__ */ jsx(ArrowDownIcon, {}), /* @__PURE__ */ jsx("span", { children: !follow ? "Follow" : "Following" })]
423
+ })] }),
424
+ selectedLines.size > 0 ? /* @__PURE__ */ jsxs("div", {
425
+ className: "relative flex items-center h-full",
426
+ children: [/* @__PURE__ */ jsxs("button", {
427
+ type: "button",
428
+ className: "btn btn-neutral",
429
+ onClick: copySelectedLines,
430
+ children: [copied ? /* @__PURE__ */ jsx(CheckIcon, { className: "text-green-500" }) : /* @__PURE__ */ jsx(CopyIcon, {}), /* @__PURE__ */ jsx("span", { children: copied ? tCommon.copied : t.copyLines(selectedLines.size) })]
431
+ }), /* @__PURE__ */ jsx("button", {
432
+ type: "button",
433
+ className: "absolute -top-1.5 -right-1.5 btn-neutral rounded-full p-1 border transition-colors shadow-sm cursor-pointer z-10",
434
+ onClick: () => setSelectedLines(/* @__PURE__ */ new Set()),
435
+ title: t.clearSelection,
436
+ children: /* @__PURE__ */ jsx(XIcon, { className: "text-[10px]" })
437
+ })]
438
+ }) : /* @__PURE__ */ jsxs("button", {
439
+ type: "button",
440
+ className: "btn btn-neutral",
441
+ onClick: copyAllLogs,
442
+ children: [copied ? /* @__PURE__ */ jsx(CheckIcon, { className: "text-green-500" }) : /* @__PURE__ */ jsx(CopyIcon, {}), /* @__PURE__ */ jsx("span", { children: copied ? tCommon.copied : t.copyAll })]
443
+ }),
444
+ additionalLeftBarComponents,
445
+ enableWorkflowCommands && /* @__PURE__ */ jsx("button", {
446
+ type: "button",
447
+ className: cx("btn btn-neutral", showDebug && "btn-active"),
448
+ onClick: () => setShowDebug((prev) => !prev),
449
+ children: /* @__PURE__ */ jsx("span", { children: showDebug ? t.hideDebug : t.showDebug })
450
+ })
451
+ ]
452
+ }),
453
+ /* @__PURE__ */ jsx("input", {
454
+ type: "text",
455
+ className: "rounded-md text-sm h-8",
456
+ placeholder: t.filterLogs,
457
+ onChange: handleFilterChange
458
+ }),
459
+ /* @__PURE__ */ jsxs("a", {
460
+ href: logUrl,
461
+ className: cx(!logUrl && "disabled", "btn btn-neutral"),
462
+ download: true,
463
+ children: [/* @__PURE__ */ jsx(DownloadIcon, {}), /* @__PURE__ */ jsx("span", { children: t.download })]
464
+ }),
465
+ /* @__PURE__ */ jsxs("button", {
466
+ type: "button",
467
+ className: cx("btn btn-neutral", hideExpand && "hidden"),
468
+ onClick: () => setFullscreen((prev) => !prev),
469
+ children: [/* @__PURE__ */ jsx(ExpandButtonIcon, {}), /* @__PURE__ */ jsx("span", { children: expandButtonText })]
470
+ })
471
+ ]
472
+ }), additionalRightBarComponents]
473
+ }),
474
+ /* @__PURE__ */ jsx("div", {
475
+ "data-testid": "logviewer",
476
+ className: cx(roundedBottom && "rounded-b-lg", "w-full overflow-x-auto", !inline && "h-full grow overflow-y-auto", !inline && "bg-(--theme-muted-panel-bg)", contentClassName),
477
+ style: inline ? void 0 : {
478
+ overflowY: "auto",
479
+ position: "relative",
480
+ overscrollBehavior: "contain"
481
+ },
482
+ ref: logContainerRef,
483
+ onScroll: inline ? void 0 : handleLogScroll,
484
+ onMouseUp: handleTextMouseUp,
485
+ children: /* @__PURE__ */ jsx("div", {
486
+ className: "min-w-max",
487
+ style: inline ? void 0 : {
488
+ paddingTop,
489
+ paddingBottom
490
+ },
491
+ children: visibleLogs.map((item) => /* @__PURE__ */ jsx(LogLine, {
492
+ index: item.index,
493
+ segments: item.segments,
494
+ lineNumbers,
495
+ isSelected: selectedLines.has(item.index + 1),
496
+ lineHeight: LINE_HEIGHT,
497
+ onLineClick: handleLineClick,
498
+ searchTerm: filter,
499
+ inline,
500
+ command: enableWorkflowCommands ? item.command : void 0,
501
+ groupDepth: enableWorkflowCommands ? item.groupDepth : 0,
502
+ isGroupCollapsed: enableWorkflowCommands && item.command?.type === "group" ? collapsedGroups.has(item.index) : void 0,
503
+ onGroupToggle: enableWorkflowCommands ? handleGroupToggle : void 0,
504
+ annotationLabels
505
+ }, item.index))
506
+ })
507
+ }),
508
+ highlightCopyPosition && highlightedText && /* @__PURE__ */ jsx("button", {
509
+ type: "button",
510
+ "data-highlight-copy-button": true,
511
+ className: cx("fixed z-[10000] flex items-center gap-1 px-2 py-1.5 text-xs font-medium", "rounded-md shadow-lg border cursor-pointer transition-colors duration-150", "bg-(--theme-panel-bg) border-(--theme-border) hover:bg-(--theme-hover) text-(--theme-app)"),
512
+ style: {
513
+ left: highlightCopyPosition.x,
514
+ top: highlightCopyPosition.y + 8,
515
+ transform: "translateX(-50%)"
516
+ },
517
+ onClick: copyHighlightedText,
518
+ children: copied ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(CheckIcon, { className: "text-green-500 w-3.5 h-3.5" }), /* @__PURE__ */ jsx("span", { children: tCommon.copied })] }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(CopyIcon, { className: "w-3.5 h-3.5" }), /* @__PURE__ */ jsx("span", { children: tCommon.copy })] })
519
+ }),
520
+ (additionalBottomLeftBarComponents || additionalBottomRightBarComponents) && /* @__PURE__ */ jsxs("div", {
521
+ className: cx("flex theme-panel select-none items-center gap-x-2 p-2"),
522
+ children: [/* @__PURE__ */ jsx("div", {
523
+ className: cx("w-full flex flex-row gap-x-2 items-center", hideExpand && "justify-between"),
524
+ children: /* @__PURE__ */ jsx("div", {
525
+ className: "flex gap-x-2 w-full",
526
+ children: additionalBottomLeftBarComponents
527
+ })
528
+ }), additionalBottomRightBarComponents]
529
+ })
530
+ ]
531
+ });
532
+ }
533
+ //#endregion
534
+ export { LogViewer };
@@ -0,0 +1,70 @@
1
+ /* Log viewer search highlighting */
2
+ mark.search-highlight {
3
+ background-color: rgba(251, 191, 36, 0.4);
4
+ color: inherit;
5
+ padding: 0 2px;
6
+ border-radius: 2px;
7
+ }
8
+
9
+ /*
10
+ * Scheme-aware ANSI colors for LogViewer.
11
+ *
12
+ * The `anser` library is invoked with { use_classes: true }, so ANSI codes
13
+ * become classes like `ansi-white-fg` / `ansi-black-bg` rather than inline
14
+ * styles. Rules below remap only the combinations that would be unreadable
15
+ * against the log surface — white/bright-white on a light surface, black on
16
+ * a dark surface, and a few low-contrast hues. Colors that already read well
17
+ * pass through as `currentcolor`/`transparent` so users still see the intent
18
+ * of their ANSI output. `light-dark()` follows the host's `color-scheme`, so
19
+ * no component ever branches on a scheme.
20
+ */
21
+ .ansi-white-fg,
22
+ .ansi-bright-white-fg {
23
+ color: light-dark(var(--theme-app, #111827), currentcolor);
24
+ }
25
+ .ansi-black-fg {
26
+ color: light-dark(currentcolor, var(--theme-muted-text-color, #9ca3af));
27
+ }
28
+ .ansi-red-fg {
29
+ color: light-dark(#991b1b, #f87171);
30
+ }
31
+ .ansi-bright-red-fg {
32
+ color: light-dark(#b91c1c, currentcolor);
33
+ }
34
+ .ansi-green-fg {
35
+ color: light-dark(#166534, #4ade80);
36
+ }
37
+ .ansi-bright-green-fg {
38
+ color: light-dark(#15803d, currentcolor);
39
+ }
40
+ .ansi-yellow-fg {
41
+ color: light-dark(#a16207, #fde047);
42
+ }
43
+ .ansi-bright-yellow-fg {
44
+ color: light-dark(#b45309, currentcolor);
45
+ }
46
+ .ansi-blue-fg {
47
+ color: light-dark(#1e40af, #60a5fa);
48
+ }
49
+ .ansi-bright-blue-fg {
50
+ color: light-dark(#1d4ed8, currentcolor);
51
+ }
52
+ .ansi-magenta-fg {
53
+ color: light-dark(#86198f, #e879f9);
54
+ }
55
+ .ansi-bright-magenta-fg {
56
+ color: light-dark(#a21caf, currentcolor);
57
+ }
58
+ .ansi-cyan-fg {
59
+ color: light-dark(#155e75, #67e8f9);
60
+ }
61
+ .ansi-bright-cyan-fg {
62
+ color: light-dark(#0e7490, currentcolor);
63
+ }
64
+ .ansi-white-bg,
65
+ .ansi-bright-white-bg {
66
+ background-color: light-dark(#e5e7eb, transparent);
67
+ }
68
+ .ansi-black-bg {
69
+ background-color: light-dark(transparent, #374151);
70
+ }