feat(onboarding): make step 3 about Mika, with the runtime as a sub-decision

The step was titled after its dependency — "Pick an agent runtime" — while
the thing actually being created was named only in the grey lede. A member
reached a button reading "Start with Mika" without having been told who that
is, and said so: being confused there is not a failure to read carefully, it
is the page putting the lead role in a footnote.

So the subject changes rather than the step count. The headline names the
outcome, a card carries the introduction with a mark on it, and the runtime
list drops to a labelled sub-section under "Where should Mika run?". Reading
order becomes: who you are getting, she needs a machine, pick one, start.

MikaIntro sits above the phase switch so the subject holds still while the
runtime block below cycles through scanning / found / empty, and each phase's
own heading drops from h1 to h2 now that the page has a real h1.

Mika does not exist yet at this point — she is created on commit — so the
card cannot render her stored avatar. It reuses the mark the Runtimes page
already uses for "Start with Mika", so the two entry points read as the same
thing.

No fourth screen: the introduction and the only decision on this screen are
one beat, and splitting them would add a step between finishing setup and the
payoff. The defect was never a missing screen, it was an invisible
introduction.

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
Lambda
2026-08-03 01:34:02 +08:00
parent 7650a66f97
commit 0e8ffb6c70
6 changed files with 95 additions and 14 deletions

View File

@@ -185,8 +185,8 @@
"scanning_headline": "Connecting this computer…",
"scanning_lede_prefix": "Your agent works through an AI coding tool you already have — like ",
"scanning_lede_suffix": ", and others. We're checking this computer for ones you've installed.",
"found_headline": "Pick an agent runtime.",
"found_lede": "Mika, your workspace's Chief of Staff, runs on the one you pick. You can switch it or add more later.",
"found_headline": "Where should Mika run?",
"found_lede": "These are the agent runtimes we found on this computer. You can switch it or add more later.",
"remote_computer_later_note": "No local runtime, or prefer a remote computer? Skip this step and connect one later.",
"runtime_count_one": "{{count}} agent runtime",
"runtime_count_other": "{{count}} agent runtimes",
@@ -250,5 +250,11 @@
"errors": {
"skip_failed": "Failed to finish onboarding",
"mika_setup_failed": "Couldn't start your Mika workspace"
},
"mika_intro": {
"headline": "Meet Mika, your Chief of Staff.",
"name": "Mika",
"role": "Chief of Staff",
"blurb": "Tell her what you want done. She turns it into an issue and brings in the right agent to run it."
}
}

View File

@@ -185,8 +185,8 @@
"scanning_headline": "このコンピュータを接続中…",
"scanning_lede_prefix": "エージェントは、あなたがすでに持っている AI コーディングツールを通じて作業します — たとえば ",
"scanning_lede_suffix": " など。このコンピュータにインストール済みのものを確認しています。",
"found_headline": "エージェントランタイムを選んでください。",
"found_lede": "このワークスペースの参謀長である Mika は、選んだランタイムで動きます。あとで切り替えたり追加したりできます。",
"found_headline": "Mika はどこで動かしますか?",
"found_lede": "このコンピュータで見つかったエージェントランタイムです。あとで切り替えたり追加したりできます。",
"remote_computer_later_note": "ローカルにランタイムがない、またはリモートのコンピュータを使いたい場合は、このステップを飛ばしてあとで接続できます。",
"runtime_count_other": "エージェントランタイム {{count}} 個",
"status_all_online": "すべてオンライン",
@@ -248,5 +248,11 @@
"errors": {
"skip_failed": "オンボーディングを完了できませんでした",
"mika_setup_failed": "Mika ワークスペースを開始できませんでした"
},
"mika_intro": {
"headline": "参謀長の Mika を紹介します。",
"name": "Mika",
"role": "参謀長",
"blurb": "やりたいことを伝えると、issue に変えて、適したエージェントを呼んでくれます。"
}
}

View File

@@ -185,8 +185,8 @@
"scanning_headline": "이 컴퓨터 연결 중...",
"scanning_lede_prefix": "에이전트는 이미 사용하는 AI 코딩 도구를 통해 작업합니다. 예: ",
"scanning_lede_suffix": " 등. 이 컴퓨터에 설치된 도구를 확인하는 중입니다.",
"found_headline": "에이전트 런타임을 선택하세요.",
"found_lede": "이 워크스페이스의 참모장인 Mika는 선택한 런타임에서 실행됩니다. 나중에 바꾸거나 더 추가할 수 있습니다.",
"found_headline": "Mika를 어디서 실행할까요?",
"found_lede": "이 컴퓨터에서 찾은 에이전트 런타임입니다. 나중에 바꾸거나 더 추가할 수 있습니다.",
"remote_computer_later_note": "로컬 런타임이 없거나 원격 컴퓨터를 쓰고 싶다면 이 단계를 건너뛰고 나중에 연결하세요.",
"runtime_count_other": "에이전트 런타임 {{count}}개",
"status_all_online": "모두 온라인",
@@ -248,5 +248,11 @@
"errors": {
"skip_failed": "온보딩을 마치지 못했습니다",
"mika_setup_failed": "Mika 워크스페이스를 시작하지 못했습니다"
},
"mika_intro": {
"headline": "참모장 Mika를 소개합니다.",
"name": "Mika",
"role": "참모장",
"blurb": "하고 싶은 일을 알려주면 이슈로 만들고 알맞은 에이전트를 불러옵니다."
}
}

View File

@@ -185,8 +185,8 @@
"scanning_headline": "正在连接这台电脑…",
"scanning_lede_prefix": "你的智能体借助本机已安装的 AI 编程工具运行,例如 ",
"scanning_lede_suffix": " 等。我们正在检测这台电脑上装了哪些。",
"found_headline": "挑一个 agent 运行时。",
"found_lede": "Mika 是这个工作区的参谋长,会跑在你挑的这个运行时。之后可以随时切换或再加。",
"found_headline": "Mika 跑在哪台上?",
"found_lede": "以下是在这台电脑上找到的 agent 运行时。之后可以随时切换或再加。",
"remote_computer_later_note": "没有本地运行时,或者想用远程电脑?可以跳过这一步,之后再连。",
"runtime_count_other": "{{count}} 个 agent 运行时",
"status_all_online": "全部在线",
@@ -248,5 +248,11 @@
"errors": {
"skip_failed": "完成上手引导失败",
"mika_setup_failed": "无法启动 Mika 工作区"
},
"mika_intro": {
"headline": "认识一下 Mika你的参谋长。",
"name": "Mika",
"role": "参谋长",
"blurb": "告诉她你想做什么,她会把它变成一个 issue并找合适的智能体来做。"
}
}

View File

@@ -0,0 +1,54 @@
"use client";
import { Sparkles } from "lucide-react";
import { cn } from "@multica/ui/lib/utils";
import { useT } from "../../i18n";
import { STEP_MEASURE } from "./step-shell";
/**
* The subject of step 3, stated as an object rather than a footnote.
*
* The step used to be titled after its dependency — "Pick an agent runtime" —
* with Mika named only in the grey lede. That put the thing being created in
* the small print and the technical prerequisite in the headline, so a member
* reached a button saying "Start with Mika" without ever being told who that
* is. Headline plus a card with a mark on it is what makes the introduction
* survive not reading carefully.
*
* Mika does not exist yet at this point in the flow — she is created when the
* member commits — so this cannot render her stored avatar. It reuses the mark
* the Runtimes page already uses for "Start with Mika" instead, which keeps
* the two entry points recognisably the same thing.
*/
export function MikaIntro() {
const { t } = useT("onboarding");
return (
<>
<h1 className="text-balance font-serif text-display font-medium leading-[1.1] tracking-tight text-foreground">
{t(($) => $.mika_intro.headline)}
</h1>
<div
className={cn(
"mt-6 flex items-start gap-4 rounded-xl border bg-card px-5 py-4",
STEP_MEASURE,
)}
>
<span className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-foreground text-background">
<Sparkles aria-hidden className="size-4" />
</span>
<div className="min-w-0">
<p className="text-body font-medium text-foreground">
{t(($) => $.mika_intro.name)}
<span className="text-muted-foreground">
{" · "}
{t(($) => $.mika_intro.role)}
</span>
</p>
<p className="mt-1 text-body leading-[1.55] text-muted-foreground">
{t(($) => $.mika_intro.blurb)}
</p>
</div>
</div>
</>
);
}

View File

@@ -18,6 +18,7 @@ import {
} from "@multica/core/runtimes";
import type { AgentRuntime } from "@multica/core/types";
import { DragStrip } from "@multica/views/platform";
import { MikaIntro } from "../components/mika-intro";
import {
STEP_BLOCK_PADDING,
STEP_FRAME,
@@ -261,6 +262,8 @@ function FancyView({
key={phase}
className={cn("animate-onboarding-enter", STEP_FRAME, STEP_BLOCK_PADDING)}
>
<MikaIntro />
{phase === "scanning" && <ScanningView />}
{phase === "found" && (
<FoundView
@@ -335,9 +338,9 @@ function ScanningView() {
const { t } = useT("onboarding");
return (
<div>
<h1 className="text-balance font-serif text-display font-medium leading-[1.1] tracking-tight text-foreground">
<h2 className="mt-10 text-title-lg font-medium tracking-tight text-foreground">
{t(($) => $.step_runtime.scanning_headline)}
</h1>
</h2>
<p className="mt-4 max-w-[620px] text-body-lg leading-[1.55] text-muted-foreground">
{t(($) => $.step_runtime.scanning_lede_prefix)}
<span className="font-medium text-foreground">{"Claude Code"}</span>
@@ -384,9 +387,9 @@ function FoundView({
return (
<div>
<h1 className="text-balance font-serif text-display font-medium leading-[1.1] tracking-tight text-foreground">
<h2 className="mt-10 text-title-lg font-medium tracking-tight text-foreground">
{t(($) => $.step_runtime.found_headline)}
</h1>
</h2>
<p className="mt-4 max-w-[620px] text-body-lg leading-[1.55] text-muted-foreground">
{t(($) => $.step_runtime.found_lede)}
</p>
@@ -442,9 +445,9 @@ function EmptyView({
return (
<div>
<div className="flex items-start justify-between gap-4">
<h1 className="text-balance font-serif text-display font-medium leading-[1.1] tracking-tight text-foreground">
<h2 className="mt-10 text-title-lg font-medium tracking-tight text-foreground">
{t(($) => $.step_runtime.empty_headline)}
</h1>
</h2>
<RefreshButton
onClick={onRefresh}
refreshing={refreshing}