Skip to content

Commit 6e0742f

Browse files
authored
fix: dashboard and init improvements (#56)
* fix(mcp): use .env CURRENT_ROLE instead of git user for RBAC * fix(dashboard): update category dropdowns immediately and improve import modal style * fix(init): improve MCP tool selection UX and require list_category before list_convention * feat(templates): add category definitions to all policy templates * fix: remove unused multiselect template code
1 parent bf5b059 commit 6e0742f

9 files changed

Lines changed: 185 additions & 90 deletions

File tree

internal/cmd/mcp_register.go

Lines changed: 121 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -88,27 +88,13 @@ func promptMCPRegistration() {
8888
}
8989
}
9090

91-
// Use custom template to hide "type to filter" and typed characters
92-
restore := useMultiSelectTemplateNoFilter()
93-
defer restore()
94-
9591
fmt.Println()
9692
printTitle("MCP", "Register Symphony as an MCP server")
9793
fmt.Println(indent("Symphony MCP provides code convention tools for AI assistants"))
98-
fmt.Println(indent("(Use arrows to move, space to select, enter to submit)"))
9994
fmt.Println()
10095

101-
// Multi-select prompt for tools
102-
var selectedTools []string
103-
prompt := &survey.MultiSelect{
104-
Message: "Select vibe coding tools to integrate:",
105-
Options: mcpToolOptions,
106-
}
107-
108-
if err := survey.AskOne(prompt, &selectedTools); err != nil {
109-
fmt.Println("Skipped MCP registration")
110-
return
111-
}
96+
// Use Select with toggle behavior - Enter toggles selection, "Submit" confirms
97+
selectedTools := selectToolsWithEnterToggle(mcpToolOptions)
11298

11399
// If no tools selected, skip
114100
if len(selectedTools) == 0 {
@@ -414,9 +400,12 @@ func getClaudeCodeInstructions() string {
414400
415401
**Check MCP Status**: Verify Symphony MCP server is active. If unavailable, warn the user and do not proceed.
416402
417-
**Query Conventions**: Use ` + "`mcp__symphony__list_convention`" + ` to retrieve relevant rules.
418-
- Select appropriate category: security, style, documentation, error_handling, architecture, performance, testing
403+
**Query Categories First**: Use ` + "`mcp__symphony__list_category`" + ` to get available categories.
404+
- **IMPORTANT**: Do NOT invent category names. Only use categories returned by list_category.
405+
406+
**Query Conventions**: Use ` + "`mcp__symphony__list_convention`" + ` with a category from list_category.
419407
- Filter by languages as needed
408+
420409
**After Updating Rules/Categories**: If you add/edit/remove conventions or categories, run ` + "`mcp__symphony__convert`" + ` to regenerate derived policy and linter configs (then re-run validation if needed).
421410
422411
### 2. After Writing Code
@@ -428,11 +417,12 @@ func getClaudeCodeInstructions() string {
428417
## Workflow
429418
430419
1. Verify Symphony MCP is active
431-
2. Query conventions for your task
432-
3. Write code
433-
4. Validate with Symphony
434-
5. Fix violations
435-
6. Commit
420+
2. Query categories (list_category)
421+
3. Query conventions with valid category (list_convention)
422+
4. Write code
423+
5. Validate with Symphony
424+
6. Fix violations
425+
7. Commit
436426
` + symphonySectionEnd + "\n"
437427
}
438428

@@ -453,22 +443,15 @@ alwaysApply: true
453443
454444
### Before Code Generation
455445
1. **Verify Symphony MCP is active** - If not available, stop and warn user
456-
2. **Query conventions** - Use ` + "`symphony/list_convention`" + ` with appropriate category and language
457-
3. **After updating conventions/categories** - Use ` + "`symphony/convert`" + ` to regenerate derived policy and linter configs
446+
2. **Query categories first** - Use ` + "`symphony/list_category`" + ` to get available categories
447+
- **IMPORTANT**: Do NOT invent category names. Only use categories returned by list_category.
448+
3. **Query conventions** - Use ` + "`symphony/list_convention`" + ` with a category from step 2
449+
4. **After updating conventions/categories** - Use ` + "`symphony/convert`" + ` to regenerate derived policy and linter configs
458450
459451
### After Code Generation
460452
1. **Validate all changes** - Use ` + "`symphony/validate_code`" + `
461453
2. **Fix violations** - Address issues before committing
462454
463-
## Convention Categories
464-
- security
465-
- style
466-
- documentation
467-
- error_handling
468-
- architecture
469-
- performance
470-
- testing
471-
472455
---
473456
474457
*Auto-generated by Symphony*
@@ -491,20 +474,119 @@ This project uses Symphony MCP for automated code convention management.
491474
492475
### Before Writing Code
493476
1. Verify Symphony MCP server is active. If not available, warn user and stop.
494-
2. Query relevant conventions using symphony/list_convention tool.
495-
- Categories: security, style, documentation, error_handling, architecture, performance, testing
496-
- Filter by programming language
497-
3. If you add/edit/remove conventions or categories, run symphony/convert (then validate again if needed).
477+
2. Query available categories using symphony/list_category tool.
478+
- **IMPORTANT**: Do NOT invent category names. Only use categories returned by list_category.
479+
3. Query relevant conventions using symphony/list_convention tool with a category from step 2.
480+
- Filter by programming language as needed
481+
4. If you add/edit/remove conventions or categories, run symphony/convert (then validate again if needed).
498482
499483
### After Writing Code
500484
1. Always validate changes using symphony/validate_code tool (validates all git changes)
501485
2. Fix any violations found
502486
3. Only commit after validation passes
503487
504488
## Workflow
505-
Check MCP → Query Conventions → Write Code → Validate → Fix → Commit
489+
Check MCP → Query Categories → Query Conventions → Write Code → Validate → Fix → Commit
506490
507491
---
508492
Auto-generated by Symphony
509493
`
510494
}
495+
496+
// selectToolsWithEnterToggle allows users to select tools using Enter key to toggle
497+
// and "Submit" option to confirm selection
498+
func selectToolsWithEnterToggle(tools []string) []string {
499+
selected := make(map[string]bool)
500+
lastChoice := "" // Track last selected option to maintain cursor position
501+
502+
// Use custom template to hide message output
503+
restore := useSelectTemplateNoMessage()
504+
defer restore()
505+
506+
// Print header once with cyan hint
507+
fmt.Printf("Select tools to integrate: %s\n", colorize(cyan, "[Enter: toggle]"))
508+
509+
for {
510+
// Count selected items
511+
count := 0
512+
for _, v := range selected {
513+
if v {
514+
count++
515+
}
516+
}
517+
518+
// Build submit option with count
519+
var submitOption string
520+
if count > 0 {
521+
submitOption = fmt.Sprintf("✓ Submit (%d selected)", count)
522+
} else {
523+
submitOption = "✓ Submit"
524+
}
525+
526+
// Build options with selection indicators
527+
options := make([]string, 0, len(tools)+1)
528+
for _, tool := range tools {
529+
if selected[tool] {
530+
options = append(options, fmt.Sprintf("[x] %s", tool))
531+
} else {
532+
options = append(options, fmt.Sprintf("[ ] %s", tool))
533+
}
534+
}
535+
options = append(options, submitOption)
536+
537+
// Find default option index based on last choice
538+
defaultOption := options[0]
539+
if lastChoice != "" {
540+
for _, opt := range options {
541+
// Match by tool name (ignore [x]/[ ] prefix and submit option changes)
542+
if strings.HasPrefix(lastChoice, "✓") && strings.HasPrefix(opt, "✓") {
543+
defaultOption = opt
544+
break
545+
}
546+
for _, tool := range tools {
547+
if strings.Contains(lastChoice, tool) && strings.Contains(opt, tool) {
548+
defaultOption = opt
549+
break
550+
}
551+
}
552+
}
553+
}
554+
555+
// Show selection prompt
556+
var choice string
557+
prompt := &survey.Select{
558+
Message: "",
559+
Options: options,
560+
Default: defaultOption,
561+
}
562+
563+
if err := survey.AskOne(prompt, &choice); err != nil {
564+
// User cancelled
565+
return nil
566+
}
567+
568+
lastChoice = choice
569+
570+
// Check if Submit was selected
571+
if strings.HasPrefix(choice, "✓ Submit") {
572+
break
573+
}
574+
575+
// Toggle the selected tool
576+
for _, tool := range tools {
577+
if strings.Contains(choice, tool) {
578+
selected[tool] = !selected[tool]
579+
break
580+
}
581+
}
582+
}
583+
584+
// Collect selected tools
585+
var result []string
586+
for _, tool := range tools {
587+
if selected[tool] {
588+
result = append(result, tool)
589+
}
590+
}
591+
return result
592+
}

internal/cmd/survey_templates.go

Lines changed: 20 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -24,29 +24,6 @@ var selectTemplateNoFilter = `
2424
{{- end}}
2525
{{- end}}`
2626

27-
// Custom MultiSelect template that:
28-
// 1. Removes "type to filter" hint
29-
// 2. Hides typed characters (removes .FilterMessage)
30-
// 3. Shows clear control instructions
31-
var multiSelectTemplateNoFilter = `
32-
{{- define "option"}}
33-
{{- if eq .SelectedIndex .CurrentIndex }}{{color .Config.Icons.SelectFocus.Format }}{{ .Config.Icons.SelectFocus.Text }}{{color "reset"}}{{else}} {{end}}
34-
{{- if index .Checked .CurrentOpt.Index }}{{color .Config.Icons.MarkedOption.Format }} {{ .Config.Icons.MarkedOption.Text }} {{else}}{{color .Config.Icons.UnmarkedOption.Format }} {{ .Config.Icons.UnmarkedOption.Text }} {{end}}
35-
{{- color "reset"}}
36-
{{- " "}}{{- .CurrentOpt.Value}}{{ if ne ($.GetDescription .CurrentOpt) "" }} - {{color "cyan"}}{{ $.GetDescription .CurrentOpt }}{{color "reset"}}{{end}}
37-
{{end}}
38-
{{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}}
39-
{{- color .Config.Icons.Question.Format }}{{ .Config.Icons.Question.Text }} {{color "reset"}}
40-
{{- color "default+hb"}}{{ .Message }}{{color "reset"}}
41-
{{- if .ShowAnswer}}{{color "cyan"}} {{.Answer}}{{color "reset"}}{{"\n"}}
42-
{{- else }}
43-
{{- " "}}{{- color "cyan"}}[Arrow keys: move, Space: toggle, Enter: confirm]{{color "reset"}}
44-
{{- "\n"}}
45-
{{- range $ix, $option := .PageEntries}}
46-
{{- template "option" $.IterateOption $ix $option}}
47-
{{- end}}
48-
{{- end}}`
49-
5027
// useSelectTemplateNoFilter temporarily overrides the global Select template
5128
// to hide "type to filter" and prevent typed characters from showing.
5229
// Returns a restore function that must be called to restore the original template.
@@ -58,13 +35,26 @@ func useSelectTemplateNoFilter() func() {
5835
}
5936
}
6037

61-
// useMultiSelectTemplateNoFilter temporarily overrides the global MultiSelect template
62-
// to hide "type to filter" and prevent typed characters from showing.
63-
// Returns a restore function that must be called to restore the original template.
64-
func useMultiSelectTemplateNoFilter() func() {
65-
original := survey.MultiSelectQuestionTemplate
66-
survey.MultiSelectQuestionTemplate = multiSelectTemplateNoFilter
38+
// Custom Select template with no message output - only shows options
39+
var selectTemplateNoMessage = `
40+
{{- define "option"}}
41+
{{- if eq .SelectedIndex .CurrentIndex }}{{color .Config.Icons.SelectFocus.Format }}{{ .Config.Icons.SelectFocus.Text }} {{else}}{{color "default"}} {{end}}
42+
{{- .CurrentOpt.Value}}
43+
{{- color "reset"}}
44+
{{end}}
45+
{{- if .ShowAnswer}}{{/* hide answer line */}}
46+
{{- else}}
47+
{{- range $ix, $option := .PageEntries}}
48+
{{- template "option" $.IterateOption $ix $option}}
49+
{{- end}}
50+
{{- end}}`
51+
52+
// useSelectTemplateNoMessage temporarily overrides the global Select template
53+
// to hide message and answer output. Only shows options.
54+
func useSelectTemplateNoMessage() func() {
55+
original := survey.SelectQuestionTemplate
56+
survey.SelectQuestionTemplate = selectTemplateNoMessage
6757
return func() {
68-
survey.MultiSelectQuestionTemplate = original
58+
survey.SelectQuestionTemplate = original
6959
}
7060
}

internal/mcp/server.go

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -853,48 +853,36 @@ func (s *Server) convertUserPolicy(userPolicyPath, codePolicyPath string) error
853853
return ConvertPolicyWithLLM(userPolicyPath, codePolicyPath)
854854
}
855855

856-
// getRBACInfo returns RBAC information for the current user
856+
// getRBACInfo returns RBAC information for the current role
857857
func (s *Server) getRBACInfo() string {
858-
// Try to get current user
859-
username, err := git.GetCurrentUser()
860-
if err != nil {
861-
// Not in a git environment or user not configured
858+
// Get current role from .env
859+
userRole, err := roles.GetCurrentRole()
860+
if err != nil || userRole == "" {
861+
// No role selected
862862
return ""
863863
}
864864

865-
// Get user's role
866-
userRole, err := roles.GetUserRole(username)
867-
if err != nil {
868-
// Roles not configured
869-
return ""
870-
}
871-
872-
if userRole == "none" {
873-
return fmt.Sprintf("⚠️ RBAC: User '%s' has no assigned role. You may not have permission to modify files.", username)
874-
}
875-
876865
// Load user policy to get RBAC details
877866
userPolicy, err := roles.LoadUserPolicyFromRepo()
878867
if err != nil {
879868
// User policy not available
880-
return fmt.Sprintf("🔐 RBAC: Current user '%s' has role '%s'", username, userRole)
869+
return fmt.Sprintf("🔐 RBAC: Current role '%s'", userRole)
881870
}
882871

883872
// Check if RBAC is defined
884873
if userPolicy.RBAC == nil || userPolicy.RBAC.Roles == nil {
885-
return fmt.Sprintf("🔐 RBAC: Current user '%s' has role '%s' (no restrictions defined)", username, userRole)
874+
return fmt.Sprintf("🔐 RBAC: Current role '%s' (no restrictions defined)", userRole)
886875
}
887876

888877
// Get role configuration
889878
roleConfig, exists := userPolicy.RBAC.Roles[userRole]
890879
if !exists {
891-
return fmt.Sprintf("⚠️ RBAC: User '%s' has role '%s', but role is not defined in policy", username, userRole)
880+
return fmt.Sprintf("⚠️ RBAC: Role '%s' is not defined in policy", userRole)
892881
}
893882

894883
// Build RBAC info message
895884
var rbacMsg strings.Builder
896885
rbacMsg.WriteString("🔐 RBAC Information:\n")
897-
rbacMsg.WriteString(fmt.Sprintf(" User: %s\n", username))
898886
rbacMsg.WriteString(fmt.Sprintf(" Role: %s\n", userRole))
899887

900888
if len(roleConfig.AllowWrite) > 0 {

internal/policy/templates/demo-template.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
{
22
"version": "1.0.0",
3+
"category": [
4+
{"name": "naming", "description": "Naming conventions for classes, methods, and variables"},
5+
{"name": "error_handling", "description": "Error handling and exception management rules"}
6+
],
37
"defaults": {
48
"languages": ["java"],
59
"severity": "error"

internal/policy/templates/react-template.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
{
22
"version": "1.0.0",
3+
"category": [
4+
{"name": "naming", "description": "Naming conventions for components, variables, and functions"},
5+
{"name": "error_handling", "description": "Error handling and React Hooks best practices"},
6+
{"name": "formatting", "description": "Code formatting and component structure rules"},
7+
{"name": "performance", "description": "Performance optimization and rendering efficiency"}
8+
],
39
"defaults": {
410
"languages": ["javascript", "typescript", "jsx", "tsx"],
511
"severity": "error",

internal/policy/templates/typescript-template.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
{
22
"version": "1.0.0",
3+
"category": [
4+
{"name": "error_handling", "description": "Type safety and error handling rules"},
5+
{"name": "naming", "description": "Naming conventions for types, interfaces, and variables"},
6+
{"name": "formatting", "description": "Code formatting and module structure"},
7+
{"name": "documentation", "description": "Documentation rules (JSDoc, type annotations)"}
8+
],
39
"defaults": {
410
"languages": ["typescript"],
511
"severity": "error",

internal/policy/templates/vue-template.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
{
22
"version": "1.0.0",
3+
"category": [
4+
{"name": "naming", "description": "Naming conventions for components and files"},
5+
{"name": "formatting", "description": "Code formatting and Composition API structure"},
6+
{"name": "error_handling", "description": "Error handling and reactive state management"},
7+
{"name": "performance", "description": "Performance optimization and computed properties"}
8+
],
39
"defaults": {
410
"languages": ["javascript", "typescript", "vue"],
511
"severity": "error",

internal/server/static/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ <h3 class="text-xl font-semibold text-slate-900 flex items-center gap-2">
216216

217217
<!-- Import Modal -->
218218
<div id="import-modal" class="fixed inset-0 modal-overlay flex items-center justify-center p-4 z-50 hidden">
219-
<div class="w-full max-w-lg bg-white rounded-lg shadow-xl border border-gray-300">
219+
<div class="w-[28rem] max-w-[calc(100vw-2rem)] bg-white rounded-lg shadow-xl border border-gray-300">
220220
<header class="p-6 flex items-center justify-between border-b border-gray-300">
221221
<h3 class="text-xl font-semibold text-slate-900">📥 컨벤션 가져오기</h3>
222222
<button id="close-import-modal" class="text-slate-500 hover:text-slate-800 text-2xl">&times;</button>

0 commit comments

Comments
 (0)