diff --git a/src/renderer/src/components/feature-tips/CliFeatureTipVisual.tsx b/src/renderer/src/components/feature-tips/CliFeatureTipVisual.tsx
new file mode 100644
index 000000000..55e1e3531
--- /dev/null
+++ b/src/renderer/src/components/feature-tips/CliFeatureTipVisual.tsx
@@ -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 (
+
+
+
+
+
+
+
+
+
+
+ ●Claude Code session started
+
+ {CLI_AGENT_COMMANDS.map((command, index) => {
+ const isVisible = index < visibleCommandCount
+ const isCurrentLine = isVisible && index === visibleCommandCount - 1
+ return (
+
+ >
+ {command}
+ {isCurrentLine ? (
+
+ ) : null}
+
+ )
+ })}
+
+
+
+
+
+ )
+}
diff --git a/src/renderer/src/components/feature-tips/CliSkillSetupTerminal.tsx b/src/renderer/src/components/feature-tips/CliSkillSetupTerminal.tsx
new file mode 100644
index 000000000..02c427cdc
--- /dev/null
+++ b/src/renderer/src/components/feature-tips/CliSkillSetupTerminal.tsx
@@ -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 => {
+ 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 (
+
+
+
+ {ORCA_CLI_ORCHESTRATION_SKILL_INSTALL_COMMAND}
+
+
+
+
+
+
+ Copy command
+
+
+
+
+
+ )
+}
diff --git a/src/renderer/src/components/feature-tips/FeatureTipsModal.tsx b/src/renderer/src/components/feature-tips/FeatureTipsModal.tsx
index a6afa18d7..c60371e8d 100644
--- a/src/renderer/src/components/feature-tips/FeatureTipsModal.tsx
+++ b/src/renderer/src/components/feature-tips/FeatureTipsModal.tsx
@@ -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 (
-
-
-
-
-
-
-
-
-
-
- ●Claude Code session started
-
- {CLI_AGENT_COMMANDS.map((command, index) => {
- const isVisible = index < visibleCommandCount
- const isCurrentLine = isVisible && index === visibleCommandCount - 1
- return (
-
- >
- {command}
- {isCurrentLine ? (
-
- ) : null}
-
- )
- })}
-
-
-
-
-
- )
-}
-
function FeatureTipVisual({ tip }: { tip: FeatureTip }): JSX.Element {
if (tip.action === 'setup-cli') {
return
@@ -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 (