Files
orca/mobile/src/session/MobileNativeChatFullContent.tsx
T
OrcaWinandOrcaWin d8eef80bf4 feat(mobile): localize sessions, terminal, agent chat, browser and files (#35553)
Moves ~970 strings in session screens, the terminal, agent chat (including the
copy mobile used to render straight from shared English records), the browser pane
and the file explorer to the translation API, with all ten translations.

- Chat tool-run sentences, task lists, receipts, notices and turn status take
  translated words; the shared sentence joiner accepts an optional continuation so
  German nouns and Turkish casing survive mid-sentence (desktop unchanged)
- "Used the browser" vs "… 21 times" and resolved PR comment headers use explicit
  keys instead of plural rules, which would drop the count for Russian 21, 31, …
- Action sheet rows carry stable ids instead of being keyed by translated labels
- Rename inputs keep the raw title rather than a translated placeholder

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-29 03:55:41 +00:00

212 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useContext, useEffect, useMemo, useState } from 'react'
import { Pressable, Text, View, type TextStyle } from 'react-native'
import { truncateToolDetail } from '../../../src/shared/native-chat/tools/native-chat-tool-summary'
import {
isNativeChatToolOutputClipped,
nativeChatToolFullPayload,
nativeChatToolOutputPartialSizes
} from '../../../src/shared/native-chat/tools/native-chat-tool-output'
import type {
NativeChatMessage,
NativeChatToolResultBlock
} from '../../../src/shared/native-chat-types'
import type { AgentJournalBoundedPayload } from '../../../src/shared/agent-session-journal-types'
import { INLINE_TEXT_SELECTION } from '../components/inline-text-selection'
import { styles } from './native-chat/transcript/mobile-native-chat-message-styles'
import { toolStyles } from './native-chat/transcript/mobile-native-chat-tool-styles'
import { useMobileColorScheme } from '../theme/use-mobile-color-scheme'
import { useMobileTranslation } from '../i18n/use-mobile-translation'
import { MobileNativeChatOwnedScrollView } from './MobileNativeChatOwnedScrollView'
import {
MobileNativeChatItemSourceContext,
readMobileNativeChatItem
} from './mobile-native-chat-item-read'
// Keep full-content state per row so live transcript revisions cannot overwrite the reader’s requested expansion.
type Load = 'idle' | 'loading' | 'failed'
function ShowFull({
label,
failedLabel,
load,
onPress
}: {
label: string
failedLabel: string
load: Load
onPress: () => void
}): React.JSX.Element {
useMobileColorScheme()
return (
<Pressable
style={styles.showFull}
onPress={onPress}
disabled={load === 'loading'}
hitSlop={8}
accessibilityRole="button"
>
<Text style={styles.showFullText}>{load === 'failed' ? failedLabel : label}</Text>
</Pressable>
)
}
export function MobileNativeChatToolOutput({
body,
type,
rootIdentity,
positionKey
}: {
body: NativeChatToolResultBlock
type: TextStyle
rootIdentity: string
positionKey: string
}): React.JSX.Element {
useMobileColorScheme()
const t = useMobileTranslation()
const source = useContext(MobileNativeChatItemSourceContext)
const { output, isError, fullOutputItemId } = body
const client = source?.client,
sessionId = source?.sessionId
const [expanded, setExpanded] = useState(false)
const [detail, setDetail] = useState<{
body: typeof body
client: typeof client
sessionId: typeof sessionId
full?: AgentJournalBoundedPayload
load: Load
} | null>(null)
useEffect(
() => () =>
setDetail((held) =>
held?.body === body && held.client === client && held.sessionId === sessionId ? null : held
),
[body, client, sessionId]
)
const current =
detail?.body === body && detail.client === client && detail.sessionId === sessionId
? detail
: null
const full = current?.full
const clipped = isNativeChatToolOutputClipped(body, { expanded, fullLoaded: Boolean(full) })
const partial = useMemo(() => (full ? nativeChatToolOutputPartialSizes(full) : null), [full])
const showFull = (): void => {
if (fullOutputItemId === undefined || source === null) {
setExpanded(true)
return
}
setDetail({ body, client, sessionId, load: 'loading' })
const finish = (full?: AgentJournalBoundedPayload): void => {
setDetail((held) =>
held?.body === body && held.client === client && held.sessionId === sessionId
? { body, client, sessionId, full, load: full ? 'idle' : 'failed' }
: held
)
}
readMobileNativeChatItem(source, fullOutputItemId)
.then((item) =>
finish(
item?.itemId === fullOutputItemId
? (nativeChatToolFullPayload(item.body) ?? undefined)
: undefined
)
)
.catch(() => finish())
}
return (
<View>
<MobileNativeChatOwnedScrollView
rootIdentity={rootIdentity}
positionKey={positionKey}
style={[toolStyles.codeBox, toolStyles.codeScroll]}
nestedScrollEnabled
keyboardShouldPersistTaps="handled"
>
<Text
selectable={INLINE_TEXT_SELECTION}
style={[toolStyles.codeText, type, isError && toolStyles.errorText]}
>
{full ? full.head : expanded ? output : truncateToolDetail(output)}
</Text>
</MobileNativeChatOwnedScrollView>
{partial ? (
<Text style={styles.showFullText}>
{t('session.nativeChatFullContent.partialOutput', 'Showing {{shown}} of {{total}}.', {
shown: partial.shown,
total: partial.total
})}
</Text>
) : null}
{clipped ? (
<ShowFull
label={t('session.nativeChatFullContent.showFullOutput', 'Show full output')}
failedLabel={t(
'session.nativeChatFullContent.loadOutputFailed',
"Show full output — couldn't load, tap to retry"
)}
load={current?.load ?? 'idle'}
onPress={showFull}
/>
) : null}
</View>
)
}
export function useMobileNativeChatFullMessage(message: NativeChatMessage): {
shown: NativeChatMessage
showFull: React.JSX.Element | null
recovery: { load: Load; onPress: () => void } | null
} {
useMobileColorScheme()
const t = useMobileTranslation()
const source = useContext(MobileNativeChatItemSourceContext)
const detail = source?.messageDetail
const client = source?.client,
sessionId = source?.sessionId
const raw = detail?.raw(message.id)
const [request, setRequest] = useState<{
raw: typeof raw
client: typeof client
sessionId: typeof sessionId
load: Load
} | null>(null)
const load =
request?.raw === raw && request?.client === client && request?.sessionId === sessionId
? (request?.load ?? 'idle')
: 'idle'
const state = detail?.state(message.id)
const showFull = (): void => {
setRequest({ raw, client, sessionId, load: 'loading' })
const finish = (load: Load): void =>
setRequest((held) =>
held?.raw === raw && held?.client === client && held?.sessionId === sessionId
? { raw, client, sessionId, load }
: held
)
void detail
?.load(message.id)
.then(() => finish('idle'))
.catch(() => finish('failed'))
}
return {
shown: message,
recovery: state === 'available' ? { load, onPress: showFull } : null,
showFull:
state === 'partial' ? (
<Text style={styles.showFullText}>
{t('session.nativeChatFullContent.partialMessage', 'Showing part of this message.')}
</Text>
) : state === 'available' ? (
<ShowFull
label={t('session.nativeChatFullContent.showFullMessage', 'Show full message')}
failedLabel={t(
'session.nativeChatFullContent.loadMessageFailed',
"Show full message — couldn't load, tap to retry"
)}
load={load}
onPress={showFull}
/>
) : null
}
}