fix(diagnostics): render sheet reports reliably
Diagnostic sheets now share one themed code surface. String reports render line by line so large log lines do not turn the whole report into one oversized native text surface.
This commit is contained in:
@@ -1,19 +1,13 @@
|
||||
import * as Clipboard from "expo-clipboard";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
Platform,
|
||||
Pressable,
|
||||
ScrollView,
|
||||
Text,
|
||||
View,
|
||||
type PressableStateCallbackType,
|
||||
} from "react-native";
|
||||
import { Platform, Pressable, Text, View, type PressableStateCallbackType } from "react-native";
|
||||
import { Copy, RotateCw } from "lucide-react-native";
|
||||
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { AdaptiveModalSheet, type SheetHeader } from "@/components/adaptive-modal-sheet";
|
||||
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||
import { ScrollableCodeSurface, SurfaceCard } from "@/components/ui/scrollable-code-surface";
|
||||
import { useToast } from "@/contexts/toast-context";
|
||||
import { getDesktopDaemonLogs, getDesktopDaemonStatus } from "@/desktop/daemon/desktop-daemon";
|
||||
import {
|
||||
@@ -24,7 +18,6 @@ import {
|
||||
redactAppDiagnosticReport,
|
||||
} from "@/diagnostics/app-diagnostic-report";
|
||||
import { getHostRuntimeStore, useHosts, type HostRuntimeSnapshot } from "@/runtime/host-runtime";
|
||||
import { settingsStyles } from "@/styles/settings";
|
||||
import { ICON_SIZE, type Theme } from "@/styles/theme";
|
||||
import type { HostProfile } from "@/types/host-connection";
|
||||
|
||||
@@ -230,16 +223,12 @@ export function AppDiagnosticSheet({
|
||||
scrollable={false}
|
||||
testID="app-diagnostic-sheet"
|
||||
>
|
||||
<View style={DIAGNOSTIC_CARD_STYLE}>
|
||||
{diagnostic ? (
|
||||
<ScrollView style={styles.codeScroll} contentContainerStyle={styles.codeContent}>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator>
|
||||
<Text style={styles.codeText} selectable>
|
||||
{diagnostic}
|
||||
</Text>
|
||||
</ScrollView>
|
||||
</ScrollView>
|
||||
) : (
|
||||
{diagnostic ? (
|
||||
<ScrollableCodeSurface key={visible ? "visible" : "hidden"} maxHeight={520}>
|
||||
{diagnostic}
|
||||
</ScrollableCodeSurface>
|
||||
) : (
|
||||
<SurfaceCard key={visible ? "visible" : "hidden"}>
|
||||
<View style={styles.progressContent}>
|
||||
{progress.length === 0 ? (
|
||||
<View style={styles.progressRow}>
|
||||
@@ -256,7 +245,7 @@ export function AppDiagnosticSheet({
|
||||
/>
|
||||
) : (
|
||||
<View
|
||||
style={row.status === "failed" ? FAILED_STATUS_DOT_STYLE : styles.statusDot}
|
||||
style={row.status === "failed" ? styles.statusDotFailed : styles.statusDot}
|
||||
/>
|
||||
)}
|
||||
<Text
|
||||
@@ -266,8 +255,8 @@ export function AppDiagnosticSheet({
|
||||
))
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</SurfaceCard>
|
||||
)}
|
||||
</AdaptiveModalSheet>
|
||||
);
|
||||
}
|
||||
@@ -374,22 +363,6 @@ function formatProgressStatus(status: ProgressStatus): string {
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
diagnosticCard: {
|
||||
overflow: "hidden",
|
||||
},
|
||||
codeScroll: {
|
||||
maxHeight: 520,
|
||||
},
|
||||
codeContent: {
|
||||
paddingVertical: theme.spacing[3],
|
||||
paddingHorizontal: theme.spacing[4],
|
||||
},
|
||||
codeText: {
|
||||
fontFamily: theme.fontFamily.mono,
|
||||
fontSize: theme.fontSize.code,
|
||||
color: theme.colors.foreground,
|
||||
lineHeight: 18,
|
||||
},
|
||||
progressContent: {
|
||||
paddingVertical: theme.spacing[4],
|
||||
paddingHorizontal: theme.spacing[4],
|
||||
@@ -431,9 +404,9 @@ const styles = StyleSheet.create((theme) => ({
|
||||
backgroundColor: theme.colors.success,
|
||||
},
|
||||
statusDotFailed: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: theme.borderRadius.full,
|
||||
backgroundColor: theme.colors.destructive,
|
||||
},
|
||||
}));
|
||||
|
||||
const DIAGNOSTIC_CARD_STYLE = [settingsStyles.card, styles.diagnosticCard];
|
||||
const FAILED_STATUS_DOT_STYLE = [styles.statusDot, styles.statusDotFailed];
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
ActivityIndicator,
|
||||
Pressable,
|
||||
type PressableStateCallbackType,
|
||||
ScrollView,
|
||||
Text,
|
||||
View,
|
||||
} from "react-native";
|
||||
@@ -19,6 +18,7 @@ import {
|
||||
} from "@/components/adaptive-modal-sheet";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||
import { ScrollableCodeSurface, SurfaceCard } from "@/components/ui/scrollable-code-surface";
|
||||
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||
import { isWeb } from "@/constants/platform";
|
||||
import { useToast } from "@/contexts/toast-context";
|
||||
@@ -363,26 +363,26 @@ function DiagnosticSubSheet({
|
||||
let body: React.ReactNode;
|
||||
if (loading && !diagnostic) {
|
||||
body = (
|
||||
<View style={sheetStyles.codeBlockLoading}>
|
||||
<ActivityIndicator size="small" color={theme.colors.foregroundMuted} />
|
||||
<Text style={sheetStyles.mutedText}>{t("settings.providers.diagnostic.running")}</Text>
|
||||
</View>
|
||||
<SurfaceCard key={visible ? "visible" : "hidden"}>
|
||||
<View style={sheetStyles.codeBlockLoading}>
|
||||
<ActivityIndicator size="small" color={theme.colors.foregroundMuted} />
|
||||
<Text style={sheetStyles.mutedText}>{t("settings.providers.diagnostic.running")}</Text>
|
||||
</View>
|
||||
</SurfaceCard>
|
||||
);
|
||||
} else if (diagnostic) {
|
||||
body = (
|
||||
<ScrollView style={sheetStyles.codeScroll} contentContainerStyle={sheetStyles.codeContent}>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator>
|
||||
<Text style={sheetStyles.codeText} selectable dataSet={CODE_SURFACE_DATASET}>
|
||||
{diagnostic}
|
||||
</Text>
|
||||
</ScrollView>
|
||||
</ScrollView>
|
||||
<ScrollableCodeSurface key={visible ? "visible" : "hidden"} maxHeight={480}>
|
||||
{diagnostic}
|
||||
</ScrollableCodeSurface>
|
||||
);
|
||||
} else {
|
||||
body = (
|
||||
<View style={sheetStyles.codeBlockLoading}>
|
||||
<Text style={sheetStyles.mutedText}>{t("settings.providers.diagnostic.none")}</Text>
|
||||
</View>
|
||||
<SurfaceCard key={visible ? "visible" : "hidden"}>
|
||||
<View style={sheetStyles.codeBlockLoading}>
|
||||
<Text style={sheetStyles.mutedText}>{t("settings.providers.diagnostic.none")}</Text>
|
||||
</View>
|
||||
</SurfaceCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -395,7 +395,7 @@ function DiagnosticSubSheet({
|
||||
scrollable={false}
|
||||
testID="provider-diagnostic-sheet"
|
||||
>
|
||||
<View style={DIAGNOSTIC_CARD_STYLE}>{body}</View>
|
||||
{body}
|
||||
</AdaptiveModalSheet>
|
||||
);
|
||||
}
|
||||
@@ -862,22 +862,6 @@ const sheetStyles = StyleSheet.create((theme) => ({
|
||||
justifyContent: "flex-end",
|
||||
gap: theme.spacing[2],
|
||||
},
|
||||
diagnosticCard: {
|
||||
overflow: "hidden",
|
||||
},
|
||||
codeScroll: {
|
||||
maxHeight: 480,
|
||||
},
|
||||
codeContent: {
|
||||
paddingVertical: theme.spacing[3],
|
||||
paddingHorizontal: theme.spacing[4],
|
||||
},
|
||||
codeText: {
|
||||
fontFamily: theme.fontFamily.mono,
|
||||
fontSize: theme.fontSize.code,
|
||||
color: theme.colors.foreground,
|
||||
lineHeight: 18,
|
||||
},
|
||||
codeBlockLoading: {
|
||||
paddingVertical: theme.spacing[4],
|
||||
paddingHorizontal: theme.spacing[4],
|
||||
@@ -893,4 +877,3 @@ const COMPACT_FOOTER_META_STYLE = [sheetStyles.footerMeta, sheetStyles.compactFo
|
||||
const MAIN_SNAP_POINTS = ["65%", "92%"];
|
||||
const ADD_SNAP_POINTS = ["40%"];
|
||||
const DIAGNOSTIC_SNAP_POINTS = ["50%", "85%"];
|
||||
const DIAGNOSTIC_CARD_STYLE = [settingsStyles.card, sheetStyles.diagnosticCard];
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
import { useMemo, type ReactNode } from "react";
|
||||
import {
|
||||
ScrollView,
|
||||
Text,
|
||||
View,
|
||||
type StyleProp,
|
||||
type TextStyle,
|
||||
type ViewStyle,
|
||||
} from "react-native";
|
||||
import { StyleSheet } from "react-native-unistyles";
|
||||
|
||||
import { AppearanceStyleBoundary } from "@/components/appearance-style-boundary";
|
||||
import { isWeb } from "@/constants/platform";
|
||||
import { CODE_SURFACE_DATASET } from "@/styles/code-surface";
|
||||
import { inlineUnistylesStyle } from "@/styles/unistyles-inline-style";
|
||||
|
||||
type CodeSurfaceTone = "surface0" | "surface1" | "surface2";
|
||||
|
||||
interface ScrollableCodeSurfaceProps {
|
||||
children: ReactNode;
|
||||
maxHeight?: number;
|
||||
horizontal?: boolean;
|
||||
selectable?: boolean;
|
||||
tone?: CodeSurfaceTone;
|
||||
bordered?: boolean;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
scrollStyle?: StyleProp<ViewStyle>;
|
||||
contentContainerStyle?: StyleProp<ViewStyle>;
|
||||
textStyle?: StyleProp<TextStyle>;
|
||||
testID?: string;
|
||||
accessibilityLabel?: string;
|
||||
}
|
||||
|
||||
interface SurfaceCardProps {
|
||||
children: ReactNode;
|
||||
tone?: CodeSurfaceTone;
|
||||
bordered?: boolean;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
testID?: string;
|
||||
accessibilityLabel?: string;
|
||||
codeSurface?: boolean;
|
||||
}
|
||||
|
||||
export function SurfaceCard({
|
||||
children,
|
||||
tone = "surface1",
|
||||
bordered = true,
|
||||
style,
|
||||
testID,
|
||||
accessibilityLabel,
|
||||
codeSurface = false,
|
||||
}: SurfaceCardProps) {
|
||||
const surfaceStyle = getSurfaceStyle(tone);
|
||||
const containerStyle = useMemo(
|
||||
() => [styles.container, surfaceStyle, bordered && styles.bordered, style],
|
||||
[bordered, style, surfaceStyle],
|
||||
);
|
||||
|
||||
const surface = (
|
||||
<View
|
||||
style={containerStyle}
|
||||
dataSet={codeSurface ? CODE_SURFACE_DATASET : undefined}
|
||||
testID={testID}
|
||||
accessible={accessibilityLabel != null}
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
>
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
return <AppearanceStyleBoundary>{surface}</AppearanceStyleBoundary>;
|
||||
}
|
||||
|
||||
export function ScrollableCodeSurface({
|
||||
children,
|
||||
maxHeight,
|
||||
horizontal = true,
|
||||
selectable = true,
|
||||
tone = "surface1",
|
||||
bordered = true,
|
||||
style,
|
||||
scrollStyle,
|
||||
contentContainerStyle,
|
||||
textStyle,
|
||||
testID,
|
||||
accessibilityLabel,
|
||||
}: ScrollableCodeSurfaceProps) {
|
||||
const maxHeightStyle = useMemo(
|
||||
() => (maxHeight == null ? null : inlineUnistylesStyle({ maxHeight })),
|
||||
[maxHeight],
|
||||
);
|
||||
const outerScrollStyle = useMemo(
|
||||
() => [maxHeightStyle, scrollStyle],
|
||||
[maxHeightStyle, scrollStyle],
|
||||
);
|
||||
const contentStyle = useMemo(
|
||||
() => [styles.content, contentContainerStyle],
|
||||
[contentContainerStyle],
|
||||
);
|
||||
const codeTextStyle = useMemo(() => [styles.text, textStyle], [textStyle]);
|
||||
const codeContent =
|
||||
typeof children === "string" ? (
|
||||
<PlainCodeText text={children} selectable={selectable} textStyle={codeTextStyle} />
|
||||
) : (
|
||||
<Text selectable={selectable} style={codeTextStyle} dataSet={CODE_SURFACE_DATASET}>
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
|
||||
return (
|
||||
<SurfaceCard
|
||||
tone={tone}
|
||||
bordered={bordered}
|
||||
style={style}
|
||||
testID={testID}
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
codeSurface
|
||||
>
|
||||
<ScrollView
|
||||
style={outerScrollStyle}
|
||||
contentContainerStyle={contentStyle}
|
||||
nestedScrollEnabled
|
||||
showsVerticalScrollIndicator
|
||||
>
|
||||
{horizontal ? (
|
||||
<ScrollView horizontal nestedScrollEnabled showsHorizontalScrollIndicator>
|
||||
{codeContent}
|
||||
</ScrollView>
|
||||
) : (
|
||||
codeContent
|
||||
)}
|
||||
</ScrollView>
|
||||
</SurfaceCard>
|
||||
);
|
||||
}
|
||||
|
||||
function PlainCodeText({
|
||||
text,
|
||||
selectable,
|
||||
textStyle,
|
||||
}: {
|
||||
text: string;
|
||||
selectable: boolean;
|
||||
textStyle: StyleProp<TextStyle>;
|
||||
}) {
|
||||
const lines = useMemo(() => {
|
||||
let offset = 0;
|
||||
return text.split("\n").map((line) => {
|
||||
const key = `${offset}:${line.length}`;
|
||||
offset += line.length + 1;
|
||||
return { key, line };
|
||||
});
|
||||
}, [text]);
|
||||
return (
|
||||
<View dataSet={CODE_SURFACE_DATASET}>
|
||||
{lines.map(({ key, line }) => (
|
||||
<Text key={key} selectable={selectable} style={textStyle} dataSet={CODE_SURFACE_DATASET}>
|
||||
{line.length === 0 ? "\u200B" : line}
|
||||
</Text>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function getSurfaceStyle(tone: CodeSurfaceTone): StyleProp<ViewStyle> {
|
||||
switch (tone) {
|
||||
case "surface0":
|
||||
return styles.surface0;
|
||||
case "surface2":
|
||||
return styles.surface2;
|
||||
case "surface1":
|
||||
return styles.surface1;
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
container: {
|
||||
overflow: "hidden",
|
||||
},
|
||||
bordered: {
|
||||
borderRadius: theme.borderRadius.lg,
|
||||
borderWidth: theme.borderWidth[1],
|
||||
borderColor: theme.colors.border,
|
||||
},
|
||||
surface0: {
|
||||
backgroundColor: theme.colors.surface0,
|
||||
},
|
||||
surface1: {
|
||||
backgroundColor: theme.colors.surface1,
|
||||
},
|
||||
surface2: {
|
||||
backgroundColor: theme.colors.surface2,
|
||||
},
|
||||
content: {
|
||||
paddingVertical: theme.spacing[3],
|
||||
paddingHorizontal: theme.spacing[4],
|
||||
},
|
||||
text: {
|
||||
backgroundColor: "transparent",
|
||||
fontFamily: theme.fontFamily.mono,
|
||||
fontSize: theme.fontSize.code,
|
||||
color: theme.colors.foreground,
|
||||
lineHeight: 18,
|
||||
...(isWeb
|
||||
? {
|
||||
whiteSpace: "pre",
|
||||
overflowWrap: "normal",
|
||||
}
|
||||
: null),
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user