chore(onboarding): remove legacy agent & first-issue steps (#5774)

The "Create your first agent" and "first issue" onboarding steps were
dropped from the in-flow sequence (helper-agent creation moved to the
post-onboarding workspace shell), but their code was left behind. Remove
the now-dead residue:

- Delete unused step components `step-agent.tsx` and `step-first-issue.tsx`
  (not referenced or exported anywhere).
- Delete `recommend-template.ts` (+ test) and drop its core export — it
  was consumed only by `step-agent.tsx`.
- Drop the dead `agent` / `first_issue` members from the `OnboardingStep`
  union.
- Remove the orphaned `step_agent` and `first_issue` i18n sections across
  all four locales (en / zh-Hans / ja / ko); parity test stays green.
- Fix a stale doc path in `step-order.ts` (welcome-after-onboarding.tsx).

No behavior change: the live flow is welcome → source → role → use_case
→ workspace → runtime. Typecheck (core/views/web/desktop) and onboarding
+ locale-parity tests pass.

Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
Jiayuan Zhang
2026-07-22 18:16:41 +08:00
committed by GitHub
parent 4dc47ef113
commit f6902a5f5b
11 changed files with 3 additions and 958 deletions

View File

@@ -16,7 +16,6 @@ export {
needsSourceBackfill,
SOURCE_BACKFILL_MAX_DISMISSALS,
} from "./needs-backfill";
export { recommendTemplate, type AgentTemplateId } from "./recommend-template";
export {
useWelcomeStore,
type WelcomeSignal,

View File

@@ -1,191 +0,0 @@
import { describe, expect, it } from "vitest";
import { recommendTemplate } from "./recommend-template";
import type { Role, UseCase } from "./types";
const ALL_USE_CASES: UseCase[] = [
"ship_code",
"manage_team",
"personal_tasks",
"plan_research",
"write_publish",
"automate_ops",
"evaluate",
"other",
];
const ALL_ROLES: Role[] = [
"engineer",
"product",
"designer",
"founder",
"marketing",
"writer",
"research",
"ops",
"student",
"other",
];
describe("recommendTemplate", () => {
describe("engineer × use_case tiebreaker", () => {
it.each<UseCase>(["manage_team", "plan_research"])(
"engineer × [%s] → planning",
(use_case) => {
expect(
recommendTemplate({ role: "engineer", use_case: [use_case] }),
).toBe("planning");
},
);
it("engineer × [write_publish] → writing", () => {
expect(
recommendTemplate({ role: "engineer", use_case: ["write_publish"] }),
).toBe("writing");
});
it.each<UseCase>([
"ship_code",
"personal_tasks",
"automate_ops",
"evaluate",
"other",
])("engineer × [%s] → coding", (use_case) => {
expect(
recommendTemplate({ role: "engineer", use_case: [use_case] }),
).toBe("coding");
});
it("engineer × [] → coding", () => {
expect(recommendTemplate({ role: "engineer", use_case: [] })).toBe(
"coding",
);
});
});
describe("multi-select priority (first matching branch wins)", () => {
// Engineer + (manage_team OR plan_research) wins over write_publish
// wins over the default coding fallback. The order inside the
// recommendTemplate switch is the implicit priority.
it("engineer × [ship_code, manage_team] → planning (manage_team wins over default)", () => {
expect(
recommendTemplate({
role: "engineer",
use_case: ["ship_code", "manage_team"],
}),
).toBe("planning");
});
it("engineer × [write_publish, ship_code] → writing", () => {
expect(
recommendTemplate({
role: "engineer",
use_case: ["write_publish", "ship_code"],
}),
).toBe("writing");
});
it("engineer × [manage_team, write_publish] → planning (earlier branch wins)", () => {
expect(
recommendTemplate({
role: "engineer",
use_case: ["manage_team", "write_publish"],
}),
).toBe("planning");
});
it("null × [ship_code, write_publish] → coding (fallback priority)", () => {
expect(
recommendTemplate({
role: null,
use_case: ["ship_code", "write_publish"],
}),
).toBe("coding");
});
});
describe("product × use_case", () => {
it("product × [ship_code] → coding", () => {
expect(
recommendTemplate({ role: "product", use_case: ["ship_code"] }),
).toBe("coding");
});
it.each<UseCase>(["manage_team", "plan_research", "evaluate", "other"])(
"product × [%s] → planning",
(use_case) => {
expect(
recommendTemplate({ role: "product", use_case: [use_case] }),
).toBe("planning");
},
);
it("product × [] → planning", () => {
expect(recommendTemplate({ role: "product", use_case: [] })).toBe(
"planning",
);
});
});
describe("marketing × use_case", () => {
it.each<UseCase>(["write_publish", "plan_research"])(
"marketing × [%s] → writing",
(use_case) => {
expect(
recommendTemplate({ role: "marketing", use_case: [use_case] }),
).toBe("writing");
},
);
it("marketing × [manage_team] → planning", () => {
expect(
recommendTemplate({ role: "marketing", use_case: ["manage_team"] }),
).toBe("planning");
});
});
describe("single-template roles", () => {
it.each(ALL_USE_CASES)("writer × [%s] → writing", (use_case) => {
expect(recommendTemplate({ role: "writer", use_case: [use_case] })).toBe(
"writing",
);
});
it.each(ALL_USE_CASES)("designer × [%s] → assistant", (use_case) => {
expect(
recommendTemplate({ role: "designer", use_case: [use_case] }),
).toBe("assistant");
});
it.each(ALL_USE_CASES)("research × [%s] → planning", (use_case) => {
expect(
recommendTemplate({ role: "research", use_case: [use_case] }),
).toBe("planning");
});
it.each<Role>(["founder", "ops", "student", "other"])(
"%s → assistant",
(role) => {
expect(recommendTemplate({ role, use_case: [] })).toBe("assistant");
},
);
});
describe("role skipped — use_case fallback", () => {
it("null × [ship_code] → coding", () => {
expect(recommendTemplate({ role: null, use_case: ["ship_code"] })).toBe(
"coding",
);
});
it("null × [write_publish] → writing", () => {
expect(
recommendTemplate({ role: null, use_case: ["write_publish"] }),
).toBe("writing");
});
it.each<UseCase>(["manage_team", "plan_research"])(
"null × [%s] → planning",
(use_case) => {
expect(recommendTemplate({ role: null, use_case: [use_case] })).toBe(
"planning",
);
},
);
it("both empty → assistant", () => {
expect(recommendTemplate({ role: null, use_case: [] })).toBe("assistant");
});
});
describe("exhaustive role coverage", () => {
it.each(ALL_ROLES)("role=%s returns a valid template id", (role) => {
const result = recommendTemplate({ role, use_case: [] });
expect(["coding", "planning", "writing", "assistant"]).toContain(result);
});
});
});

View File

@@ -1,71 +0,0 @@
import type { QuestionnaireAnswers, Role, UseCase } from "./types";
/**
* Identifier for the four legacy onboarding agent templates. Keep in
* sync with the template registry inside StepAgent in
* `packages/views/onboarding/steps/step-agent.tsx`.
*/
export type AgentTemplateId = "coding" | "planning" | "writing" | "assistant";
/**
* Pick a recommended agent template based on the questionnaire
* (role × use_case). Role is the primary signal; use_case is a
* tiebreaker for roles that legitimately split between templates
* (engineer / product / marketing).
*
* `use_case` is multi-select — when a user picks several, the rules
* below use `.includes(...)` against the set. Order of evaluation
* inside each role's switch is the implicit priority (first match
* wins). For ambiguous overlaps (e.g. engineer who picks both
* `manage_team` and `write_publish`) the earlier branch wins, which
* matches the prior single-select behavior when only one of those
* was selectable.
*
* Fallback chain when role is skipped or null:
* 1. Derive from use_case alone (same priority order).
* 2. Both unknown → `assistant` (the generic default).
*
* Pure / deterministic — safe to call on every render.
*/
export function recommendTemplate(
answers: Pick<QuestionnaireAnswers, "role" | "use_case">,
): AgentTemplateId {
const role: Role | null = answers.role;
const useCases: readonly UseCase[] = answers.use_case ?? [];
if (role === null) return fallbackFromUseCase(useCases);
switch (role) {
case "engineer":
if (useCases.includes("manage_team") || useCases.includes("plan_research"))
return "planning";
if (useCases.includes("write_publish")) return "writing";
return "coding";
case "product":
if (useCases.includes("ship_code")) return "coding";
return "planning";
case "designer":
return "assistant";
case "writer":
return "writing";
case "marketing":
if (useCases.includes("write_publish") || useCases.includes("plan_research"))
return "writing";
return "planning";
case "research":
return "planning";
case "founder":
case "ops":
case "student":
case "other":
return "assistant";
}
}
function fallbackFromUseCase(useCases: readonly UseCase[]): AgentTemplateId {
if (useCases.includes("ship_code")) return "coding";
if (useCases.includes("write_publish")) return "writing";
if (useCases.includes("manage_team") || useCases.includes("plan_research"))
return "planning";
return "assistant";
}

View File

@@ -16,8 +16,8 @@ import type { OnboardingStep } from "./types";
*
* Note: "teammate" (the old "Create your first agent" step) is no longer
* part of the in-flow sequence. Helper agent creation now happens after
* onboarding exits, via the workspace OnboardingHelperModal — see
* `packages/views/workspace/onboarding-helper-modal.tsx`.
* onboarding exits, in the workspace shell — see
* `packages/views/workspace/welcome-after-onboarding.tsx`.
*/
export const ONBOARDING_STEP_ORDER: readonly OnboardingStep[] = [
"source",

View File

@@ -4,9 +4,7 @@ export type OnboardingStep =
| "role"
| "use_case"
| "workspace"
| "runtime"
| "agent"
| "first_issue";
| "runtime";
/**
* Exit path from the onboarding flow. Sent to

View File

@@ -134,59 +134,6 @@
"success_toast": "You're on the list. We'll email when cloud runtimes are live.",
"failed_toast": "Failed to join waitlist"
},
"first_issue": {
"error_title": "Something went wrong",
"retry": "Retry",
"retry_failed": "Retry failed",
"finishing": "Finishing up",
"opening": "Almost there — opening your workspace."
},
"step_agent": {
"eyebrow": "Your first agent",
"headline": "Meet your first teammate.",
"lede_prefix": "Your answers point to a ",
"lede_suffix": ". Pick whichever of the four fits you — each template ships ready to take its first issue. You can retune its instructions from the agent settings page later.",
"footer_hint": "One agent is enough to start. Add more from the sidebar later.",
"create_action": "Create {{name}}",
"create_failed": "Failed to create agent",
"recommended_badge": "Recommended",
"templates": {
"coding": {
"label": "Coding Agent",
"blurb": "Writes, refactors, and ships code. Reads your repo.",
"instructions": "You are a Coding Agent on a product team. Pick up coding issues — implement features, fix bugs, write tests, and open pull requests. Read the repository before you start, follow existing code conventions, and keep diffs focused. Ask for clarification when the acceptance criteria are ambiguous."
},
"planning": {
"label": "Planning Agent",
"blurb": "Breaks down work, drafts specs, keeps the board tidy.",
"instructions": "You are a Planning Agent. Turn loose ideas and open issues into scoped, ready-to-execute work: break them down into subtasks, write acceptance criteria, and propose owners and sequencing. Prefer clarity over speed. When blocked by missing context, ask one specific question rather than guessing."
},
"writing": {
"label": "Writing Agent",
"blurb": "Drafts, summarizes, researches. Long-form friendly.",
"instructions": "You are a Writing Agent. Draft documents, summarize long content, and research topics on the web when needed. Structure your output as finished prose a reader can use directly — not an outline. Cite sources when you draw from them. Match the tone the user establishes in the issue."
},
"assistant": {
"label": "Assistant",
"blurb": "General-purpose. Good default when the task is unclear.",
"instructions": "You are a general-purpose teammate. Handle varied tasks — light coding, writing, research, planning — and stay pragmatic about scope. When the task is ambiguous, ask one clarifying question before diving in. Default to short, useful outputs over exhaustive ones."
}
},
"about_eyebrow": "What's an agent",
"about_headline": "An AI teammate that lives in your workspace.",
"about_body": "Agents show up in every assignee picker, just like any other colleague — except they can work 24/7 on whatever runtime you give them.",
"ways_eyebrow": "Ways to work with an agent",
"way_assign_title": "Assign it an issue",
"way_assign_body": "It picks up the task and reports back in the thread.",
"way_mention_title": "@mention in a comment",
"way_mention_body": "Pull it into a conversation for a quick take.",
"way_chat_title": "Chat one-on-one",
"way_chat_body": "Ask quick questions without creating an issue.",
"way_autopilot_title": "Put it on Autopilot",
"way_autopilot_body": "Daily triage, weekly digest, monthly audit — on a schedule.",
"add_more_hint": "Add more agents anytime. A small team of specialized agents beats one jack-of-all-trades.",
"docs_link": "Creating your first agent →"
},
"welcome_after_onboarding": {
"loading_helper": "Preparing your Helper…",
"error_title": "Couldn't finish setup",

View File

@@ -134,59 +134,6 @@
"success_toast": "ウェイトリストに登録しました。クラウドランタイムが利用可能になったらメールでお知らせします。",
"failed_toast": "ウェイトリストに登録できませんでした"
},
"first_issue": {
"error_title": "問題が発生しました",
"retry": "再試行",
"retry_failed": "再試行に失敗しました",
"finishing": "仕上げ中",
"opening": "もうすぐです — ワークスペースを開いています。"
},
"step_agent": {
"eyebrow": "最初のエージェント",
"headline": "最初のチームメイトに会いましょう。",
"lede_prefix": "あなたの回答からは ",
"lede_suffix": " が合っていそうです。4 つの中から自分に合うものを選んでください。どのテンプレートも最初のイシューをすぐに引き受けられる状態で用意されています。指示はあとでエージェント設定ページから調整できます。",
"footer_hint": "始めるにはエージェント 1 つで十分です。あとでサイドバーから追加できます。",
"create_action": "{{name}} を作成",
"create_failed": "エージェントを作成できませんでした",
"recommended_badge": "おすすめ",
"templates": {
"coding": {
"label": "コーディングエージェント",
"blurb": "コードを書き、リファクタリングし、リリースします。リポジトリを読みます。",
"instructions": "あなたはプロダクトチームの Coding Agent です。コーディングのイシューを引き受け、機能を実装し、バグを修正し、テストを書き、Pull request を開いてください。始める前にリポジトリを読み、既存のコード規約に従い、diff は焦点を絞ったものに保ってください。受け入れ基準が曖昧なときは確認の質問をしてください。"
},
"planning": {
"label": "プランニングエージェント",
"blurb": "作業を分解し、仕様を起草し、ボードを整理します。",
"instructions": "あなたは Planning Agent です。漠然としたアイデアや未着手のイシューを、スコープが定まった実行可能な作業に変えてください。サブタスクに分解し、受け入れ基準を書き、担当者と順序を提案します。スピードより明確さを優先してください。コンテキスト不足で行き詰まったら、推測せずに具体的な質問を 1 つしてください。"
},
"writing": {
"label": "ライティングエージェント",
"blurb": "下書き、要約、リサーチを行います。長文も得意です。",
"instructions": "あなたは Writing Agent です。ドキュメントを起草し、長い内容を要約し、必要に応じてウェブでトピックをリサーチしてください。アウトラインではなく、読者がそのまま使える完成した文章として出力を構成してください。資料を参照した場合は出典を引用してください。イシューでユーザーが定めたトーンに合わせてください。"
},
"assistant": {
"label": "アシスタント",
"blurb": "汎用型。タスクが不明確なときに頼れるデフォルトです。",
"instructions": "あなたは汎用のチームメイトです。軽めのコーディング、執筆、リサーチ、計画など多様なタスクをこなし、スコープについては実用的に判断してください。タスクが曖昧なときは、取りかかる前に確認の質問を 1 つしてください。網羅的な出力より、短く役立つ出力を基本としてください。"
}
},
"about_eyebrow": "エージェントとは",
"about_headline": "ワークスペースの中に住む AI のチームメイトです。",
"about_body": "エージェントは、他の同僚と同じようにすべての担当者ピッカーに表示されます。違いは、与えたランタイムの上でなら 24 時間働けることです。",
"ways_eyebrow": "エージェントとの働き方",
"way_assign_title": "イシューを割り当てる",
"way_assign_body": "タスクを引き受け、スレッドで報告します。",
"way_mention_title": "コメントで @メンション",
"way_mention_body": "会話に呼んで、すばやく意見をもらえます。",
"way_chat_title": "1 対 1 でチャット",
"way_chat_body": "イシューを作らずに、すばやく質問できます。",
"way_autopilot_title": "オートパイロットに設定",
"way_autopilot_body": "毎日のトリアージ、週次ダイジェスト、月次監査などをスケジュールに沿って実行します。",
"add_more_hint": "エージェントはいつでも追加できます。1 人の万能型より、専門エージェントの小さなチームのほうが優れています。",
"docs_link": "最初のエージェントを作成する →"
},
"welcome_after_onboarding": {
"loading_helper": "Helper を準備中…",
"error_title": "セットアップを完了できませんでした",

View File

@@ -134,59 +134,6 @@
"success_toast": "대기자 명단에 등록했습니다. 클라우드 런타임이 준비되면 이메일로 알려드릴게요.",
"failed_toast": "대기자 명단에 등록하지 못했습니다"
},
"first_issue": {
"error_title": "문제가 발생했습니다",
"retry": "다시 시도",
"retry_failed": "다시 시도 실패",
"finishing": "마무리 중",
"opening": "거의 다 됐습니다. 워크스페이스를 여는 중입니다."
},
"step_agent": {
"eyebrow": "첫 에이전트",
"headline": "첫 팀원을 만나보세요.",
"lede_prefix": "답변을 보면 ",
"lede_suffix": "가 잘 맞아 보입니다. 네 가지 중 원하는 템플릿을 고르세요. 모두 첫 이슈를 바로 맡을 준비가 되어 있으며, 나중에 에이전트 설정에서 지침을 조정할 수 있습니다.",
"footer_hint": "시작에는 에이전트 하나면 충분합니다. 나중에 사이드바에서 더 추가하세요.",
"create_action": "{{name}} 만들기",
"create_failed": "에이전트를 만들지 못했습니다",
"recommended_badge": "추천",
"templates": {
"coding": {
"label": "코딩 에이전트",
"blurb": "코드를 작성, 리팩터링, 배포합니다. 저장소를 읽습니다.",
"instructions": "당신은 제품팀의 코딩 에이전트입니다. 코딩 이슈를 맡아 기능을 구현하고, 버그를 고치고, 테스트를 작성하고, Pull request를 여세요. 시작하기 전에 저장소를 읽고, 기존 코드 규칙을 따르며, diff는 집중된 범위로 유지하세요. 수락 기준이 모호하면 확인 질문을 하세요."
},
"planning": {
"label": "기획 에이전트",
"blurb": "작업을 쪼개고, 스펙을 작성하고, 보드를 정리합니다.",
"instructions": "당신은 기획 에이전트입니다. 느슨한 아이디어와 열린 이슈를 실행 가능한 작업으로 정리하세요. 하위 작업으로 나누고, 수락 기준을 작성하고, 담당자와 순서를 제안하세요. 속도보다 명확성을 우선하세요. 맥락이 부족해 막히면 추측하지 말고 구체적인 질문 하나를 하세요."
},
"writing": {
"label": "글쓰기 에이전트",
"blurb": "초안 작성, 요약, 리서치를 합니다. 긴 글에 강합니다.",
"instructions": "당신은 글쓰기 에이전트입니다. 문서를 작성하고, 긴 내용을 요약하고, 필요하면 웹에서 리서치하세요. 결과는 개요가 아니라 독자가 바로 사용할 수 있는 완성된 문장으로 구성하세요. 자료를 참고했다면 출처를 인용하세요. 이슈에서 사용자가 만든 톤을 맞추세요."
},
"assistant": {
"label": "어시스턴트",
"blurb": "범용입니다. 작업이 모호할 때 좋은 기본값입니다.",
"instructions": "당신은 범용 팀원입니다. 가벼운 코딩, 글쓰기, 리서치, 기획 등 다양한 작업을 처리하고 범위를 실용적으로 관리하세요. 작업이 모호하면 시작하기 전에 확인 질문 하나를 하세요. 기본적으로 장황함보다 짧고 유용한 결과를 우선하세요."
}
},
"about_eyebrow": "에이전트란?",
"about_headline": "워크스페이스 안에 있는 AI 팀원입니다.",
"about_body": "에이전트는 다른 동료처럼 모든 담당자 선택 목록에 나타납니다. 다만 연결한 런타임만 있으면 24시간 작업할 수 있습니다.",
"ways_eyebrow": "에이전트와 일하는 방법",
"way_assign_title": "이슈 할당",
"way_assign_body": "작업을 맡고 스레드에 진행 상황을 남깁니다.",
"way_mention_title": "댓글에서 @멘션",
"way_mention_body": "대화에 불러 빠른 의견을 받을 수 있습니다.",
"way_chat_title": "1:1 채팅",
"way_chat_body": "이슈를 만들지 않고 간단히 질문합니다.",
"way_autopilot_title": "오토파일럿으로 예약",
"way_autopilot_body": "일일 분류, 주간 요약, 월간 감사 같은 작업을 일정에 맞춰 실행합니다.",
"add_more_hint": "에이전트는 언제든 추가할 수 있습니다. 한 명의 만능 에이전트보다 전문 에이전트 몇 명으로 이루어진 작은 팀이 더 좋습니다.",
"docs_link": "첫 에이전트 만들기 →"
},
"welcome_after_onboarding": {
"loading_helper": "Helper 준비 중...",
"error_title": "설정을 마치지 못했습니다",

View File

@@ -134,59 +134,6 @@
"success_toast": "已加入候补名单。云运行时上线时会通过邮件通知你。",
"failed_toast": "加入候补名单失败"
},
"first_issue": {
"error_title": "出错了",
"retry": "重试",
"retry_failed": "重试失败",
"finishing": "即将完成",
"opening": "马上就好 —— 正在打开你的工作区。"
},
"step_agent": {
"eyebrow": "你的第一个智能体",
"headline": "和你的第一位队友打个招呼。",
"lede_prefix": "根据你的回答,推荐的是 ",
"lede_suffix": "。从这四个模板里挑一个最贴你的——每个模板都开箱即用,可以立刻接 issue。之后可以在智能体设置页里再调整指令。",
"footer_hint": "一个智能体足够上手了。之后可以从侧边栏添加更多。",
"create_action": "创建 {{name}}",
"create_failed": "创建智能体失败",
"recommended_badge": "推荐",
"templates": {
"coding": {
"label": "编码智能体",
"blurb": "写代码、重构、发版。会读你的仓库。",
"instructions": "你是产品团队里的编码智能体。负责接编码相关的 issue —— 实现功能、修 bug、写测试、提 PR。开工前先读一下仓库遵循已有的代码规范保持 diff 聚焦。验收标准模糊时主动问清楚。"
},
"planning": {
"label": "规划智能体",
"blurb": "拆解工作、写规格、维护看板。",
"instructions": "你是规划智能体。把零散的想法和 open issue 转化成范围清晰、可立即执行的工作:拆成子任务、写验收标准、提出负责人和先后顺序。清晰优先于速度。缺少上下文时提一个具体问题,不要猜。"
},
"writing": {
"label": "写作智能体",
"blurb": "起草、摘要、调研。擅长长文。",
"instructions": "你是写作智能体。负责起草文档、总结长文,必要时上网调研主题。输出要做成读者可以直接用的成文 —— 不是大纲。引用资料时注明来源。语气和 issue 中用户的语气保持一致。"
},
"assistant": {
"label": "通用助手",
"blurb": "通用型。任务不明确时的默认选择。",
"instructions": "你是一名通用型队友。处理各种任务 —— 轻度编码、写作、调研、规划 —— 对范围保持务实。任务模糊时先问一个澄清性问题再开工。默认输出简短有用,而不是详尽冗长。"
}
},
"about_eyebrow": "什么是智能体",
"about_headline": "住在你工作区里的 AI 队友。",
"about_body": "智能体会出现在每个负责人选择器里,就像任何同事一样 —— 区别是它们可以 24/7 在你指定的运行时上工作。",
"ways_eyebrow": "和智能体协作的方式",
"way_assign_title": "分配 issue",
"way_assign_body": "它会接下任务,并在评论里反馈进展。",
"way_mention_title": "在评论中 @提及",
"way_mention_body": "把它拉进对话,给个快速反馈。",
"way_chat_title": "一对一聊天",
"way_chat_body": "无需创建 issue直接问快速问题。",
"way_autopilot_title": "交给自动化",
"way_autopilot_body": "按周期跑:每日整理、每周摘要、每月审计。",
"add_more_hint": "随时添加更多智能体。一支专长各异的小团队胜过一个万能选手。",
"docs_link": "创建第一个智能体 →"
},
"welcome_after_onboarding": {
"loading_helper": "正在为你准备 Helper……",
"error_title": "准备失败",

View File

@@ -1,352 +0,0 @@
"use client";
import { useRef, useState } from "react";
import { ArrowLeft, ArrowRight, Loader2 } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@multica/ui/components/ui/button";
import { useScrollFade } from "@multica/ui/hooks/use-scroll-fade";
import { cn } from "@multica/ui/lib/utils";
import { api } from "@multica/core/api";
import {
recommendTemplate,
type AgentTemplateId,
type QuestionnaireAnswers,
} from "@multica/core/onboarding";
import type {
Agent,
AgentRuntime,
CreateAgentRequest,
} from "@multica/core/types";
import { DragStrip } from "@multica/views/platform";
import { StepHeader } from "../components/step-header";
import { useT } from "../../i18n";
/**
* Step 4 — create the user's first agent.
*
* Picks a recommended template from the questionnaire answers
* (`recommendTemplate()` maps role × use_case → one of 4 templates),
* attaches the template's default name + instructions, and ships a
* ready-to-work agent on Create. Layout mirrors Questionnaire /
* Workspace: a 2-column editorial shell with DragStrip + 3-region
* app column (header / scrollable main / footer) + "About agents"
* side panel hidden below lg.
*
* No rename, runtime-swap, or instructions editor on this step —
* every template defaults are good enough to ship immediately, and
* the agent settings page handles all customization post-onboarding.
* Intentional: minimizing surface area keeps time-to-first-agent low.
*
* No skip path either — if the user arrived here they have a runtime
* (Step 3 only routes to Step 4 when a runtime was picked), so
* creating an agent is the purpose of this step. Users who want a
* runtime-less workspace skip out at Step 3.
*/
interface AgentTemplate {
id: AgentTemplateId;
label: string;
defaultName: string;
emoji: string;
blurb: string;
instructions: string;
}
// Defaults stay constant (names + emoji are visual identity, not copy);
// label / blurb / instructions resolve from the bundle at render time.
const TEMPLATE_DEFAULTS: readonly Omit<AgentTemplate, "label" | "blurb" | "instructions">[] = [
{ id: "coding", defaultName: "Atlas", emoji: "⌘" },
{ id: "planning", defaultName: "Orion", emoji: "◐" },
{ id: "writing", defaultName: "Mira", emoji: "✎" },
{ id: "assistant", defaultName: "Vega", emoji: "✦" },
] as const;
function useAgentTemplates(): {
templates: readonly AgentTemplate[];
byId: Record<AgentTemplateId, AgentTemplate>;
} {
const { t } = useT("onboarding");
const templates = TEMPLATE_DEFAULTS.map((d) => ({
...d,
label: t(($) => $.step_agent.templates[d.id].label),
blurb: t(($) => $.step_agent.templates[d.id].blurb),
instructions: t(($) => $.step_agent.templates[d.id].instructions),
})) as readonly AgentTemplate[];
const byId = Object.fromEntries(templates.map((tpl) => [tpl.id, tpl])) as Record<
AgentTemplateId,
AgentTemplate
>;
return { templates, byId };
}
export function StepAgent({
runtime,
questionnaire,
onCreated,
onBack,
}: {
runtime: AgentRuntime;
questionnaire: QuestionnaireAnswers;
onCreated: (agent: Agent) => void | Promise<void>;
onBack?: () => void;
}) {
const { t } = useT("onboarding");
const { templates: AGENT_TEMPLATES, byId: TEMPLATE_BY_ID } = useAgentTemplates();
const recommendedId = recommendTemplate(questionnaire);
const recommended = TEMPLATE_BY_ID[recommendedId];
const [templateId, setTemplateId] =
useState<AgentTemplateId>(recommendedId);
const template = TEMPLATE_BY_ID[templateId];
const [creating, setCreating] = useState(false);
const handleCreate = async () => {
if (creating) return;
setCreating(true);
try {
const req: CreateAgentRequest = {
name: template.defaultName,
description: template.blurb,
instructions: template.instructions,
runtime_id: runtime.id,
visibility: "workspace",
template: templateId,
};
const agent = await api.createAgent(req);
await onCreated(agent);
} catch (err) {
toast.error(
err instanceof Error ? err.message : t(($) => $.step_agent.create_failed),
);
setCreating(false);
}
};
const mainRef = useRef<HTMLElement>(null);
const fadeStyle = useScrollFade(mainRef);
return (
<div className="animate-onboarding-enter grid h-full min-h-0 grid-cols-1 lg:grid-cols-[minmax(0,1fr)_480px]">
{/* Left column — DragStrip + 3-region app shell */}
<div className="flex min-h-0 flex-col">
<DragStrip />
{/* Fixed header — Back + progress indicator */}
<header className="flex shrink-0 items-center gap-4 bg-background px-6 py-3 sm:px-10 md:px-14 lg:px-16">
{onBack ? (
<button
type="button"
onClick={onBack}
className="flex items-center gap-1.5 text-sm text-muted-foreground transition-colors hover:text-foreground"
>
<ArrowLeft className="h-3.5 w-3.5" />
{t(($) => $.common.back)}
</button>
) : (
<span aria-hidden className="w-0" />
)}
<div className="flex-1">
<StepHeader currentStep="agent" />
</div>
</header>
{/* Scrollable middle. `useScrollFade` softly masks content at
the header / footer edges as the user scrolls, replacing a
hard divider line. */}
<main
ref={mainRef}
style={fadeStyle}
className="min-h-0 flex-1 overflow-y-auto"
>
<div className="mx-auto w-full max-w-[620px] px-6 py-10 sm:px-10 md:px-14 lg:px-0 lg:py-14">
<div className="mb-2 text-xs font-medium uppercase tracking-[0.08em] text-muted-foreground">
{t(($) => $.step_agent.eyebrow)}
</div>
<h1 className="text-balance font-serif text-[36px] font-medium leading-[1.1] tracking-tight text-foreground">
{t(($) => $.step_agent.headline)}
</h1>
<p className="mt-4 text-[15.5px] leading-[1.55] text-foreground/80">
{t(($) => $.step_agent.lede_prefix)}
<strong className="font-medium text-foreground">
{recommended.label}
</strong>
{t(($) => $.step_agent.lede_suffix)}
</p>
<div className="mt-10 grid grid-cols-1 gap-3 sm:grid-cols-2">
{AGENT_TEMPLATES.map((t) => (
<TemplateCard
key={t.id}
template={t}
selected={templateId === t.id}
recommended={recommendedId === t.id}
onSelect={() => setTemplateId(t.id)}
/>
))}
</div>
{/* Inline action bar — hint + Create CTA. No skip path:
reaching Step 4 means a runtime was picked at Step 3,
so creating the agent IS this step. */}
<div className="mt-8 flex flex-wrap items-center justify-end gap-x-4 gap-y-2">
<span className="mr-auto hidden text-xs text-muted-foreground sm:block">
{t(($) => $.step_agent.footer_hint)}
</span>
<Button size="lg" onClick={handleCreate} disabled={creating}>
{creating && <Loader2 className="h-4 w-4 animate-spin" />}
{t(($) => $.step_agent.create_action, { name: template.defaultName })}
<ArrowRight className="h-4 w-4" />
</Button>
</div>
</div>
</main>
</div>
{/* Right — About agents side panel, independent scroll */}
<aside className="hidden min-h-0 border-l bg-muted/40 lg:flex lg:flex-col">
<DragStrip />
<div className="min-h-0 flex-1 overflow-y-auto px-12 py-12">
<AboutAgentsSide />
</div>
</aside>
</div>
);
}
function TemplateCard({
template,
selected,
recommended,
onSelect,
}: {
template: AgentTemplate;
selected: boolean;
recommended: boolean;
onSelect: () => void;
}) {
const { t } = useT("onboarding");
return (
<button
type="button"
role="radio"
aria-checked={selected}
onClick={onSelect}
className={cn(
"flex flex-col items-start gap-3 rounded-lg border bg-card px-4 py-4 text-left transition-all",
selected
? "border-foreground shadow-[inset_0_0_0_1px_var(--color-foreground)]"
: "hover:border-foreground/20 hover:bg-accent/30",
)}
>
<div className="flex w-full items-start justify-between gap-2">
<span
aria-hidden
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-muted/70 font-serif text-lg text-foreground/80"
>
{template.emoji}
</span>
{recommended && (
<span className="shrink-0 rounded-full bg-brand/10 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wider text-brand">
{t(($) => $.step_agent.recommended_badge)}
</span>
)}
</div>
<div className="flex flex-col gap-1">
<div className="text-sm font-medium text-foreground">
{template.label}
</div>
<p className="text-xs leading-snug text-muted-foreground">
{template.blurb}
</p>
</div>
</button>
);
}
function AboutAgentsSide() {
const { t } = useT("onboarding");
return (
<div className="flex max-w-[380px] flex-col gap-8">
<section className="flex flex-col gap-4">
<div className="text-xs font-medium uppercase tracking-[0.08em] text-muted-foreground">
{t(($) => $.step_agent.about_eyebrow)}
</div>
<h2 className="font-serif text-[22px] font-medium leading-[1.25] tracking-tight text-foreground">
{t(($) => $.step_agent.about_headline)}
</h2>
<p className="text-[14px] leading-[1.6] text-foreground/80">
{t(($) => $.step_agent.about_body)}
</p>
</section>
<section className="flex flex-col gap-4">
<div className="text-xs font-medium uppercase tracking-[0.08em] text-muted-foreground">
{t(($) => $.step_agent.ways_eyebrow)}
</div>
<div className="flex flex-col gap-4">
<WayItem
glyph="→"
title={t(($) => $.step_agent.way_assign_title)}
body={t(($) => $.step_agent.way_assign_body)}
/>
<WayItem
glyph="@"
title={t(($) => $.step_agent.way_mention_title)}
body={t(($) => $.step_agent.way_mention_body)}
/>
<WayItem
glyph="◯"
title={t(($) => $.step_agent.way_chat_title)}
body={t(($) => $.step_agent.way_chat_body)}
/>
<WayItem
glyph="↻"
title={t(($) => $.step_agent.way_autopilot_title)}
body={t(($) => $.step_agent.way_autopilot_body)}
/>
</div>
</section>
<p className="text-[13px] leading-[1.55] text-muted-foreground">
{t(($) => $.step_agent.add_more_hint)}
</p>
<a
href="https://multica.ai/docs/agents-create"
target="_blank"
rel="noopener noreferrer"
className="self-start text-[13px] text-muted-foreground underline underline-offset-4 transition-colors hover:text-foreground"
>
{t(($) => $.step_agent.docs_link)}
</a>
</div>
);
}
function WayItem({
glyph,
title,
body,
}: {
glyph: string;
title: string;
body: string;
}) {
return (
<div className="grid grid-cols-[22px_1fr] gap-3">
<div
aria-hidden
className="flex h-[20px] w-[20px] items-center justify-center text-[14px] text-muted-foreground"
>
{glyph}
</div>
<div className="flex flex-col gap-1">
<div className="text-[14px] font-medium leading-tight text-foreground">
{title}
</div>
<p className="text-[13px] leading-[1.5] text-muted-foreground">
{body}
</p>
</div>
</div>
);
}

View File

@@ -1,126 +0,0 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { Loader2, AlertCircle } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@multica/ui/components/ui/button";
import {
completeOnboarding,
type OnboardingCompletionPath,
} from "@multica/core/onboarding";
import { useT } from "../../i18n";
/**
* Step 5 — the final onboarding beat.
*
* Runtime-skipped finalizer. The runtime-connected path now bootstraps one
* default assistant plus one onboarding issue server-side and routes there
* directly. This step remains for users who skip runtime connection: it only
* flips `onboarded_at` and lands them in the workspace.
* Two consequences of that move:
*
* 1. This step can't fail in user-visible ways any more. `completeOnboarding`
* is one PATCH to `/api/me`; the only failure mode is a network error,
* which we surface as a toast + Retry, not a full error screen.
* 2. The sub-issue "Unknown" assignee race is gone for free — by the time
* the import runs, the user has already landed in the workspace, so
* `listMembers` has resolved and the current user's member_id is in
* the query cache.
*/
export function StepFirstIssue({
onFinished,
completionPath,
workspaceId,
}: {
/** Called after `onboarded_at` is set server-side. Parent handles
* navigation to the workspace landing page. */
onFinished: () => void;
/** Which exit label the server should record on `onboarding_completed`.
* Computed in the parent shell where runtime + waitlist state are
* both in scope. */
completionPath: OnboardingCompletionPath;
workspaceId?: string;
}) {
const { t } = useT("onboarding");
const [error, setError] = useState<string | null>(null);
const [retrying, setRetrying] = useState(false);
const started = useRef(false);
const onFinishedRef = useRef(onFinished);
onFinishedRef.current = onFinished;
const completionPathRef = useRef(completionPath);
completionPathRef.current = completionPath;
const workspaceIdRef = useRef(workspaceId);
workspaceIdRef.current = workspaceId;
useEffect(() => {
if (started.current) return;
started.current = true;
(async () => {
try {
await completeOnboarding(
completionPathRef.current,
workspaceIdRef.current,
);
onFinishedRef.current();
} catch (err) {
setError(
err instanceof Error ? err.message : t(($) => $.errors.skip_failed),
);
}
})();
}, [t]);
const retry = async () => {
if (retrying) return;
setRetrying(true);
setError(null);
try {
await completeOnboarding(
completionPathRef.current,
workspaceIdRef.current,
);
onFinishedRef.current();
} catch (err) {
const msg =
err instanceof Error ? err.message : t(($) => $.first_issue.retry_failed);
setError(msg);
toast.error(msg);
} finally {
setRetrying(false);
}
};
if (error) {
return (
<div className="animate-onboarding-enter flex w-full flex-col items-center gap-6 text-center">
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-destructive/10 text-destructive">
<AlertCircle className="h-6 w-6" />
</div>
<div className="flex flex-col gap-2">
<h1 className="text-2xl font-semibold tracking-tight">
{t(($) => $.first_issue.error_title)}
</h1>
<p className="text-sm text-muted-foreground">{error}</p>
</div>
<Button onClick={retry} disabled={retrying}>
{retrying && <Loader2 className="h-4 w-4 animate-spin" />}
{t(($) => $.first_issue.retry)}
</Button>
</div>
);
}
return (
<div className="animate-onboarding-enter flex w-full flex-col items-center gap-6 text-center">
<Loader2 className="h-10 w-10 animate-spin text-primary" />
<div className="flex flex-col gap-2">
<h1 className="text-2xl font-semibold tracking-tight">
{t(($) => $.first_issue.finishing)}
</h1>
<p className="text-sm text-muted-foreground">
{t(($) => $.first_issue.opening)}
</p>
</div>
</div>
);
}