Polish CLI feature tip terminal transition (#4344)

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Brennan Benson 2026-05-31 20:23:35 -07:00 committed by GitHub
parent 1f1aaee78c
commit eaae2dd4ce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 237 additions and 144 deletions

View File

@ -0,0 +1,103 @@
import { useEffect, useState, type JSX } from 'react'
import { AgentsOrchestrationVisual } from '@/components/feature-wall/AgentsOrchestrationVisual'
import {
ORCHESTRATION_CLI_COMMAND_LOOP_MS,
ORCHESTRATION_CLI_COMMAND_TIMINGS_MS
} from '@/components/feature-wall/agents-orchestration/orchestration-types'
import { usePrefersReducedMotion } from '@/components/feature-wall/feature-wall-modal-helpers'
const CLI_AGENT_COMMANDS = [
'orca worktree create --name auth-pr-1',
'orca worktree create --name auth-pr-2',
'orca orchestration dispatch --task pr1 --to w1',
'orca orchestration dispatch --task pr2 --to w2'
]
export function CliFeatureTipVisual(): JSX.Element {
const reducedMotion = usePrefersReducedMotion()
const [animatedVisibleCommandCount, setAnimatedVisibleCommandCount] = useState(0)
// Why: reduced-motion users should see the static completed state without a
// post-render state repair; only the animated path needs timer-backed state.
const visibleCommandCount = reducedMotion
? CLI_AGENT_COMMANDS.length
: animatedVisibleCommandCount
useEffect(() => {
if (reducedMotion) {
return
}
let cancelled = false
const timeouts: number[] = []
const later = (fn: () => void, ms: number): void => {
timeouts.push(window.setTimeout(() => !cancelled && fn(), ms))
}
// Why: terminal lines mirror the orchestration tour beat timings so the
// shell shows each command as the parent agent runs it.
const runOnce = (): void => {
setAnimatedVisibleCommandCount(0)
ORCHESTRATION_CLI_COMMAND_TIMINGS_MS.forEach((ms, index) => {
later(() => setAnimatedVisibleCommandCount(index + 1), ms)
})
later(runOnce, ORCHESTRATION_CLI_COMMAND_LOOP_MS)
}
runOnce()
return () => {
cancelled = true
timeouts.forEach((id) => window.clearTimeout(id))
}
}, [reducedMotion])
return (
<div
className="relative flex min-h-[27rem] flex-col overflow-hidden bg-muted/60 px-6 py-7"
aria-hidden="true"
>
<div className="absolute inset-x-0 top-0 h-16 bg-gradient-to-b from-background/55 to-transparent" />
<div className="relative rounded-lg border border-border/70 bg-card/95 shadow-xs">
<div className="flex items-center gap-2 border-b border-border/70 px-3 py-2">
<span className="size-2 rounded-full bg-muted-foreground/35" />
<span className="size-2 rounded-full bg-muted-foreground/25" />
<span className="size-2 rounded-full bg-muted-foreground/20" />
</div>
<div className="space-y-1.5 px-3 py-3 font-mono text-[10.5px] leading-[1.35] text-foreground">
<div className="truncate text-muted-foreground">
<span className="mr-1.5 text-foreground"></span>Claude Code session started
</div>
{CLI_AGENT_COMMANDS.map((command, index) => {
const isVisible = index < visibleCommandCount
const isCurrentLine = isVisible && index === visibleCommandCount - 1
return (
<div
key={command}
className={`truncate ${isVisible ? 'animate-cli-tip-command-line' : 'invisible'}`}
>
<span className="text-foreground">&gt; </span>
<span>{command}</span>
{isCurrentLine ? (
<span className="animate-cli-tip-caret ml-0.5 inline-block h-3 w-1 translate-y-0.5 rounded-sm bg-foreground/70" />
) : null}
</div>
)
})}
</div>
</div>
<div className="cli-tip-orchestration-frame relative mt-5 flex h-[17rem] items-center justify-center overflow-hidden rounded-lg border border-border/70 bg-background/80 px-5 shadow-xs">
<div className="origin-center">
<AgentsOrchestrationVisual
activeStepId="orchestration"
reducedMotion={reducedMotion}
widthPx={350}
heightPx={252}
orchestrationCreatedChildCount={Math.min(visibleCommandCount, 2)}
orchestrationLoopMs={ORCHESTRATION_CLI_COMMAND_LOOP_MS}
orchestrationShowResponseBeats={false}
/>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,54 @@
import { Copy } from 'lucide-react'
import { toast } from 'sonner'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { OnboardingInlineCommandTerminal } from '@/components/onboarding/OnboardingInlineCommandTerminal'
import { ORCA_CLI_ORCHESTRATION_SKILL_INSTALL_COMMAND } from '@/lib/agent-feature-install-commands'
export function CliSkillSetupTerminal(): React.JSX.Element {
const handleCopySkillCommand = async (): Promise<void> => {
try {
await window.api.ui.writeClipboardText(ORCA_CLI_ORCHESTRATION_SKILL_INSTALL_COMMAND)
toast.success('Copied the skill install command.')
} catch (error) {
toast.error(error instanceof Error ? error.message : 'Failed to copy skill command.')
}
}
return (
<div className="min-w-0">
<div className="flex min-w-0 items-center gap-2 rounded-md border border-border bg-muted/35 px-3 py-2">
<code className="scrollbar-sleek min-w-0 flex-1 overflow-x-auto whitespace-nowrap font-mono text-xs text-muted-foreground">
{ORCA_CLI_ORCHESTRATION_SKILL_INSTALL_COMMAND}
</code>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon-sm"
className="shrink-0"
onClick={() => void handleCopySkillCommand()}
aria-label="Copy skill install command"
>
<Copy className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={4}>
Copy command
</TooltipContent>
</Tooltip>
</div>
<OnboardingInlineCommandTerminal
command={ORCA_CLI_ORCHESTRATION_SKILL_INSTALL_COMMAND}
title="Skill setup"
ariaLabel="Orca CLI and orchestration skill install terminal"
description="Press Enter to install the Orca CLI orchestration skill for your agents."
terminalHeightPx={280}
terminalTopMarginPx={8}
descriptionPaddingClassName="px-4 py-2"
autoScrollIntoView={false}
worktreeId="feature-tip-cli-skills-terminal"
/>
</div>
)
}

View File

@ -1,17 +1,9 @@
import { useEffect, useState, type JSX } from 'react'
import { useEffect, useRef, useState, type JSX } from 'react'
import { Loader2, Mic } from 'lucide-react'
import { toast } from 'sonner'
import { getDefaultVoiceSettings } from '../../../../shared/constants'
import type { FeatureTip } from '../../../../shared/feature-tips'
import { Button } from '@/components/ui/button'
import { AgentsOrchestrationVisual } from '@/components/feature-wall/AgentsOrchestrationVisual'
import {
ORCHESTRATION_CLI_COMMAND_LOOP_MS,
ORCHESTRATION_CLI_COMMAND_TIMINGS_MS
} from '@/components/feature-wall/agents-orchestration/orchestration-types'
import { usePrefersReducedMotion } from '@/components/feature-wall/feature-wall-modal-helpers'
import { OnboardingInlineCommandTerminal } from '@/components/onboarding/OnboardingInlineCommandTerminal'
import { ORCA_CLI_ORCHESTRATION_SKILL_INSTALL_COMMAND } from '@/lib/agent-feature-install-commands'
import {
ORCHESTRATION_ENABLED_STORAGE_KEY,
ORCHESTRATION_SETUP_DISMISSED_STORAGE_KEY,
@ -26,6 +18,8 @@ import {
DialogTitle
} from '@/components/ui/dialog'
import { useAppStore } from '@/store'
import { CliFeatureTipVisual } from './CliFeatureTipVisual'
import { CliSkillSetupTerminal } from './CliSkillSetupTerminal'
import { installCliFromFeatureTip } from './feature-tip-cli-install-action'
import { getFeatureTipForModal } from './feature-tip-modal-state'
import {
@ -36,12 +30,6 @@ import {
import { useMountedRef } from '@/hooks/useMountedRef'
const WAVEFORM_BAR_HEIGHTS = [30, 60, 90, 70, 100, 50, 80, 35, 65]
const CLI_AGENT_COMMANDS = [
'orca worktree create --name auth-pr-1',
'orca worktree create --name auth-pr-2',
'orca orchestration dispatch --task pr1 --to w1',
'orca orchestration dispatch --task pr2 --to w2'
]
function WorktreePromptTerm({ children }: { children: string }): JSX.Element {
return (
@ -51,95 +39,6 @@ function WorktreePromptTerm({ children }: { children: string }): JSX.Element {
)
}
function CliFeatureTipVisual(): JSX.Element {
const reducedMotion = usePrefersReducedMotion()
const [animatedVisibleCommandCount, setAnimatedVisibleCommandCount] = useState(0)
// Why: reduced-motion users should see the static completed state without a
// post-render state repair; only the animated path needs timer-backed state.
const visibleCommandCount = reducedMotion
? CLI_AGENT_COMMANDS.length
: animatedVisibleCommandCount
useEffect(() => {
if (reducedMotion) {
return
}
let cancelled = false
const timeouts: number[] = []
const later = (fn: () => void, ms: number): void => {
timeouts.push(window.setTimeout(() => !cancelled && fn(), ms))
}
// Why: terminal lines mirror the orchestration tour beat timings so the
// shell shows each command as the parent agent runs it.
const runOnce = (): void => {
setAnimatedVisibleCommandCount(0)
ORCHESTRATION_CLI_COMMAND_TIMINGS_MS.forEach((ms, index) => {
later(() => setAnimatedVisibleCommandCount(index + 1), ms)
})
later(runOnce, ORCHESTRATION_CLI_COMMAND_LOOP_MS)
}
runOnce()
return () => {
cancelled = true
timeouts.forEach((id) => window.clearTimeout(id))
}
}, [reducedMotion])
return (
<div
className="relative flex min-h-[27rem] flex-col overflow-hidden bg-muted/60 px-6 py-7"
aria-hidden="true"
>
<div className="absolute inset-x-0 top-0 h-16 bg-gradient-to-b from-background/55 to-transparent" />
<div className="relative rounded-lg border border-border/70 bg-card/95 shadow-xs">
<div className="flex items-center gap-2 border-b border-border/70 px-3 py-2">
<span className="size-2 rounded-full bg-muted-foreground/35" />
<span className="size-2 rounded-full bg-muted-foreground/25" />
<span className="size-2 rounded-full bg-muted-foreground/20" />
</div>
<div className="space-y-1.5 px-3 py-3 font-mono text-[10.5px] leading-[1.35] text-foreground">
<div className="truncate text-muted-foreground">
<span className="mr-1.5 text-foreground"></span>Claude Code session started
</div>
{CLI_AGENT_COMMANDS.map((command, index) => {
const isVisible = index < visibleCommandCount
const isCurrentLine = isVisible && index === visibleCommandCount - 1
return (
<div
key={command}
className={`truncate ${isVisible ? 'animate-cli-tip-command-line' : 'invisible'}`}
>
<span className="text-amber-600">&gt; </span>
<span>{command}</span>
{isCurrentLine ? (
<span className="animate-cli-tip-caret ml-0.5 inline-block h-3 w-1 translate-y-0.5 rounded-sm bg-foreground/70" />
) : null}
</div>
)
})}
</div>
</div>
<div className="cli-tip-orchestration-frame relative mt-5 flex h-[17rem] items-center justify-center overflow-hidden rounded-lg border border-border/70 bg-background/80 px-5 shadow-xs">
<div className="origin-center">
<AgentsOrchestrationVisual
activeStepId="orchestration"
reducedMotion={reducedMotion}
widthPx={350}
heightPx={252}
orchestrationCreatedChildCount={Math.min(visibleCommandCount, 2)}
orchestrationLoopMs={ORCHESTRATION_CLI_COMMAND_LOOP_MS}
orchestrationShowResponseBeats={false}
/>
</div>
</div>
</div>
)
}
function FeatureTipVisual({ tip }: { tip: FeatureTip }): JSX.Element {
if (tip.action === 'setup-cli') {
return <CliFeatureTipVisual />
@ -219,6 +118,8 @@ export default function FeatureTipsModal(): JSX.Element | null {
const markFeatureTipsSeen = useAppStore((s) => s.markFeatureTipsSeen)
const modalData = useAppStore((s) => s.modalData)
const mountedRef = useMountedRef()
const activeModalRef = useRef(activeModal)
const setupRequestIdRef = useRef(0)
const [primaryBusy, setPrimaryBusy] = useState(false)
const [skillTerminalOpen, setSkillTerminalOpen] = useState(false)
const isOpen = activeModal === 'feature-tips'
@ -230,6 +131,10 @@ export default function FeatureTipsModal(): JSX.Element | null {
settings
})
useEffect(() => {
activeModalRef.current = activeModal
}, [activeModal])
const markCurrentTipSeen = (): void => {
if (currentTip) {
markFeatureTipsSeen([currentTip.id])
@ -238,14 +143,19 @@ export default function FeatureTipsModal(): JSX.Element | null {
const handleOpenChange = (open: boolean): void => {
if (!open) {
setupRequestIdRef.current += 1
markCurrentTipSeen()
setSkillTerminalOpen(false)
setPrimaryBusy(false)
closeModal()
}
}
const handleSkip = (): void => {
setupRequestIdRef.current += 1
markCurrentTipSeen()
setSkillTerminalOpen(false)
setPrimaryBusy(false)
closeModal()
}
@ -281,6 +191,14 @@ export default function FeatureTipsModal(): JSX.Element | null {
break
}
case 'setup-cli': {
const setupRequestId = setupRequestIdRef.current + 1
setupRequestIdRef.current = setupRequestId
// Why: this modal is lazily mounted; closing it does not unmount the
// component, so async install results must not reopen UI after dismissal.
const canApplySetupResult = (): boolean =>
mountedRef.current &&
activeModalRef.current === 'feature-tips' &&
setupRequestIdRef.current === setupRequestId
const telemetrySource = getOrcaCliFeatureTipTelemetrySource(modalData.source)
trackOrcaCliFeatureTipSetupClicked(telemetrySource)
setPrimaryBusy(true)
@ -288,17 +206,17 @@ export default function FeatureTipsModal(): JSX.Element | null {
const result = await installCliFromFeatureTip(() => window.api.cli.install())
if (result.kind === 'installed') {
trackOrcaCliFeatureTipSetupResult(telemetrySource, 'installed')
enableOrchestrationSkillSetup()
if (!mountedRef.current) {
if (!canApplySetupResult()) {
return
}
enableOrchestrationSkillSetup()
toast.success('Registered `orca` in PATH.')
setSkillTerminalOpen(true)
return
}
trackOrcaCliFeatureTipSetupResult(telemetrySource, 'needs_attention')
if (!mountedRef.current) {
if (!canApplySetupResult()) {
return
}
toast.warning('Orca CLI needs attention', {
@ -313,21 +231,21 @@ export default function FeatureTipsModal(): JSX.Element | null {
message.includes('Development mode uses a generated launcher for validation only')
) {
trackOrcaCliFeatureTipSetupResult(telemetrySource, 'dev_preview')
enableOrchestrationSkillSetup()
if (!mountedRef.current) {
if (!canApplySetupResult()) {
return
}
enableOrchestrationSkillSetup()
toast.info('Development preview: opening skills setup terminal.')
setSkillTerminalOpen(true)
return
}
trackOrcaCliFeatureTipSetupResult(telemetrySource, 'failed')
if (mountedRef.current) {
if (canApplySetupResult()) {
toast.error(message)
}
} finally {
if (mountedRef.current) {
if (canApplySetupResult()) {
setPrimaryBusy(false)
}
}
@ -342,43 +260,47 @@ export default function FeatureTipsModal(): JSX.Element | null {
if (currentTip.action === 'setup-cli') {
return (
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
<DialogContent className="grid gap-0 overflow-hidden p-0 sm:max-w-4xl md:grid-cols-[minmax(22rem,0.95fr)_minmax(26rem,1.05fr)]">
<div className="flex min-h-[27rem] flex-col justify-between px-8 py-9">
<DialogHeader className="gap-4 text-left">
<div className="space-y-3">
<DialogTitle className="max-w-[22rem] text-3xl font-semibold leading-tight tracking-tight">
<DialogContent
className="!flex max-h-[calc(100vh-2rem)] flex-col gap-0 overflow-hidden p-0 sm:max-w-4xl md:!h-[min(31rem,calc(100vh-2rem))] md:!flex-row"
showCloseButton={!skillTerminalOpen}
>
<div
className={`scrollbar-sleek flex min-h-0 min-w-0 flex-1 flex-col justify-between overflow-y-auto px-8 py-9 transition-[flex-basis] duration-500 ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none md:shrink-0 ${
skillTerminalOpen ? 'basis-auto md:basis-full' : 'basis-auto md:basis-[47.5%]'
}`}
>
<DialogHeader className={`${skillTerminalOpen ? 'gap-2' : 'gap-4'} text-left`}>
<div>
<DialogTitle
className={`text-3xl font-semibold leading-tight tracking-tight ${
skillTerminalOpen ? 'max-w-2xl' : 'max-w-[22rem]'
}`}
>
{currentTip.title}
</DialogTitle>
<DialogDescription className="max-w-sm text-sm leading-relaxed">
<DialogDescription className="mt-3 max-w-2xl text-sm leading-relaxed">
{currentTip.description}
</DialogDescription>
{skillTerminalOpen ? null : (
<div className="max-w-sm space-y-2 rounded-md border border-border/70 bg-muted/35 p-3 text-sm leading-relaxed text-muted-foreground">
<p className="font-medium text-foreground">Try asking:</p>
<p>
Split this PR into two <WorktreePromptTerm>worktrees</WorktreePromptTerm> and
create PRs for each.
</p>
<p>
When the agent in <WorktreePromptTerm>worktree</WorktreePromptTerm> X
finishes, send it the review task.
</p>
</div>
)}
<div
aria-hidden={skillTerminalOpen}
className={`max-w-sm space-y-2 overflow-hidden rounded-md border text-sm leading-relaxed text-muted-foreground transition-[max-height,opacity,transform,margin,padding,border-color] duration-300 ease-out motion-reduce:transition-none ${
skillTerminalOpen
? 'pointer-events-none mt-0 max-h-0 -translate-y-2 border-transparent p-0 opacity-0'
: 'mt-3 max-h-64 translate-y-0 border-border/70 bg-muted/35 p-3 opacity-100'
}`}
>
<p className="font-medium text-foreground">Try asking:</p>
<p>
Split this PR into two <WorktreePromptTerm>worktrees</WorktreePromptTerm> and
create PRs for each.
</p>
<p>
When the agent in <WorktreePromptTerm>worktree</WorktreePromptTerm> X finishes,
send it the review task.
</p>
</div>
</div>
{skillTerminalOpen ? (
<OnboardingInlineCommandTerminal
command={ORCA_CLI_ORCHESTRATION_SKILL_INSTALL_COMMAND}
title="Skill setup"
ariaLabel="Orca CLI and orchestration skill install terminal"
description="Press Enter to install the Orca CLI and orchestration skills for your agents."
terminalHeightPx={150}
terminalTopMarginPx={4}
descriptionPaddingClassName="px-4 py-2"
autoScrollIntoView={false}
worktreeId="feature-tip-cli-skills-terminal"
/>
) : null}
{skillTerminalOpen ? <CliSkillSetupTerminal /> : null}
</DialogHeader>
<DialogFooter className="mt-8 flex sm:justify-stretch">
@ -398,7 +320,21 @@ export default function FeatureTipsModal(): JSX.Element | null {
)}
</DialogFooter>
</div>
<FeatureTipVisual tip={currentTip} />
<div
className={`min-h-0 min-w-0 shrink-0 overflow-hidden transition-[flex-basis,max-height] duration-500 ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none ${
skillTerminalOpen
? 'pointer-events-none max-h-0 basis-0 md:max-h-none md:basis-0'
: 'max-h-[40rem] basis-auto md:basis-[52.5%]'
}`}
>
<div
className={`h-full transition-[transform,opacity] duration-500 ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none md:w-[29.4rem] ${
skillTerminalOpen ? 'translate-x-full opacity-0' : 'translate-x-0 opacity-100'
}`}
>
{skillTerminalOpen ? null : <FeatureTipVisual tip={currentTip} />}
</div>
</div>
</DialogContent>
</Dialog>
)