mirror of
https://github.com/stablyai/orca.git
synced 2026-09-28 08:02:43 +00:00
#14397 split `shared/types.ts` into 46 per-domain modules but kept the path as a re-export barrel so the import sites did not have to change. This removes the barrel: every consumer now imports from the module that actually declares the type, and `src/shared/types.ts` is deleted. Barrels hide where a type lives, make every consumer look like it depends on the whole domain, and let an unrelated edit invalidate a module that ~2,000 files transitively import. 2,323 import declarations across 2,321 files. Rewritten mechanically: each specifier was resolved to an absolute path via the TypeScript AST and recomputed, rather than string-substituted, so alias forms (`@/../../shared/ types`) and per-specifier `type` modifiers survive. Four cases the mechanical pass had to handle, each found by a gate rather than by reading the diff: - Modules inside `src/shared` import the barrel as `./types`, not `shared/types`. A pre-filter on the latter string skipped 176 of them and left imports dangling at a deleted file, which surfaced as confusing `Property 'x' is optional in type 'Repo' but required in Pick<Repo, ...>` errors rather than "module not found". - The barrel RENAMED one type on the way through (`WorkspaceSource as WorkspaceCreateTelemetrySource`), so the original name in the owning module has to be re-aliased at each consumer. - Three test files put `;(globalThis as ...)` on the line after the import. TypeScript parses that `;` as the import statement's terminator, so replacing through `statement.getEnd()` deletes it and breaks ASI. The rewrite now stops at the module specifier. - A file that already imported directly from a module got a SECOND import from it, because the barrel re-exported those same names — which trips `import/no-duplicates` under `--deny-warnings`. A post-pass merges declarations sharing a specifier and type-only-ness; the `import type` plus `import` pair from one module is left alone, since that form is allowed. Splitting one barrel import into several genuinely adds lines, which pushed `terminal-layout-pty-ownership.ts` to 301 counted lines: its 107-character import must wrap, and neither local type collapses onto one line (101 and 116 characters). Rather than contort a type declaration to fit a line budget, `collectLeafIds` and `pruneLeaves` move to `terminal-pane-layout-tree.ts` — they are pure structural operations on the layout tree and independent of PTY ownership. `visible-worktrees.ts` similarly loses its own mini-barrel re-export of `isDefaultBranchWorkspace`, with the four real consumers repointed at the declaring module. No `max-lines` bypass added. Verified: cold `tsc --noEmit` green on node, cli, and web (buildinfo deleted first — these projects are `composite: true` and reuse stale caches); the full `pnpm lint` green, not just bare oxlint — the narrower local check is what let the duplicate imports reach CI; max-lines ratchet OK at 344.
252 lines
7.4 KiB
TypeScript
252 lines
7.4 KiB
TypeScript
import { useMemo, useRef, useState } from 'react'
|
|
import { Alert, View, Text, Pressable, StyleSheet } from 'react-native'
|
|
import { ChevronLeft } from 'lucide-react-native'
|
|
import { colors, spacing } from '../theme/mobile-theme'
|
|
import { BottomDrawer } from '../components/BottomDrawer'
|
|
import type { RpcClient } from '../transport/rpc-client'
|
|
import type { TerminalQuickCommand } from '../../../src/shared/terminal-quick-command-types'
|
|
import {
|
|
getQuickCommandPreview,
|
|
MAX_QUICK_COMMANDS,
|
|
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'
|
|
|
|
type Props = {
|
|
visible: boolean
|
|
onClose: () => void
|
|
client: RpcClient | null
|
|
repoId: string | null
|
|
repoName: string | null
|
|
onLaunch: (command: TerminalQuickCommand) => boolean
|
|
}
|
|
|
|
type SheetView = 'list' | 'editor' | 'agent'
|
|
|
|
export function QuickCommandsSheet({
|
|
visible,
|
|
onClose,
|
|
client,
|
|
repoId,
|
|
repoName,
|
|
onLaunch
|
|
}: Props) {
|
|
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 savingRef = useRef(false)
|
|
|
|
const [wasVisible, setWasVisible] = useState(visible)
|
|
if (visible !== wasVisible) {
|
|
setWasVisible(visible)
|
|
if (visible) {
|
|
setView('list')
|
|
setQuery('')
|
|
setDraft(null)
|
|
}
|
|
}
|
|
|
|
// Why: prompt bodies can total ~240 KB. 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) => {
|
|
// Why: the host rejects full-list updates above this cap; existing rows
|
|
// must remain editable/deletable when creation is no longer possible.
|
|
if (!command && commands.length >= MAX_QUICK_COMMANDS) {
|
|
return
|
|
}
|
|
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(
|
|
`Delete "${command.label || 'Untitled'}"?`,
|
|
'This quick command will be removed from your saved list.',
|
|
[
|
|
{ text: 'Cancel', style: 'cancel' },
|
|
{
|
|
text: 'Delete',
|
|
style: 'destructive',
|
|
onPress: () => {
|
|
void persist({ type: 'delete', id: command.id })
|
|
}
|
|
}
|
|
]
|
|
)
|
|
}
|
|
|
|
const handleSave = async () => {
|
|
if (!draft || savingRef.current) {
|
|
return
|
|
}
|
|
const built = draftToQuickCommand(draft)
|
|
if (!built) {
|
|
return
|
|
}
|
|
// Why: state cannot lock out a second tap until React commits the disabled UI.
|
|
savingRef.current = true
|
|
setSaving(true)
|
|
try {
|
|
const ok = await persist({ type: 'upsert', command: built })
|
|
if (ok) {
|
|
setView('list')
|
|
setDraft(null)
|
|
}
|
|
} finally {
|
|
savingRef.current = false
|
|
setSaving(false)
|
|
}
|
|
}
|
|
|
|
const title =
|
|
view === 'editor'
|
|
? draft?.id
|
|
? 'Edit Quick Command'
|
|
: 'Add Quick Command'
|
|
: view === 'agent'
|
|
? 'Choose Agent'
|
|
: '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={() => setView(view === 'agent' ? 'editor' : 'list')}
|
|
accessibilityLabel="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}>
|
|
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}
|
|
canAdd={commands.length < MAX_QUICK_COMMANDS}
|
|
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}
|
|
onChange={(patch) =>
|
|
setDraft((current) => (current ? { ...current, ...patch } : current))
|
|
}
|
|
onOpenAgentPicker={() => setView('agent')}
|
|
onCancel={() => {
|
|
setView('list')
|
|
setDraft(null)
|
|
}}
|
|
onSave={() => void handleSave()}
|
|
/>
|
|
) : null}
|
|
|
|
{view === 'agent' && draft ? (
|
|
<QuickCommandAgentPicker
|
|
selected={draft.agent}
|
|
onSelect={(agent) => {
|
|
setDraft((current) => (current ? { ...current, agent } : current))
|
|
setView('editor')
|
|
}}
|
|
/>
|
|
) : null}
|
|
</BottomDrawer>
|
|
)
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
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 }
|
|
})
|