Skip to content

Commit 3ef3f26

Browse files
committed
feat(plugin): add first-run interactive onboarding tour (#1037)
Add 3-step onboarding tour for first-time users introducing PLAN/ACT/EVAL workflow, 38 specialist agents, and checklists/skills. Includes first-run detection via ~/.codingbuddy/onboarded flag, i18n support (en/ko/ja/zh/es), skip option, and 22 tests.
1 parent 45c462c commit 3ef3f26

3 files changed

Lines changed: 420 additions & 0 deletions

File tree

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
"""First-run onboarding tour for CodingBuddy.
2+
3+
Detects first-run via ~/.codingbuddy/onboarded flag file and renders
4+
an interactive 3-step tour introducing core features.
5+
"""
6+
import os
7+
from pathlib import Path
8+
from typing import Any, Dict, Optional
9+
10+
from buddy_renderer import (
11+
ANSI_COLORS,
12+
BUDDY_FACE,
13+
DEFAULT_BUDDY_CONFIG,
14+
get_buddy_config,
15+
)
16+
17+
# Flag file location
18+
ONBOARDED_DIR = os.path.join(os.path.expanduser("~"), ".codingbuddy")
19+
ONBOARDED_FLAG = os.path.join(ONBOARDED_DIR, "onboarded")
20+
21+
# Environment variable to skip tour
22+
SKIP_ENV_VAR = "CODINGBUDDY_SKIP_TOUR"
23+
24+
25+
def is_first_run() -> bool:
26+
"""Check if this is the user's first run.
27+
28+
Returns:
29+
True if onboarded flag does not exist and skip env var is not set.
30+
"""
31+
if os.environ.get(SKIP_ENV_VAR):
32+
return False
33+
return not os.path.isfile(ONBOARDED_FLAG)
34+
35+
36+
def mark_onboarded() -> None:
37+
"""Create the onboarded flag file to prevent future tours."""
38+
os.makedirs(ONBOARDED_DIR, exist_ok=True)
39+
Path(ONBOARDED_FLAG).touch()
40+
41+
42+
# ── i18n Tour Content ──────────────────────────────────────────────
43+
44+
TOUR_WELCOME: Dict[str, str] = {
45+
"en": "Welcome to CodingBuddy! Here's a quick tour...",
46+
"ko": "CodingBuddy에 오신 걸 환영해요! 간단히 소개할게요...",
47+
"ja": "CodingBuddyへようこそ!簡単にご紹介します...",
48+
"zh": "欢迎使用CodingBuddy!快速介绍一下...",
49+
"es": "Bienvenido a CodingBuddy! Un tour rapido...",
50+
}
51+
52+
TOUR_STEPS: Dict[int, Dict[str, Dict[str, str]]] = {
53+
1: {
54+
"en": {
55+
"title": "PLAN/ACT/EVAL Workflow",
56+
"body": "Type PLAN to design, ACT to implement, EVAL to review — or AUTO for the full cycle.",
57+
"example": 'PLAN add user authentication',
58+
},
59+
"ko": {
60+
"title": "PLAN/ACT/EVAL 워크플로우",
61+
"body": "PLAN으로 설계, ACT로 구현, EVAL로 검토 — 또는 AUTO로 전체 사이클을 실행하세요.",
62+
"example": 'PLAN 사용자 인증 추가',
63+
},
64+
"ja": {
65+
"title": "PLAN/ACT/EVAL ワークフロー",
66+
"body": "PLANで設計、ACTで実装、EVALでレビュー — またはAUTOで全サイクル実行。",
67+
"example": 'PLAN ユーザー認証を追加',
68+
},
69+
"zh": {
70+
"title": "PLAN/ACT/EVAL 工作流",
71+
"body": "用PLAN设计、ACT实现、EVAL审查 — 或AUTO执行完整周期。",
72+
"example": 'PLAN 添加用户认证',
73+
},
74+
"es": {
75+
"title": "Flujo PLAN/ACT/EVAL",
76+
"body": "PLAN para disenar, ACT para implementar, EVAL para revisar — o AUTO para el ciclo completo.",
77+
"example": 'PLAN agregar autenticacion',
78+
},
79+
},
80+
2: {
81+
"en": {
82+
"title": "38 Specialist Agents",
83+
"body": "Security, accessibility, performance... experts ready to analyze your code.",
84+
"example": 'AUTO implement login page',
85+
},
86+
"ko": {
87+
"title": "38명의 전문가 에이전트",
88+
"body": "보안, 접근성, 성능... 전문가들이 코드 분석을 도와줍니다.",
89+
"example": 'AUTO 로그인 페이지 구현',
90+
},
91+
"ja": {
92+
"title": "38人の専門エージェント",
93+
"body": "セキュリティ、アクセシビリティ、パフォーマンス...専門家がコード分析をサポート。",
94+
"example": 'AUTO ログインページを実装',
95+
},
96+
"zh": {
97+
"title": "38位专家代理",
98+
"body": "安全、无障碍、性能...专家随时准备分析您的代码。",
99+
"example": 'AUTO 实现登录页面',
100+
},
101+
"es": {
102+
"title": "38 Agentes Especialistas",
103+
"body": "Seguridad, accesibilidad, rendimiento... expertos listos para analizar tu codigo.",
104+
"example": 'AUTO implementar pagina de login',
105+
},
106+
},
107+
3: {
108+
"en": {
109+
"title": "Checklists & Skills",
110+
"body": "Auto-generated quality checklists and specialized skills for every task.",
111+
"example": 'EVAL review my changes',
112+
},
113+
"ko": {
114+
"title": "체크리스트 & 스킬",
115+
"body": "자동 생성되는 품질 체크리스트와 모든 작업을 위한 전문 스킬.",
116+
"example": 'EVAL 변경사항 검토',
117+
},
118+
"ja": {
119+
"title": "チェックリスト & スキル",
120+
"body": "自動生成の品質チェックリストと各タスク向けの専門スキル。",
121+
"example": 'EVAL 変更をレビュー',
122+
},
123+
"zh": {
124+
"title": "清单 & 技能",
125+
"body": "自动生成质量清单和每个任务的专业技能。",
126+
"example": 'EVAL 审查我的更改',
127+
},
128+
"es": {
129+
"title": "Listas & Habilidades",
130+
"body": "Listas de calidad auto-generadas y habilidades especializadas para cada tarea.",
131+
"example": 'EVAL revisar mis cambios',
132+
},
133+
},
134+
}
135+
136+
TOUR_SKIP: Dict[str, str] = {
137+
"en": "Skip future tours: touch ~/.codingbuddy/onboarded",
138+
"ko": "투어 건너뛰기: touch ~/.codingbuddy/onboarded",
139+
"ja": "ツアーをスキップ: touch ~/.codingbuddy/onboarded",
140+
"zh": "跳过教程: touch ~/.codingbuddy/onboarded",
141+
"es": "Saltar tour: touch ~/.codingbuddy/onboarded",
142+
}
143+
144+
TOUR_HEADER: Dict[str, str] = {
145+
"en": "Quick Tour",
146+
"ko": "퀵 투어",
147+
"ja": "クイックツアー",
148+
"zh": "快速导览",
149+
"es": "Tour Rapido",
150+
}
151+
152+
# Step number circled digits
153+
_STEP_NUMBERS = {1: "\u2460", 2: "\u2461", 3: "\u2462"}
154+
155+
156+
def _get_text(mapping: Dict[str, str], language: str) -> str:
157+
"""Get localized text with English fallback."""
158+
return mapping.get(language, mapping.get("en", ""))
159+
160+
161+
def _get_step(step_num: int, language: str) -> Dict[str, str]:
162+
"""Get localized step content with English fallback."""
163+
step = TOUR_STEPS.get(step_num, {})
164+
return step.get(language, step.get("en", {}))
165+
166+
167+
def render_onboarding_tour(
168+
language: str = "en",
169+
buddy_config: Optional[Dict[str, str]] = None,
170+
) -> str:
171+
"""Render the complete onboarding tour output.
172+
173+
Args:
174+
language: Language code (en, ko, ja, zh, es).
175+
buddy_config: Optional buddy customization from get_buddy_config().
176+
177+
Returns:
178+
Formatted onboarding tour string.
179+
"""
180+
bc = buddy_config or DEFAULT_BUDDY_CONFIG
181+
face = bc.get("face", BUDDY_FACE)
182+
welcome = _get_text(TOUR_WELCOME, language)
183+
184+
cyan = ANSI_COLORS["cyan"]
185+
yellow = ANSI_COLORS["yellow"]
186+
green = ANSI_COLORS["green"]
187+
magenta = ANSI_COLORS["magenta"]
188+
reset = ANSI_COLORS["reset"]
189+
190+
lines = [
191+
f"\u256d\u2501\u2501\u2501\u256e",
192+
f"\u2503 {face} \u2503 {cyan}{welcome}{reset}",
193+
f"\u2570\u2501\u2501\u2501\u256f",
194+
"",
195+
f"\u2501\u2501 {_get_text(TOUR_HEADER, language)} \u2501\u2501\u2501\u2501\u2501\u2501",
196+
]
197+
198+
for step_num in (1, 2, 3):
199+
step = _get_step(step_num, language)
200+
if not step:
201+
continue
202+
circled = _STEP_NUMBERS.get(step_num, str(step_num))
203+
title = step.get("title", "")
204+
body = step.get("body", "")
205+
example = step.get("example", "")
206+
207+
lines.append(f"")
208+
lines.append(f" {yellow}{circled}{reset} {green}{title}{reset}")
209+
lines.append(f" {body}")
210+
if example:
211+
lines.append(f" {magenta}\U0001f4a1 {example}{reset}")
212+
213+
lines.append("")
214+
lines.append(f"\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501")
215+
lines.append(f"\U0001f4ac {_get_text(TOUR_SKIP, language)}")
216+
217+
return "\n".join(lines)

packages/claude-code-plugin/hooks/session-start.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,19 @@ def main():
598598
buddy_section = cfg.get("buddy") if isinstance(cfg.get("buddy"), dict) else {}
599599
typing_enabled = buddy_section.get("typingEffect", True) and not os.environ.get("CI")
600600

601+
# First-run onboarding tour (#1037)
602+
try:
603+
from onboarding_tour import is_first_run, render_onboarding_tour, mark_onboarded
604+
if is_first_run() and not previous_session:
605+
tour_output = render_onboarding_tour(
606+
language=language, buddy_config=buddy_cfg,
607+
)
608+
if tour_output:
609+
print(tour_output, file=sys.stderr)
610+
mark_onboarded()
611+
except Exception:
612+
pass # Never block session start for tour
613+
601614
# Render and output
602615
output = render_session_start(
603616
scan_data, recommendations, tone, language,

0 commit comments

Comments
 (0)