mirror of
https://github.com/stablyai/orca.git
synced 2026-11-06 03:57:35 +00:00
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>
289 lines
9.3 KiB
TypeScript
289 lines
9.3 KiB
TypeScript
import { useLayoutEffect, useMemo, useRef, useState } from 'react'
|
|
import { Alert, View, Text, Pressable } from 'react-native'
|
|
import { ChevronLeft } from 'lucide-react-native'
|
|
import { colors, spacing } from '../theme/mobile-theme'
|
|
import { themedStyleSheet } from '../theme/themed-styles'
|
|
import { useMobileColorScheme } from '../theme/use-mobile-color-scheme'
|
|
import { BottomDrawer } from '../components/BottomDrawer'
|
|
import type { RpcClient } from '../transport/rpc-client'
|
|
import type { TerminalQuickCommand } from '../../../src/shared/terminal-quick-command-types'
|
|
import { getQuickCommandPreview, quickCommandMatchesRepo } from '../terminal/quick-commands'
|
|
import { useQuickCommands } from './use-quick-commands'
|
|
import { QuickCommandEditorForm } from './QuickCommandEditorForm'
|
|
import { QuickCommandAgentPicker, QuickCommandsList } from './QuickCommandsList'
|
|
import {
|
|
createEmptyQuickCommandDraft,
|
|
draftToQuickCommand,
|
|
quickCommandToDraft,
|
|
type QuickCommandDraft
|
|
} from './quick-command-draft'
|
|
import { useMobileTranslation } from '../i18n/use-mobile-translation'
|
|
|
|
type Props = {
|
|
visible: boolean
|
|
onClose: () => void
|
|
client: RpcClient | null
|
|
repoId: string | null
|
|
repoName: string | null
|
|
agentPromptMaxLength: number | null
|
|
onLaunch: (command: TerminalQuickCommand) => boolean
|
|
}
|
|
|
|
type SheetView = 'list' | 'editor' | 'agent'
|
|
|
|
export function QuickCommandsSheet({
|
|
visible,
|
|
onClose,
|
|
client,
|
|
repoId,
|
|
repoName,
|
|
agentPromptMaxLength,
|
|
onLaunch
|
|
}: Props) {
|
|
useMobileColorScheme()
|
|
const t = useMobileTranslation()
|
|
const { commands, loading, ready, error, persist } = useQuickCommands({
|
|
client,
|
|
enabled: visible
|
|
})
|
|
const [view, setView] = useState<SheetView>('list')
|
|
const [query, setQuery] = useState('')
|
|
const [draft, setDraft] = useState<QuickCommandDraft | null>(null)
|
|
const [saving, setSaving] = useState(false)
|
|
const saveOwnerRef = useRef<symbol | null>(null)
|
|
const draftRevisionRef = useRef(0)
|
|
const editorGenerationRef = useRef(0)
|
|
|
|
const previousSurface = useRef({ visible, client, repoId })
|
|
useLayoutEffect(() => {
|
|
const contextChanged =
|
|
previousSurface.current.client !== client || previousSurface.current.repoId !== repoId
|
|
if (visible !== previousSurface.current.visible || contextChanged) {
|
|
draftRevisionRef.current += 1
|
|
editorGenerationRef.current += 1
|
|
previousSurface.current = { visible, client, repoId }
|
|
if (contextChanged) {
|
|
saveOwnerRef.current = null
|
|
setSaving(false)
|
|
}
|
|
if (visible || contextChanged) {
|
|
setView('list')
|
|
setQuery('')
|
|
setDraft(null)
|
|
}
|
|
}
|
|
}, [visible, client, repoId])
|
|
|
|
// Why: prompt bodies can total megabytes. Lowercase them once per settings
|
|
// update instead of allocating the same search text on every keystroke.
|
|
const searchableCommands = useMemo(() => {
|
|
return commands
|
|
.filter((command) => quickCommandMatchesRepo(command, repoId))
|
|
.map((command) => ({
|
|
command,
|
|
searchText: `${command.label} ${getQuickCommandPreview(command)}`.toLowerCase()
|
|
}))
|
|
}, [commands, repoId])
|
|
|
|
const visibleCommands = useMemo(() => {
|
|
const trimmed = query.trim().toLowerCase()
|
|
return searchableCommands
|
|
.filter((entry) => !trimmed || entry.searchText.includes(trimmed))
|
|
.map((entry) => entry.command)
|
|
}, [query, searchableCommands])
|
|
|
|
const repoCommands = visibleCommands.filter((command) => command.scope?.type === 'repo')
|
|
const globalCommands = visibleCommands.filter((command) => command.scope?.type !== 'repo')
|
|
|
|
const openEditor = (command?: TerminalQuickCommand) => {
|
|
draftRevisionRef.current += 1
|
|
editorGenerationRef.current += 1
|
|
setDraft(
|
|
command
|
|
? quickCommandToDraft(command)
|
|
: createEmptyQuickCommandDraft(repoId ? { type: 'repo', repoId } : { type: 'global' })
|
|
)
|
|
setView('editor')
|
|
}
|
|
|
|
const handleLaunch = (command: TerminalQuickCommand) => {
|
|
if (onLaunch(command)) {
|
|
onClose()
|
|
}
|
|
}
|
|
|
|
const handleDelete = (command: TerminalQuickCommand) => {
|
|
// Why: quick commands sync with desktop, so an accidental one-tap delete
|
|
// removes shared data rather than only dismissing a local row.
|
|
Alert.alert(
|
|
t('session.quickCommandsSheet.deleteConfirmTitle', 'Delete "{{label}}"?', {
|
|
label: command.label || t('session.quickCommandsSheet.untitled', 'Untitled')
|
|
}),
|
|
t(
|
|
'session.quickCommandsSheet.deleteConfirmMessage',
|
|
'This quick command will be removed from your saved list.'
|
|
),
|
|
[
|
|
{ text: t('common.cancel', 'Cancel'), style: 'cancel' },
|
|
{
|
|
text: t('common.delete', 'Delete'),
|
|
style: 'destructive',
|
|
onPress: () => {
|
|
void persist({ type: 'delete', id: command.id })
|
|
}
|
|
}
|
|
]
|
|
)
|
|
}
|
|
|
|
const handleSave = async () => {
|
|
if (!draft || saveOwnerRef.current !== null) {
|
|
return
|
|
}
|
|
const built = draftToQuickCommand(draft, agentPromptMaxLength)
|
|
if (!built) {
|
|
return
|
|
}
|
|
// Why: state cannot lock out a second tap until React commits the disabled UI.
|
|
const saveOwner = Symbol()
|
|
saveOwnerRef.current = saveOwner
|
|
setSaving(true)
|
|
const savedRevision = draftRevisionRef.current
|
|
const savedEditor = editorGenerationRef.current
|
|
try {
|
|
const ok = await persist({ type: 'upsert', command: built })
|
|
if (ok && draftRevisionRef.current === savedRevision) {
|
|
setView('list')
|
|
setDraft(null)
|
|
} else if (ok && editorGenerationRef.current === savedEditor) {
|
|
setDraft((current) => (current ? { ...current, id: built.id } : current))
|
|
}
|
|
} finally {
|
|
if (saveOwnerRef.current === saveOwner) {
|
|
saveOwnerRef.current = null
|
|
setSaving(false)
|
|
}
|
|
}
|
|
}
|
|
|
|
const title =
|
|
view === 'editor'
|
|
? draft?.id
|
|
? t('session.quickCommandsSheet.editTitle', 'Edit Quick Command')
|
|
: t('session.quickCommandsSheet.addTitle', 'Add Quick Command')
|
|
: view === 'agent'
|
|
? t('session.quickCommandsSheet.chooseAgentTitle', 'Choose Agent')
|
|
: t('session.quickCommandsSheet.title', 'Quick Commands')
|
|
|
|
return (
|
|
<BottomDrawer visible={visible} onClose={onClose}>
|
|
<View style={styles.header}>
|
|
{view === 'list' ? (
|
|
<View style={styles.backSpacer} />
|
|
) : (
|
|
<Pressable
|
|
style={({ pressed }) => [styles.backButton, pressed && styles.pressed]}
|
|
onPress={() => {
|
|
draftRevisionRef.current += 1
|
|
setView(view === 'agent' ? 'editor' : 'list')
|
|
}}
|
|
accessibilityRole="button"
|
|
accessibilityLabel={t('common.back', 'Back')}
|
|
>
|
|
<ChevronLeft size={18} color={colors.textSecondary} />
|
|
</Pressable>
|
|
)}
|
|
<Text style={styles.title}>{title}</Text>
|
|
<View style={styles.backSpacer} />
|
|
</View>
|
|
|
|
{view === 'editor' && draft ? (
|
|
<View style={styles.editorDesc}>
|
|
<Text style={styles.descText}>
|
|
{t(
|
|
'session.quickCommandsSheet.editorDescription',
|
|
'Save terminal commands or agent prompts for quick access.'
|
|
)}
|
|
</Text>
|
|
</View>
|
|
) : null}
|
|
|
|
{view === 'list' ? (
|
|
<QuickCommandsList
|
|
repoCommands={repoCommands}
|
|
globalCommands={globalCommands}
|
|
totalCount={searchableCommands.length}
|
|
query={query}
|
|
loading={loading}
|
|
disabled={!ready}
|
|
error={error}
|
|
onQueryChange={setQuery}
|
|
onLaunch={handleLaunch}
|
|
onEdit={openEditor}
|
|
onDelete={handleDelete}
|
|
onAdd={() => openEditor()}
|
|
/>
|
|
) : null}
|
|
|
|
{view === 'editor' && draft ? (
|
|
<QuickCommandEditorForm
|
|
draft={draft}
|
|
mode={draft.id ? 'edit' : 'add'}
|
|
saving={saving || !ready}
|
|
error={error}
|
|
repoId={repoId}
|
|
repoName={repoName}
|
|
agentPromptMaxLength={agentPromptMaxLength}
|
|
onChange={(patch) => {
|
|
draftRevisionRef.current += 1
|
|
setDraft((current) => (current ? { ...current, ...patch } : current))
|
|
}}
|
|
onOpenAgentPicker={() => {
|
|
draftRevisionRef.current += 1
|
|
setView('agent')
|
|
}}
|
|
onCancel={() => {
|
|
draftRevisionRef.current += 1
|
|
setView('list')
|
|
setDraft(null)
|
|
}}
|
|
onSave={() => void handleSave()}
|
|
/>
|
|
) : null}
|
|
|
|
{view === 'agent' && draft ? (
|
|
<QuickCommandAgentPicker
|
|
selected={draft.agent}
|
|
onSelect={(agent) => {
|
|
draftRevisionRef.current += 1
|
|
setDraft((current) => (current ? { ...current, agent } : current))
|
|
setView('editor')
|
|
}}
|
|
/>
|
|
) : null}
|
|
</BottomDrawer>
|
|
)
|
|
}
|
|
|
|
const styles = themedStyleSheet(() => ({
|
|
header: { flexDirection: 'row', alignItems: 'center', paddingBottom: spacing.sm },
|
|
backButton: {
|
|
width: 30,
|
|
height: 30,
|
|
borderRadius: 15,
|
|
alignItems: 'center',
|
|
justifyContent: 'center'
|
|
},
|
|
backSpacer: { width: 30 },
|
|
title: {
|
|
flex: 1,
|
|
fontSize: 16,
|
|
fontWeight: '700',
|
|
color: colors.textPrimary,
|
|
textAlign: 'center'
|
|
},
|
|
pressed: { backgroundColor: colors.bgRaised },
|
|
editorDesc: { paddingHorizontal: spacing.xs, paddingBottom: spacing.sm },
|
|
descText: { fontSize: 12, color: colors.textMuted }
|
|
}))
|