Files
orca/mobile/src/session/MobileNativeChatDiffCard.tsx
T
Kelvin Amoaba f87a746200 feat(mobile): light and dark themes with desktop's palettes (#34217)
* feat(mobile): light and dark themes with desktop's palettes

Settings → Appearance offers System, Light and Dark. The shell passes the
resolved scheme to over-the-air pages in its init frame.

Fixes #22184

* fix(mobile): draw your chat messages in desktop's user bubble

Desktop uses a faint surface with a hairline border in both themes; the inverted fill went black in light.

* test(mobile): re-measure the page script sweep after the shared theme hooks

Every route now imports the theme hooks while the entry does not, so they emit one shared script (96 for the tree). Re-swept every route prefix; the worst route step is now 15.

* test(mobile): read themed stylesheets in the text-input font-size census

The census only unwrapped StyleSheet.create({...}); inputs styled through themedStyleSheet(() => ({...})) came back unread.
2026-10-27 17:35:31 +00:00

222 lines
7.2 KiB
TypeScript

import { useMemo } from 'react'
import { Pressable, Text, View } from 'react-native'
import { MobileNativeChatOwnedScrollView } from './MobileNativeChatOwnedScrollView'
import { ChevronRight, FileMinus2, FilePen, FilePlus2 } from 'lucide-react-native'
import {
NATIVE_CHAT_DIFF_CARD_COPY,
nativeChatEditFilePatchText
} from '../../../src/shared/native-chat/tools/native-chat-edit-cards'
import {
nativeChatEditFileBaseName,
nativeChatEditGutterChars,
unifiedLineNumber,
type NativeChatEditFile,
type NativeChatEditLine
} from '../../../src/shared/native-chat/tools/native-chat-edit-model'
import { NATIVE_CHAT_TOOL_ROW_VERB_COPY } from '../../../src/shared/native-chat/tools/native-chat-tool-line-label'
import { MobileSourceCopyButton } from '../components/MobileSourceCopyButton'
import { colors } from '../theme/mobile-theme'
import { useMobileColorScheme } from '../theme/use-mobile-color-scheme'
import { useMobileNativeChatOpenedDetail } from './native-chat/visual/mobile-native-chat-opened-detail'
import { useMobileNativeChatDisclosure } from './native-chat/transcript/mobile-native-chat-disclosure-store'
import {
toolStyles as styles,
toolType
} from './native-chat/transcript/mobile-native-chat-tool-styles'
function VerbIcon({ kind, size }: { kind: NativeChatEditFile['changeKind']; size: number }) {
useMobileColorScheme()
const Icon = kind === 'added' ? FilePlus2 : kind === 'deleted' ? FileMinus2 : FilePen
return <Icon size={size} color={colors.textMuted} strokeWidth={2} />
}
function DiffRow({
line,
gutterChars,
type
}: {
line: NativeChatEditLine
gutterChars: number
type: ReturnType<typeof toolType>['code']
}): React.JSX.Element {
useMobileColorScheme()
if (line.kind === 'gap') {
return (
<Text style={[styles.diffGap, type]} accessibilityLabel={NATIVE_CHAT_DIFF_CARD_COPY.diffGap}>
⋯
</Text>
)
}
return (
<View
style={[
styles.diffRow,
line.kind === 'add' && styles.diffAddGround,
line.kind === 'del' && styles.diffDelGround
]}
>
{gutterChars > 0 ? (
<Text
style={[
styles.codeText,
styles.diffGutter,
type,
{ width: gutterChars * type.fontSize * 0.62 },
line.kind === 'add' && styles.diffAddGutter,
line.kind === 'del' && styles.diffDelGutter
]}
>
{unifiedLineNumber(line) ?? ''}
</Text>
) : null}
<Text
style={[
styles.codeText,
styles.diffSign,
type,
line.kind === 'add' && styles.diffAddText,
line.kind === 'del' && styles.diffDelText
]}
>
{line.kind === 'add' ? '+' : line.kind === 'del' ? '-' : ' '}
</Text>
<Text style={[styles.codeText, styles.diffText, type]}>{line.text}</Text>
</View>
)
}
// Mount rows only when expanded so collapsed cards avoid diff work.
function DiffCardRows({
file,
fontScale,
disclosureKey,
rootIdentity
}: {
file: NativeChatEditFile
fontScale: number
disclosureKey?: string
rootIdentity?: string
}) {
useMobileColorScheme()
const onLayout = useMobileNativeChatOpenedDetail()
const type = toolType(fontScale).code
const gutterChars = nativeChatEditGutterChars(file)
const seen = new Map<string, number>()
return (
<MobileNativeChatOwnedScrollView
rootIdentity={rootIdentity ?? `diff:${file.path}`}
positionKey={disclosureKey ?? `diff:${file.path}`}
style={[styles.codeBox, styles.diffBody]}
nestedScrollEnabled
keyboardShouldPersistTaps="handled"
onLayout={onLayout}
>
{file.lines.map((line) => {
const signature = `${line.kind}:${line.oldLineNumber}:${line.newLineNumber}:${line.text}`
const occurrence = seen.get(signature) ?? 0
seen.set(signature, occurrence + 1)
return (
<DiffRow
key={`${signature}:${occurrence}`}
line={line}
gutterChars={gutterChars}
type={type}
/>
)
})}
</MobileNativeChatOwnedScrollView>
)
}
export function MobileNativeChatDiffCard({
file,
fontScale,
disclosureKey,
rootIdentity,
onOpenFile
}: {
file: NativeChatEditFile
fontScale: number
disclosureKey?: string
rootIdentity?: string
onOpenFile?: (path: string) => void
}): React.JSX.Element {
useMobileColorScheme()
const [expanded, setExpanded] = useMobileNativeChatDisclosure(disclosureKey, false)
const hasBody = file.lines.length > 0
const copyText = useMemo(() => nativeChatEditFilePatchText(file.lines), [file.lines])
const type = toolType(fontScale)
const iconSize = 15 * fontScale
const openFile = file.changeKind === 'deleted' ? undefined : onOpenFile
return (
<View>
<View style={styles.cardHeader}>
<Pressable
style={[styles.line, styles.cardToggle]}
onPress={() => hasBody && setExpanded(!expanded)}
disabled={!hasBody}
accessibilityRole="button"
accessibilityState={hasBody ? { expanded } : undefined}
>
<View style={[styles.iconSlot, { width: iconSize }]}>
<VerbIcon kind={file.changeKind} size={iconSize} />
</View>
<Text style={[styles.lineWord, type.row]}>
{NATIVE_CHAT_TOOL_ROW_VERB_COPY[file.changeKind]}
</Text>
{file.oldPath ? (
<>
<Text
style={[styles.lineWord, styles.shrinkable, styles.struck, type.row]}
numberOfLines={1}
accessibilityLabel={file.oldPath}
>
{nativeChatEditFileBaseName(file.oldPath)}
</Text>
<Text style={[styles.lineWord, type.row]}>→</Text>
</>
) : null}
<Text
style={[styles.lineTarget, type.row, openFile && styles.lineLink]}
numberOfLines={1}
onPress={openFile ? () => openFile(file.path) : undefined}
suppressHighlighting={!openFile}
accessibilityRole={openFile ? 'link' : undefined}
>
{nativeChatEditFileBaseName(file.path)}
</Text>
<Text style={[styles.lineMeta, type.meta]}>
{file.added > 0 ? <Text style={styles.countAdded}>{`+${file.added}`}</Text> : null}
{file.added > 0 && file.removed > 0 ? ' ' : null}
{file.removed > 0 ? (
<Text style={styles.countRemoved}>{`-${file.removed}`}</Text>
) : null}
</Text>
{file.truncated ? (
<Text style={[styles.lineMeta, type.meta]}>
{NATIVE_CHAT_DIFF_CARD_COPY.diffTruncated}
</Text>
) : null}
{hasBody ? (
<View style={expanded ? styles.caretOpen : undefined}>
<ChevronRight size={14 * fontScale} color={colors.textMuted} strokeWidth={2} />
</View>
) : null}
</Pressable>
<MobileSourceCopyButton
text={copyText}
accessibilityLabel={NATIVE_CHAT_DIFF_CARD_COPY.copyDiff}
/>
</View>
{hasBody && expanded ? (
<DiffCardRows
file={file}
fontScale={fontScale}
disclosureKey={disclosureKey}
rootIdentity={rootIdentity}
/>
) : null}
</View>
)
}