feat(desktop): persist onboarding state to file system

- Add AppState module in core for managing app state persistence
- Add app-state IPC handlers for reading/writing onboarding state
- Hydrate onboarding state from file system on app startup
- Prevent flash by showing blank screen during hydration
- Update onboarding store to sync with file system
- Improve MulticaIcon with enhanced animation states
- Minor UI fixes in chat and device list components

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Naiyuan Qing 2026-02-12 10:53:04 +08:00
parent cf3ad1db91
commit eb4e1f57b1
20 changed files with 360 additions and 67 deletions

View file

@ -19,10 +19,12 @@ interface ChatInputProps {
onSubmit?: (value: string) => void;
disabled?: boolean;
placeholder?: string;
/** Initial value to pre-fill the input */
defaultValue?: string;
}
export const ChatInput = forwardRef<ChatInputRef, ChatInputProps>(
function ChatInput({ onSubmit, disabled, placeholder = "Type a message..." }, ref) {
function ChatInput({ onSubmit, disabled, placeholder = "Type a message...", defaultValue }, ref) {
// Use ref to avoid stale closure in Tiptap keydown handler
const onSubmitRef = useRef(onSubmit);
onSubmitRef.current = onSubmit;
@ -45,6 +47,7 @@ export const ChatInput = forwardRef<ChatInputRef, ChatInputProps>(
}),
Placeholder.configure({ placeholder }),
],
content: defaultValue ? `<p>${defaultValue}</p>` : "",
immediatelyRender: false,
// Scroll cursor into view on every content change (e.g., Shift+Enter newlines)
onUpdate({ editor }) {

View file

@ -41,6 +41,8 @@ export interface ChatViewProps {
onDisconnect?: () => void;
/** Optional action button in the error banner (e.g. "Configure Provider") */
errorAction?: { label: string; onClick: () => void };
/** Initial prompt to pre-fill the input (e.g., from onboarding) */
initialPrompt?: string;
}
export function ChatView({
@ -57,6 +59,7 @@ export function ChatView({
resolveApproval,
onDisconnect,
errorAction,
initialPrompt,
}: ChatViewProps) {
const mainRef = useRef<HTMLElement>(null);
const sentinelRef = useRef<HTMLDivElement>(null);
@ -259,6 +262,7 @@ export function ChatView({
onSubmit={sendMessage}
disabled={isLoading || (!!error && error.code !== 'AGENT_ERROR')}
placeholder={error && error.code !== 'AGENT_ERROR' ? "Connection error" : "Ask your Agent..."}
defaultValue={initialPrompt}
/>
</footer>
</div>

View file

@ -6,6 +6,14 @@ interface MulticaIconProps extends React.ComponentProps<"span"> {
* If true, play a one-time entrance spin animation.
*/
animate?: boolean;
/**
* If true, disable hover spin animation.
*/
noSpin?: boolean;
/**
* If true, show a border around the icon.
*/
bordered?: boolean;
}
/**
@ -16,6 +24,8 @@ interface MulticaIconProps extends React.ComponentProps<"span"> {
export function MulticaIcon({
className,
animate = false,
noSpin = false,
bordered = false,
...props
}: MulticaIconProps) {
const [entranceDone, setEntranceDone] = useState(!animate);
@ -26,12 +36,47 @@ export function MulticaIcon({
return () => clearTimeout(timer);
}, [animate]);
if (bordered) {
return (
<span
className={cn(
"inline-flex items-center justify-center p-1.5 border border-border rounded-md",
className
)}
aria-hidden="true"
{...props}
>
<span
className={cn(
"block size-3.5",
!entranceDone && "animate-entrance-spin",
entranceDone && !noSpin && "hover:animate-spin"
)}
>
<span
className="block size-full bg-current"
style={{
clipPath: `polygon(
45% 62.1%, 45% 100%, 55% 100%, 55% 62.1%,
81.8% 88.9%, 88.9% 81.8%, 62.1% 55%, 100% 55%,
100% 45%, 62.1% 45%, 88.9% 18.2%, 81.8% 11.1%,
55% 37.9%, 55% 0%, 45% 0%, 45% 37.9%,
18.2% 11.1%, 11.1% 18.2%, 37.9% 45%, 0% 45%,
0% 55%, 37.9% 55%, 11.1% 81.8%, 18.2% 88.9%
)`,
}}
/>
</span>
</span>
);
}
return (
<span
className={cn(
"inline-block size-[1em]",
!entranceDone && "animate-entrance-spin",
entranceDone && "hover:animate-spin",
entranceDone && !noSpin && "hover:animate-spin",
className
)}
aria-hidden="true"