@hienlh/ppm 0.13.26 → 0.13.28
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.
- package/CHANGELOG.md +11 -0
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/http-api.md +1 -1
- package/dist/web/assets/{audio-preview-DrmHuA-G.js → audio-preview-DL-OtRC9.js} +1 -1
- package/dist/web/assets/chat-tab-ChHeUbi6.js +12 -0
- package/dist/web/assets/{code-editor-4MBhv6Od.js → code-editor-DZvviyVX.js} +2 -2
- package/dist/web/assets/{conflict-editor-CSxdnpiG.js → conflict-editor-Ae1WLZpv.js} +1 -1
- package/dist/web/assets/{database-viewer-XcGue74R.js → database-viewer-CCICxO9M.js} +1 -1
- package/dist/web/assets/{diff-viewer-CGc4N0js.js → diff-viewer-CBE_cN6N.js} +1 -1
- package/dist/web/assets/{extension-webview-DTOGXnGR.js → extension-webview-CIkO_pa4.js} +1 -1
- package/dist/web/assets/{glide-data-grid-C2WvgT1J.js → glide-data-grid-_nnWqMUN.js} +1 -1
- package/dist/web/assets/{image-preview-f1WDtlkM.js → image-preview-Cdb0Sc4L.js} +1 -1
- package/dist/web/assets/{index-lUbclLL5.js → index-BbVelBGY.js} +3 -3
- package/dist/web/assets/index-DmkeN7Eo.css +2 -0
- package/dist/web/assets/keybindings-store-CGRLyrRW.js +1 -0
- package/dist/web/assets/{markdown-renderer-BaSBTBlu.js → markdown-renderer-BCnVrEK7.js} +1 -1
- package/dist/web/assets/notification-store-B29tRj0q.js +1 -0
- package/dist/web/assets/{pdf-preview-BhhXBrgJ.js → pdf-preview-DnONDTTg.js} +1 -1
- package/dist/web/assets/{port-forwarding-tab-BeuYRS6h.js → port-forwarding-tab-DkFth4Rt.js} +1 -1
- package/dist/web/assets/{postgres-viewer-DhV5wpcs.js → postgres-viewer-DzCDLjbV.js} +1 -1
- package/dist/web/assets/{settings-tab-i_1RYDNd.js → settings-tab-BPeOOXw1.js} +1 -1
- package/dist/web/assets/{sql-query-editor-lKOK8JsE.js → sql-query-editor-B9uNn4Y3.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-81v28Si-.js → sqlite-viewer-BRX2lt6O.js} +1 -1
- package/dist/web/assets/{terminal-tab-DheGnuJt.js → terminal-tab-B2W7MQRx.js} +1 -1
- package/dist/web/assets/{video-preview-C1RTCvxl.js → video-preview-CxiB4qdZ.js} +1 -1
- package/dist/web/index.html +2 -2
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
- package/src/web/components/chat/message-list.tsx +94 -100
- package/src/web/components/shared/markdown-error-boundary.tsx +38 -0
- package/dist/web/assets/chat-tab-DY7pY-uK.js +0 -12
- package/dist/web/assets/index-Csu6hOB7.css +0 -2
- package/dist/web/assets/keybindings-store-DChuOfXo.js +0 -1
- package/dist/web/assets/notification-store-DLSGCmV8.js +0 -1
|
@@ -5,11 +5,12 @@ import type { ChatMessage, ChatEvent } from "../../../types/chat";
|
|
|
5
5
|
import type { SessionPhase } from "../../../types/api";
|
|
6
6
|
import type { BashPartialEntry } from "../../hooks/use-chat";
|
|
7
7
|
import { ToolCard } from "./tool-cards";
|
|
8
|
-
import { extractJsonlPath
|
|
8
|
+
import { extractJsonlPath } from "./pre-compact-button";
|
|
9
9
|
const MarkdownRenderer = lazy(() =>
|
|
10
10
|
import("@/components/shared/markdown-renderer").then((m) => ({ default: m.MarkdownRenderer }))
|
|
11
11
|
);
|
|
12
12
|
import { cn, basename } from "@/lib/utils";
|
|
13
|
+
import { RenderErrorBoundary } from "@/components/shared/markdown-error-boundary";
|
|
13
14
|
|
|
14
15
|
import {
|
|
15
16
|
AlertCircle,
|
|
@@ -102,7 +103,7 @@ export function MessageList({
|
|
|
102
103
|
return filtered.slice(start);
|
|
103
104
|
}, [filtered, visibleCount]);
|
|
104
105
|
|
|
105
|
-
const
|
|
106
|
+
const hasMoreInMemory = visibleCount < filtered.length;
|
|
106
107
|
|
|
107
108
|
// Stable fork handler — avoids new closure per message (preserves MessageBubble memo)
|
|
108
109
|
const handleFork = useCallback((msgContent: string, msgId: string | undefined) => {
|
|
@@ -110,22 +111,53 @@ export function MessageList({
|
|
|
110
111
|
}, [onFork]);
|
|
111
112
|
|
|
112
113
|
// Scroll anchor bridge published from inside StickToBottom (needs the context's scrollRef).
|
|
113
|
-
// MessageList captures pre-expand scroll metrics and restores post-render so the compact
|
|
114
|
-
// message stays at the same viewport offset when history is prepended.
|
|
115
114
|
const scrollAnchorRef = useRef<ScrollAnchorHandle | null>(null);
|
|
116
115
|
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
116
|
+
// Find the topmost displayed message that has an unexpanded compact JSONL path.
|
|
117
|
+
const findTopUnexpandedCompact = useCallback((): { id: string; jsonlPath: string } | null => {
|
|
118
|
+
if (!onExpandCompact || !isCompactExpanded) return null;
|
|
119
|
+
for (const msg of displayed) {
|
|
120
|
+
if (isCompactExpanded(msg.id)) continue;
|
|
121
|
+
// Check user message content for JSONL path
|
|
122
|
+
const path = extractJsonlPath(msg.content || "");
|
|
123
|
+
if (path) return { id: msg.id, jsonlPath: path };
|
|
124
|
+
// Check assistant events for JSONL path
|
|
125
|
+
if (msg.events) {
|
|
126
|
+
for (const ev of msg.events) {
|
|
127
|
+
if (ev.type === "text") {
|
|
128
|
+
const evPath = extractJsonlPath(ev.content || "");
|
|
129
|
+
if (evPath) return { id: msg.id, jsonlPath: evPath };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}, [displayed, onExpandCompact, isCompactExpanded]);
|
|
136
|
+
|
|
137
|
+
// Unified load-more: first show more in-memory messages, then auto-expand compact history
|
|
138
|
+
const [autoLoadingCompact, setAutoLoadingCompact] = useState(false);
|
|
139
|
+
const loadMore = useCallback(async () => {
|
|
140
|
+
if (hasMoreInMemory) {
|
|
141
|
+
scrollAnchorRef.current?.capture();
|
|
142
|
+
setVisibleCount((c) => c + PAGE_SIZE);
|
|
143
|
+
requestAnimationFrame(() => requestAnimationFrame(() => scrollAnchorRef.current?.restore()));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
// All in-memory messages visible — try expanding topmost compact
|
|
147
|
+
const compact = findTopUnexpandedCompact();
|
|
148
|
+
if (!compact || !onExpandCompact || autoLoadingCompact) return;
|
|
149
|
+
setAutoLoadingCompact(true);
|
|
150
|
+
try {
|
|
151
|
+
scrollAnchorRef.current?.capture();
|
|
152
|
+
const count = await onExpandCompact(compact.id, compact.jsonlPath);
|
|
153
|
+
setVisibleCount((c) => c + count);
|
|
154
|
+
requestAnimationFrame(() => requestAnimationFrame(() => scrollAnchorRef.current?.restore()));
|
|
155
|
+
} finally {
|
|
156
|
+
setAutoLoadingCompact(false);
|
|
157
|
+
}
|
|
158
|
+
}, [hasMoreInMemory, findTopUnexpandedCompact, onExpandCompact, autoLoadingCompact]);
|
|
159
|
+
|
|
160
|
+
const hasMore = hasMoreInMemory || !!findTopUnexpandedCompact();
|
|
129
161
|
|
|
130
162
|
if (messagesLoading) {
|
|
131
163
|
return (
|
|
@@ -151,26 +183,22 @@ export function MessageList({
|
|
|
151
183
|
<StickToBottom.Content className="p-4 space-y-4 select-none [&>*]:[overflow-anchor:auto]">
|
|
152
184
|
<ScrollAnchorBridge bridgeRef={scrollAnchorRef} />
|
|
153
185
|
{hasMore && (
|
|
154
|
-
<
|
|
155
|
-
className="w-full py-2 text-xs text-text-secondary hover:text-text-primary bg-surface-elevated/50 hover:bg-surface-elevated rounded-md border border-border/50 transition-colors">
|
|
156
|
-
Load {Math.min(PAGE_SIZE, filtered.length - visibleCount)} more messages...
|
|
157
|
-
</button>
|
|
186
|
+
<LoadMoreSentinel onLoadMore={loadMore} loading={autoLoadingCompact} />
|
|
158
187
|
)}
|
|
159
188
|
{displayed.map((msg, idx) => {
|
|
160
189
|
const globalIdx = filtered.length - displayed.length + idx;
|
|
161
190
|
const prevMsg = globalIdx > 0 ? filtered[globalIdx - 1] : undefined;
|
|
162
191
|
return (
|
|
163
|
-
<
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
/>
|
|
192
|
+
<RenderErrorBoundary key={msg.id} fallbackContent={msg.content}>
|
|
193
|
+
<MessageBubble
|
|
194
|
+
message={msg}
|
|
195
|
+
isStreaming={isStreaming && msg.id.startsWith("streaming-")}
|
|
196
|
+
projectName={projectName}
|
|
197
|
+
onFork={msg.role === "user" && onFork ? handleFork : undefined}
|
|
198
|
+
prevMsgId={prevMsg?.sdkUuid ?? prevMsg?.id}
|
|
199
|
+
bashPartialOutput={bashPartialOutput}
|
|
200
|
+
/>
|
|
201
|
+
</RenderErrorBoundary>
|
|
174
202
|
);
|
|
175
203
|
})}
|
|
176
204
|
|
|
@@ -240,13 +268,35 @@ function ScrollToBottomButton() {
|
|
|
240
268
|
);
|
|
241
269
|
}
|
|
242
270
|
|
|
243
|
-
|
|
271
|
+
/** IntersectionObserver sentinel — auto-triggers loadMore when scrolled near top */
|
|
272
|
+
function LoadMoreSentinel({ onLoadMore, loading }: { onLoadMore: () => void; loading: boolean }) {
|
|
273
|
+
const sentinelRef = useRef<HTMLDivElement>(null);
|
|
274
|
+
const onLoadMoreRef = useRef(onLoadMore);
|
|
275
|
+
onLoadMoreRef.current = onLoadMore;
|
|
276
|
+
|
|
277
|
+
useEffect(() => {
|
|
278
|
+
const el = sentinelRef.current;
|
|
279
|
+
if (!el) return;
|
|
280
|
+
const observer = new IntersectionObserver(
|
|
281
|
+
([entry]) => { if (entry?.isIntersecting) onLoadMoreRef.current(); },
|
|
282
|
+
{ rootMargin: "200px 0px 0px 0px" },
|
|
283
|
+
);
|
|
284
|
+
observer.observe(el);
|
|
285
|
+
return () => observer.disconnect();
|
|
286
|
+
}, []);
|
|
287
|
+
|
|
288
|
+
return (
|
|
289
|
+
<div ref={sentinelRef} className="flex items-center justify-center py-2 text-xs text-text-secondary">
|
|
290
|
+
{loading && <><Loader2 className="size-3 animate-spin mr-1.5" />Loading previous conversation...</>}
|
|
291
|
+
</div>
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const MessageBubble = memo(function MessageBubble({ message, isStreaming, projectName, onFork, prevMsgId, bashPartialOutput }: {
|
|
244
296
|
message: ChatMessage; isStreaming: boolean; projectName?: string;
|
|
245
297
|
onFork?: (content: string, messageId: string | undefined) => void;
|
|
246
298
|
prevMsgId?: string;
|
|
247
299
|
bashPartialOutput?: React.RefObject<Map<string, BashPartialEntry>>;
|
|
248
|
-
onExpandCompact?: (compactMessageId: string, jsonlPath: string) => Promise<number>;
|
|
249
|
-
isCompactExpanded?: (compactMessageId: string) => boolean;
|
|
250
300
|
}) {
|
|
251
301
|
if (message.role === "user") {
|
|
252
302
|
const handleFork = onFork ? () => onFork(message.content, prevMsgId) : undefined;
|
|
@@ -256,8 +306,6 @@ const MessageBubble = memo(function MessageBubble({ message, isStreaming, projec
|
|
|
256
306
|
messageId={message.id}
|
|
257
307
|
projectName={projectName}
|
|
258
308
|
onFork={handleFork}
|
|
259
|
-
onExpandCompact={onExpandCompact}
|
|
260
|
-
isCompactExpanded={isCompactExpanded}
|
|
261
309
|
/>
|
|
262
310
|
);
|
|
263
311
|
}
|
|
@@ -275,7 +323,7 @@ const MessageBubble = memo(function MessageBubble({ message, isStreaming, projec
|
|
|
275
323
|
return (
|
|
276
324
|
<div className="flex flex-col gap-2">
|
|
277
325
|
{message.events && message.events.length > 0
|
|
278
|
-
? <InterleavedEvents events={message.events} isStreaming={isStreaming} projectName={projectName} bashPartialOutput={bashPartialOutput}
|
|
326
|
+
? <InterleavedEvents events={message.events} isStreaming={isStreaming} projectName={projectName} bashPartialOutput={bashPartialOutput} />
|
|
279
327
|
: message.content && (
|
|
280
328
|
<div className="text-sm text-text-primary select-text">
|
|
281
329
|
<MarkdownContent content={message.content} projectName={projectName} />
|
|
@@ -382,42 +430,22 @@ function isPdfPath(path: string): boolean {
|
|
|
382
430
|
const SYSTEM_TAG_NAMES = new Set(["task-notification", "environment_details"]);
|
|
383
431
|
|
|
384
432
|
/** User message bubble — full width, collapsible, with system tag badges */
|
|
385
|
-
function UserBubble({ content, messageId, projectName, onFork
|
|
433
|
+
function UserBubble({ content, messageId, projectName, onFork }: {
|
|
386
434
|
content: string;
|
|
387
435
|
messageId?: string;
|
|
388
436
|
projectName?: string;
|
|
389
437
|
onFork?: () => void;
|
|
390
|
-
onExpandCompact?: (compactMessageId: string, jsonlPath: string) => Promise<number>;
|
|
391
|
-
isCompactExpanded?: (compactMessageId: string) => boolean;
|
|
392
438
|
}) {
|
|
393
|
-
const { files, text, tags, command
|
|
439
|
+
const { files, text, tags, command } = useMemo(() => {
|
|
394
440
|
const parsed = parseUserAttachments(content);
|
|
395
441
|
const { cleanText: noSysTags, tags } = extractSystemTags(parsed.text);
|
|
396
442
|
const { command, cleanText } = parseCommandTags(noSysTags);
|
|
397
|
-
// Merge command args into body text so line-clamp + Show more applies uniformly
|
|
398
443
|
const bodyText = command?.args
|
|
399
444
|
? (cleanText ? `${command.args}\n\n${cleanText}` : command.args)
|
|
400
445
|
: cleanText;
|
|
401
|
-
return { files: parsed.files, text: bodyText, tags, command
|
|
446
|
+
return { files: parsed.files, text: bodyText, tags, command };
|
|
402
447
|
}, [content]);
|
|
403
448
|
|
|
404
|
-
// Pre-compact expansion state — local per button instance
|
|
405
|
-
const [preCompactStatus, setPreCompactStatus] = useState<PreCompactStatus>(() =>
|
|
406
|
-
messageId && isCompactExpanded?.(messageId) ? "loaded" : "idle",
|
|
407
|
-
);
|
|
408
|
-
const [preCompactCount, setPreCompactCount] = useState<number | undefined>();
|
|
409
|
-
const handleExpand = useCallback(async () => {
|
|
410
|
-
if (!jsonlPath || !messageId || !onExpandCompact) return;
|
|
411
|
-
setPreCompactStatus("loading");
|
|
412
|
-
try {
|
|
413
|
-
const count = await onExpandCompact(messageId, jsonlPath);
|
|
414
|
-
setPreCompactCount(count);
|
|
415
|
-
setPreCompactStatus("loaded");
|
|
416
|
-
} catch {
|
|
417
|
-
setPreCompactStatus("error");
|
|
418
|
-
}
|
|
419
|
-
}, [jsonlPath, messageId, onExpandCompact]);
|
|
420
|
-
|
|
421
449
|
const isSystemContext = tags.some((t) => SYSTEM_TAG_NAMES.has(t.name));
|
|
422
450
|
|
|
423
451
|
const [expanded, setExpanded] = useState(false);
|
|
@@ -497,15 +525,6 @@ function UserBubble({ content, messageId, projectName, onFork, onExpandCompact,
|
|
|
497
525
|
{expanded ? <><ChevronUp className="size-3" />Show less</> : <><ChevronDown className="size-3" />Show more</>}
|
|
498
526
|
</button>
|
|
499
527
|
)}
|
|
500
|
-
{/* Expand compacted conversation: detect JSONL path in compact summary user message.
|
|
501
|
-
Prepends pre-compact messages into main flattened list (see useChat.expandCompact). */}
|
|
502
|
-
{jsonlPath && messageId && onExpandCompact && (
|
|
503
|
-
<PreCompactButton
|
|
504
|
-
status={preCompactStatus}
|
|
505
|
-
onLoad={preCompactStatus === "idle" || preCompactStatus === "error" ? handleExpand : undefined}
|
|
506
|
-
count={preCompactCount}
|
|
507
|
-
/>
|
|
508
|
-
)}
|
|
509
528
|
{/* Fork/Rewind button — only for real user messages */}
|
|
510
529
|
{!isSystemContext && onFork && (
|
|
511
530
|
<button
|
|
@@ -780,31 +799,12 @@ type EventGroup =
|
|
|
780
799
|
| { kind: "thinking"; content: string }
|
|
781
800
|
| { kind: "tool"; tool: ChatEvent; result?: ChatEvent; completed?: boolean };
|
|
782
801
|
|
|
783
|
-
function InterleavedEvents({ events, isStreaming, projectName, bashPartialOutput
|
|
802
|
+
function InterleavedEvents({ events, isStreaming, projectName, bashPartialOutput }: {
|
|
784
803
|
events: ChatEvent[];
|
|
785
804
|
isStreaming: boolean;
|
|
786
805
|
projectName?: string;
|
|
787
806
|
bashPartialOutput?: React.RefObject<Map<string, BashPartialEntry>>;
|
|
788
|
-
messageId?: string;
|
|
789
|
-
onExpandCompact?: (compactMessageId: string, jsonlPath: string) => Promise<number>;
|
|
790
|
-
isCompactExpanded?: (compactMessageId: string) => boolean;
|
|
791
807
|
}) {
|
|
792
|
-
// Local state for the /compact slash-command path (assistant-authored summary)
|
|
793
|
-
const [preCompactStatus, setPreCompactStatus] = useState<PreCompactStatus>(() =>
|
|
794
|
-
messageId && isCompactExpanded?.(messageId) ? "loaded" : "idle",
|
|
795
|
-
);
|
|
796
|
-
const [preCompactCount, setPreCompactCount] = useState<number | undefined>();
|
|
797
|
-
const handleExpand = useCallback(async (jsonlPath: string) => {
|
|
798
|
-
if (!messageId || !onExpandCompact) return;
|
|
799
|
-
setPreCompactStatus("loading");
|
|
800
|
-
try {
|
|
801
|
-
const count = await onExpandCompact(messageId, jsonlPath);
|
|
802
|
-
setPreCompactCount(count);
|
|
803
|
-
setPreCompactStatus("loaded");
|
|
804
|
-
} catch {
|
|
805
|
-
setPreCompactStatus("error");
|
|
806
|
-
}
|
|
807
|
-
}, [messageId, onExpandCompact]);
|
|
808
808
|
// Group: consecutive text → merged text block; tool_use + tool_result paired by toolUseId
|
|
809
809
|
const groups: EventGroup[] = [];
|
|
810
810
|
let textBuffer = "";
|
|
@@ -919,17 +919,9 @@ function InterleavedEvents({ events, isStreaming, projectName, bashPartialOutput
|
|
|
919
919
|
}
|
|
920
920
|
if (group.kind === "text") {
|
|
921
921
|
const isLast = isStreaming && i === groups.length - 1;
|
|
922
|
-
const jsonlPath = extractJsonlPath(group.content);
|
|
923
922
|
return (
|
|
924
923
|
<div key={`text-${i}`} className="text-sm text-text-primary select-text">
|
|
925
924
|
<StreamingText content={group.content} animate={isLast} projectName={projectName} />
|
|
926
|
-
{jsonlPath && messageId && onExpandCompact && (
|
|
927
|
-
<PreCompactButton
|
|
928
|
-
status={preCompactStatus}
|
|
929
|
-
onLoad={preCompactStatus === "idle" || preCompactStatus === "error" ? () => handleExpand(jsonlPath) : undefined}
|
|
930
|
-
count={preCompactCount}
|
|
931
|
-
/>
|
|
932
|
-
)}
|
|
933
925
|
</div>
|
|
934
926
|
);
|
|
935
927
|
}
|
|
@@ -1053,9 +1045,11 @@ function MarkdownContent({ content, projectName, isStreaming }: { content: strin
|
|
|
1053
1045
|
const cleaned = stripTeammateMessages(content);
|
|
1054
1046
|
if (!cleaned) return null;
|
|
1055
1047
|
return (
|
|
1056
|
-
<
|
|
1057
|
-
<
|
|
1058
|
-
|
|
1048
|
+
<RenderErrorBoundary fallbackContent={cleaned}>
|
|
1049
|
+
<Suspense fallback={<div className="animate-pulse h-4 bg-muted rounded" />}>
|
|
1050
|
+
<MarkdownRenderer content={cleaned} projectName={projectName} codeActions isStreaming={isStreaming} />
|
|
1051
|
+
</Suspense>
|
|
1052
|
+
</RenderErrorBoundary>
|
|
1059
1053
|
);
|
|
1060
1054
|
}
|
|
1061
1055
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Component, type ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
interface Props {
|
|
4
|
+
/** Plain text fallback when fallback ReactNode is not provided */
|
|
5
|
+
fallbackContent?: string;
|
|
6
|
+
/** Custom fallback ReactNode — takes precedence over fallbackContent */
|
|
7
|
+
fallback?: ReactNode;
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface State {
|
|
12
|
+
hasError: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Error boundary that catches React DOM reconciliation errors
|
|
17
|
+
* (e.g. "removeChild" failures from rehype-raw or browser extensions).
|
|
18
|
+
* Falls back to provided content instead of crashing the whole app.
|
|
19
|
+
*/
|
|
20
|
+
export class RenderErrorBoundary extends Component<Props, State> {
|
|
21
|
+
override state: State = { hasError: false };
|
|
22
|
+
|
|
23
|
+
static getDerivedStateFromError(): State {
|
|
24
|
+
return { hasError: true };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
override render() {
|
|
28
|
+
if (this.state.hasError) {
|
|
29
|
+
if (this.props.fallback) return this.props.fallback;
|
|
30
|
+
return this.props.fallbackContent ? (
|
|
31
|
+
<div className="text-sm whitespace-pre-wrap break-words text-text-primary opacity-80">
|
|
32
|
+
{this.props.fallbackContent}
|
|
33
|
+
</div>
|
|
34
|
+
) : null;
|
|
35
|
+
}
|
|
36
|
+
return this.props.children;
|
|
37
|
+
}
|
|
38
|
+
}
|