Skip to content

Commit 760b64b

Browse files
committed
feat(plugin): add context-aware onboarding with project-specific suggestions (#1438)
- Add generate_suggestions() mapping scanner findings to mode-specific prompts - Low coverage → AUTO improve, framework → PLAN optimize, endpoints → EVAL security - i18n support (en, ko, ja, zh, es) for all suggestion templates - Generic fallback when scan data is insufficient - Integrate suggestions into render_onboarding_tour() via scan_result param - Pass scan_data from session-start.py to onboarding tour - 21 tests covering suggestion generation and tour integration Closes #1438
1 parent 323a3e5 commit 760b64b

3 files changed

Lines changed: 496 additions & 1 deletion

File tree

packages/claude-code-plugin/hooks/lib/onboarding_tour.py

Lines changed: 286 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"""
66
import os
77
from pathlib import Path
8-
from typing import Any, Dict, Optional
8+
from typing import Any, Dict, List, Optional
99

1010
from buddy_renderer import (
1111
ANSI_COLORS,
@@ -164,6 +164,275 @@ def get_tour_skip_message(lang: str) -> str:
164164
_STEP_NUMBERS = {1: "\u2460", 2: "\u2461", 3: "\u2462"}
165165

166166

167+
# ── Suggestion Templates (i18n) ───────────────────────────────────
168+
169+
_SUGGESTION_TEMPLATES: Dict[str, Dict[str, Dict[str, str]]] = {
170+
"low_coverage": {
171+
"en": {
172+
"mode": "AUTO",
173+
"prompt": "AUTO improve test coverage",
174+
"reason": "Test coverage is {coverage}%",
175+
},
176+
"ko": {
177+
"mode": "AUTO",
178+
"prompt": "AUTO 테스트 커버리지 개선",
179+
"reason": "테스트 커버리지가 {coverage}%입니다",
180+
},
181+
"ja": {
182+
"mode": "AUTO",
183+
"prompt": "AUTO テストカバレッジを改善",
184+
"reason": "テストカバレッジは{coverage}%です",
185+
},
186+
"zh": {
187+
"mode": "AUTO",
188+
"prompt": "AUTO 提高测试覆盖率",
189+
"reason": "测试覆盖率为{coverage}%",
190+
},
191+
"es": {
192+
"mode": "AUTO",
193+
"prompt": "AUTO mejorar cobertura de tests",
194+
"reason": "Cobertura de tests es {coverage}%",
195+
},
196+
},
197+
"no_coverage_with_files": {
198+
"en": {
199+
"mode": "PLAN",
200+
"prompt": "PLAN add test coverage for the project",
201+
"reason": "{file_count} source files with no test coverage data",
202+
},
203+
"ko": {
204+
"mode": "PLAN",
205+
"prompt": "PLAN 프로젝트 테스트 커버리지 추가",
206+
"reason": "{file_count}개 소스 파일에 테스트 커버리지 데이터 없음",
207+
},
208+
"ja": {
209+
"mode": "PLAN",
210+
"prompt": "PLAN プロジェクトのテストカバレッジを追加",
211+
"reason": "{file_count}個のソースファイルにテストカバレッジデータなし",
212+
},
213+
"zh": {
214+
"mode": "PLAN",
215+
"prompt": "PLAN 添加项目测试覆盖率",
216+
"reason": "{file_count}个源文件没有测试覆盖率数据",
217+
},
218+
"es": {
219+
"mode": "PLAN",
220+
"prompt": "PLAN agregar cobertura de tests al proyecto",
221+
"reason": "{file_count} archivos fuente sin datos de cobertura",
222+
},
223+
},
224+
"api_endpoints": {
225+
"en": {
226+
"mode": "EVAL",
227+
"prompt": "EVAL review API security",
228+
"reason": "{api_endpoints} API endpoint(s) to review",
229+
},
230+
"ko": {
231+
"mode": "EVAL",
232+
"prompt": "EVAL API 보안 검토",
233+
"reason": "{api_endpoints}개 API 엔드포인트 검토 필요",
234+
},
235+
"ja": {
236+
"mode": "EVAL",
237+
"prompt": "EVAL APIセキュリティをレビュー",
238+
"reason": "{api_endpoints}個のAPIエンドポイントをレビュー",
239+
},
240+
"zh": {
241+
"mode": "EVAL",
242+
"prompt": "EVAL 审查API安全",
243+
"reason": "{api_endpoints}个API端点需要审查",
244+
},
245+
"es": {
246+
"mode": "EVAL",
247+
"prompt": "EVAL revisar seguridad de API",
248+
"reason": "{api_endpoints} endpoint(s) de API para revisar",
249+
},
250+
},
251+
}
252+
253+
# Framework-specific suggestion templates
254+
_FRAMEWORK_SUGGESTIONS: Dict[str, Dict[str, Dict[str, str]]] = {
255+
"Next.js": {
256+
"en": {
257+
"prompt": "PLAN add Server Components optimization",
258+
"reason": "Next.js project detected",
259+
},
260+
"ko": {
261+
"prompt": "PLAN Server Components 최적화 추가",
262+
"reason": "Next.js 프로젝트 감지됨",
263+
},
264+
"ja": {
265+
"prompt": "PLAN Server Components最適化を追加",
266+
"reason": "Next.jsプロジェクトを検出",
267+
},
268+
"zh": {
269+
"prompt": "PLAN 添加Server Components优化",
270+
"reason": "检测到Next.js项目",
271+
},
272+
"es": {
273+
"prompt": "PLAN agregar optimizacion de Server Components",
274+
"reason": "Proyecto Next.js detectado",
275+
},
276+
},
277+
"NestJS": {
278+
"en": {
279+
"prompt": "PLAN add API validation with class-validator",
280+
"reason": "NestJS project detected",
281+
},
282+
"ko": {
283+
"prompt": "PLAN class-validator로 API 유효성 검증 추가",
284+
"reason": "NestJS 프로젝트 감지됨",
285+
},
286+
"ja": {
287+
"prompt": "PLAN class-validatorでAPIバリデーションを追加",
288+
"reason": "NestJSプロジェクトを検出",
289+
},
290+
"zh": {
291+
"prompt": "PLAN 使用class-validator添加API验证",
292+
"reason": "检测到NestJS项目",
293+
},
294+
"es": {
295+
"prompt": "PLAN agregar validacion de API con class-validator",
296+
"reason": "Proyecto NestJS detectado",
297+
},
298+
},
299+
"Vue": {
300+
"en": {
301+
"prompt": "PLAN add Composition API refactoring",
302+
"reason": "Vue project detected",
303+
},
304+
"ko": {
305+
"prompt": "PLAN Composition API 리팩토링 추가",
306+
"reason": "Vue 프로젝트 감지됨",
307+
},
308+
"ja": {
309+
"prompt": "PLAN Composition APIリファクタリングを追加",
310+
"reason": "Vueプロジェクトを検出",
311+
},
312+
"zh": {
313+
"prompt": "PLAN 添加Composition API重构",
314+
"reason": "检测到Vue项目",
315+
},
316+
"es": {
317+
"prompt": "PLAN agregar refactorizacion de Composition API",
318+
"reason": "Proyecto Vue detectado",
319+
},
320+
},
321+
"React": {
322+
"en": {
323+
"prompt": "PLAN optimize React component performance",
324+
"reason": "React project detected",
325+
},
326+
"ko": {
327+
"prompt": "PLAN React 컴포넌트 성능 최적화",
328+
"reason": "React 프로젝트 감지됨",
329+
},
330+
"ja": {
331+
"prompt": "PLAN Reactコンポーネントパフォーマンスを最適化",
332+
"reason": "Reactプロジェクトを検出",
333+
},
334+
"zh": {
335+
"prompt": "PLAN 优化React组件性能",
336+
"reason": "检测到React项目",
337+
},
338+
"es": {
339+
"prompt": "PLAN optimizar rendimiento de componentes React",
340+
"reason": "Proyecto React detectado",
341+
},
342+
},
343+
}
344+
345+
_GENERIC_FALLBACK: Dict[str, List[Dict[str, str]]] = {
346+
"en": [
347+
{"mode": "PLAN", "prompt": "PLAN add user authentication", "reason": "Common starting point for new projects"},
348+
],
349+
"ko": [
350+
{"mode": "PLAN", "prompt": "PLAN 사용자 인증 추가", "reason": "새 프로젝트의 일반적인 시작점"},
351+
],
352+
"ja": [
353+
{"mode": "PLAN", "prompt": "PLAN ユーザー認証を追加", "reason": "新規プロジェクトの一般的な出発点"},
354+
],
355+
"zh": [
356+
{"mode": "PLAN", "prompt": "PLAN 添加用户认证", "reason": "新项目的常见起点"},
357+
],
358+
"es": [
359+
{"mode": "PLAN", "prompt": "PLAN agregar autenticacion de usuario", "reason": "Punto de partida comun para nuevos proyectos"},
360+
],
361+
}
362+
363+
364+
def generate_suggestions(
365+
scan_result: Dict[str, Any],
366+
language: str = "en",
367+
) -> List[Dict[str, str]]:
368+
"""Generate project-specific prompt suggestions from scan data.
369+
370+
Maps scanner findings (coverage, framework, endpoints, file count)
371+
to mode-specific prompt templates. Falls back to generic suggestions
372+
when scan data is insufficient.
373+
374+
Args:
375+
scan_result: Output from project_scanner.scan_project().
376+
language: Language code (en, ko, ja, zh, es).
377+
378+
Returns:
379+
List of suggestion dicts, each with keys: mode, prompt, reason.
380+
"""
381+
suggestions: List[Dict[str, str]] = []
382+
lang = language if language in ("en", "ko", "ja", "zh", "es") else "en"
383+
384+
coverage = scan_result.get("coverage")
385+
framework = scan_result.get("framework", "")
386+
api_endpoints = scan_result.get("api_endpoints", 0)
387+
file_count = scan_result.get("file_count", 0)
388+
389+
# Low coverage → AUTO improve
390+
if coverage is not None and coverage < 80:
391+
tpl = _SUGGESTION_TEMPLATES["low_coverage"].get(lang, _SUGGESTION_TEMPLATES["low_coverage"]["en"])
392+
suggestions.append({
393+
"mode": tpl["mode"],
394+
"prompt": tpl["prompt"],
395+
"reason": tpl["reason"].format(coverage=coverage),
396+
})
397+
398+
# Files exist but no coverage data → suggest adding tests
399+
if coverage is None and file_count > 0:
400+
tpl = _SUGGESTION_TEMPLATES["no_coverage_with_files"].get(lang, _SUGGESTION_TEMPLATES["no_coverage_with_files"]["en"])
401+
suggestions.append({
402+
"mode": tpl["mode"],
403+
"prompt": tpl["prompt"],
404+
"reason": tpl["reason"].format(file_count=file_count),
405+
})
406+
407+
# Framework detected → PLAN framework-specific feature
408+
if framework:
409+
for fw_key, fw_tpl in _FRAMEWORK_SUGGESTIONS.items():
410+
if fw_key in framework:
411+
tpl = fw_tpl.get(lang, fw_tpl["en"])
412+
suggestions.append({
413+
"mode": "PLAN",
414+
"prompt": tpl["prompt"],
415+
"reason": tpl["reason"],
416+
})
417+
break
418+
419+
# API endpoints → EVAL security review
420+
if api_endpoints > 0:
421+
tpl = _SUGGESTION_TEMPLATES["api_endpoints"].get(lang, _SUGGESTION_TEMPLATES["api_endpoints"]["en"])
422+
suggestions.append({
423+
"mode": tpl["mode"],
424+
"prompt": tpl["prompt"],
425+
"reason": tpl["reason"].format(api_endpoints=api_endpoints),
426+
})
427+
428+
# Fallback to generic if no project-specific suggestions
429+
if not suggestions:
430+
fallback = _GENERIC_FALLBACK.get(lang, _GENERIC_FALLBACK["en"])
431+
suggestions.extend(fallback)
432+
433+
return suggestions
434+
435+
167436
def _get_text(mapping: Dict[str, str], language: str) -> str:
168437
"""Get localized text with English fallback."""
169438
return mapping.get(language, mapping.get("en", ""))
@@ -178,12 +447,17 @@ def _get_step(step_num: int, language: str) -> Dict[str, str]:
178447
def render_onboarding_tour(
179448
language: str = "en",
180449
buddy_config: Optional[Dict[str, str]] = None,
450+
scan_result: Optional[Dict[str, Any]] = None,
181451
) -> str:
182452
"""Render the complete onboarding tour output.
183453
454+
When scan_result is provided, step examples are replaced with
455+
project-specific prompt suggestions from generate_suggestions().
456+
184457
Args:
185458
language: Language code (en, ko, ja, zh, es).
186459
buddy_config: Optional buddy customization from get_buddy_config().
460+
scan_result: Optional project scan data for context-aware suggestions.
187461
188462
Returns:
189463
Formatted onboarding tour string.
@@ -198,6 +472,9 @@ def render_onboarding_tour(
198472
magenta = ANSI_COLORS["magenta"]
199473
reset = ANSI_COLORS["reset"]
200474

475+
# Generate project-specific suggestions if scan data available
476+
suggestions = generate_suggestions(scan_result, language) if scan_result else []
477+
201478
lines = [
202479
*render_face_banner(face, f"{cyan}{welcome}{reset}"),
203480
"",
@@ -213,6 +490,14 @@ def render_onboarding_tour(
213490
body = step.get("body", "")
214491
example = step.get("example", "")
215492

493+
# Replace example with project-specific suggestion if available
494+
suggestion_idx = step_num - 1
495+
if suggestions and suggestion_idx < len(suggestions):
496+
s = suggestions[suggestion_idx]
497+
example = s["prompt"]
498+
body_suffix = f" ({s['reason']})"
499+
body = body + body_suffix
500+
216501
lines.append(f"")
217502
lines.append(f" {yellow}{circled}{reset} {green}{title}{reset}")
218503
lines.append(f" {body}")

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -929,6 +929,7 @@ def main():
929929
if is_first_run() and not previous_session:
930930
tour_output = render_onboarding_tour(
931931
language=language, buddy_config=buddy_cfg,
932+
scan_result=scan_data if scan_data else None,
932933
)
933934
if tour_output:
934935
print(tour_output, file=sys.stderr)

0 commit comments

Comments
 (0)