diff --git a/apps/backend/internal/prompts/dto/dto.go b/apps/backend/internal/prompts/dto/dto.go index 027ee1077d..304dec67bc 100644 --- a/apps/backend/internal/prompts/dto/dto.go +++ b/apps/backend/internal/prompts/dto/dto.go @@ -10,6 +10,7 @@ type PromptDTO struct { ID string `json:"id"` Name string `json:"name"` Content string `json:"content"` + Builtin bool `json:"builtin"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } @@ -33,6 +34,7 @@ func FromPrompt(prompt *models.Prompt) PromptDTO { ID: prompt.ID, Name: prompt.Name, Content: prompt.Content, + Builtin: prompt.Builtin, CreatedAt: prompt.CreatedAt.UTC().Format(time.RFC3339), UpdatedAt: prompt.UpdatedAt.UTC().Format(time.RFC3339), } diff --git a/apps/backend/internal/prompts/models/prompt.go b/apps/backend/internal/prompts/models/prompt.go index a51c809f8f..0ba0d24ee4 100644 --- a/apps/backend/internal/prompts/models/prompt.go +++ b/apps/backend/internal/prompts/models/prompt.go @@ -6,6 +6,7 @@ type Prompt struct { ID string Name string Content string + Builtin bool CreatedAt time.Time UpdatedAt time.Time } diff --git a/apps/backend/internal/prompts/store/sqlite.go b/apps/backend/internal/prompts/store/sqlite.go index 472e9bbba3..8c6d1e0533 100644 --- a/apps/backend/internal/prompts/store/sqlite.go +++ b/apps/backend/internal/prompts/store/sqlite.go @@ -41,12 +41,21 @@ func (r *sqliteRepository) initSchema() error { id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, content TEXT NOT NULL, + builtin INTEGER NOT NULL DEFAULT 0, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL ); ` - _, err := r.db.Exec(schema) - return err + if _, err := r.db.Exec(schema); err != nil { + return err + } + + // Seed built-in prompts + if err := r.seedBuiltinPrompts(); err != nil { + return fmt.Errorf("failed to seed built-in prompts: %w", err) + } + + return nil } func (r *sqliteRepository) Close() error { @@ -58,9 +67,9 @@ func (r *sqliteRepository) Close() error { func (r *sqliteRepository) ListPrompts(ctx context.Context) ([]*models.Prompt, error) { rows, err := r.db.QueryContext(ctx, ` - SELECT id, name, content, created_at, updated_at + SELECT id, name, content, builtin, created_at, updated_at FROM custom_prompts - ORDER BY name ASC + ORDER BY builtin DESC, name ASC `) if err != nil { return nil, err @@ -72,9 +81,11 @@ func (r *sqliteRepository) ListPrompts(ctx context.Context) ([]*models.Prompt, e var prompts []*models.Prompt for rows.Next() { prompt := &models.Prompt{} - if err := rows.Scan(&prompt.ID, &prompt.Name, &prompt.Content, &prompt.CreatedAt, &prompt.UpdatedAt); err != nil { + var builtinInt int + if err := rows.Scan(&prompt.ID, &prompt.Name, &prompt.Content, &builtinInt, &prompt.CreatedAt, &prompt.UpdatedAt); err != nil { return nil, err } + prompt.Builtin = builtinInt == 1 prompts = append(prompts, prompt) } if err := rows.Err(); err != nil { @@ -85,27 +96,31 @@ func (r *sqliteRepository) ListPrompts(ctx context.Context) ([]*models.Prompt, e func (r *sqliteRepository) GetPromptByID(ctx context.Context, id string) (*models.Prompt, error) { row := r.db.QueryRowContext(ctx, ` - SELECT id, name, content, created_at, updated_at + SELECT id, name, content, builtin, created_at, updated_at FROM custom_prompts WHERE id = ? `, id) prompt := &models.Prompt{} - if err := row.Scan(&prompt.ID, &prompt.Name, &prompt.Content, &prompt.CreatedAt, &prompt.UpdatedAt); err != nil { + var builtinInt int + if err := row.Scan(&prompt.ID, &prompt.Name, &prompt.Content, &builtinInt, &prompt.CreatedAt, &prompt.UpdatedAt); err != nil { return nil, err } + prompt.Builtin = builtinInt == 1 return prompt, nil } func (r *sqliteRepository) GetPromptByName(ctx context.Context, name string) (*models.Prompt, error) { row := r.db.QueryRowContext(ctx, ` - SELECT id, name, content, created_at, updated_at + SELECT id, name, content, builtin, created_at, updated_at FROM custom_prompts WHERE name = ? `, name) prompt := &models.Prompt{} - if err := row.Scan(&prompt.ID, &prompt.Name, &prompt.Content, &prompt.CreatedAt, &prompt.UpdatedAt); err != nil { + var builtinInt int + if err := row.Scan(&prompt.ID, &prompt.Name, &prompt.Content, &builtinInt, &prompt.CreatedAt, &prompt.UpdatedAt); err != nil { return nil, err } + prompt.Builtin = builtinInt == 1 return prompt, nil } @@ -120,10 +135,15 @@ func (r *sqliteRepository) CreatePrompt(ctx context.Context, prompt *models.Prom } prompt.UpdatedAt = time.Now().UTC() + builtinInt := 0 + if prompt.Builtin { + builtinInt = 1 + } + _, err := r.db.ExecContext(ctx, ` - INSERT INTO custom_prompts (id, name, content, created_at, updated_at) - VALUES (?, ?, ?, ?, ?) - `, prompt.ID, prompt.Name, prompt.Content, prompt.CreatedAt, prompt.UpdatedAt) + INSERT INTO custom_prompts (id, name, content, builtin, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?) + `, prompt.ID, prompt.Name, prompt.Content, builtinInt, prompt.CreatedAt, prompt.UpdatedAt) return err } @@ -146,3 +166,223 @@ func (r *sqliteRepository) DeletePrompt(ctx context.Context, id string) error { _, err := r.db.ExecContext(ctx, `DELETE FROM custom_prompts WHERE id = ?`, id) return err } + +// seedBuiltinPrompts creates the default built-in prompts if they don't exist +func (r *sqliteRepository) seedBuiltinPrompts() error { + builtinPrompts := r.getBuiltinPrompts() + + for _, prompt := range builtinPrompts { + // Check if prompt already exists + var exists bool + err := r.db.QueryRow("SELECT 1 FROM custom_prompts WHERE id = ?", prompt.ID).Scan(&exists) + if err != nil && err != sql.ErrNoRows { + return err + } + if exists { + continue + } + + // Insert built-in prompt + _, err = r.db.Exec(` + INSERT INTO custom_prompts (id, name, content, builtin, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?) + `, prompt.ID, prompt.Name, prompt.Content, 1, prompt.CreatedAt, prompt.UpdatedAt) + if err != nil { + return fmt.Errorf("failed to insert built-in prompt %s: %w", prompt.ID, err) + } + } + + return nil +} + +// getBuiltinPrompts returns the predefined built-in prompts +func (r *sqliteRepository) getBuiltinPrompts() []*models.Prompt { + now := time.Now().UTC() + return []*models.Prompt{ + { + ID: "builtin-code-review", + Name: "code-review", + Builtin: true, + CreatedAt: now, + UpdatedAt: now, + Content: `Please review the changed files in the current git worktree. + +STEP 1: Determine what to review +- First, check if there are any uncommitted changes (dirty working directory) +- If there are uncommitted/staged changes: review those files +- If the working directory is clean: review the commits that have diverged from the master/main branch + +STEP 2: Review the changes, then output your findings in EXACTLY 4 sections: BUG, IMPROVEMENT, NITPICK, PERFORMANCE. + +Rules: +- Each section is OPTIONAL - only include it if you have findings for that category +- If a section has no findings, omit it entirely +- Format each finding as: filename:line_number - Description +- Be specific and reference exact line numbers +- Keep descriptions concise but actionable +- Sort findings by severity within each section +- Focus on logic and design issues, NOT formatting or style that automated tools handle + +Section definitions: + +BUG: Critical issues that will cause runtime errors, crashes, incorrect behavior, data corruption, or logic errors +- Examples: null/nil dereference, race conditions, incorrect algorithms, type mismatches, resource leaks, deadlocks + +IMPROVEMENT: Code quality, architecture, security, or maintainability concerns +- Examples: missing error handling, incorrect access modifiers (public/private/exported), SQL injection vulnerabilities, hardcoded credentials, tight coupling, missing validation, incorrect concurrency patterns + +NITPICK: Significant readability or maintainability issues that impact code understanding +- Examples: misleading variable/function names, overly complex logic that should be refactored, missing critical comments for complex algorithms, inconsistent error handling patterns +- EXCLUDE: formatting, whitespace, import ordering, trivial naming preferences, style issues handled by linters/formatters + +PERFORMANCE: Algorithmic or resource usage problems with measurable impact +- Examples: O(n²) where O(n) or O(1) is possible, unnecessary allocations in loops, missing indexes for database queries, blocking I/O in hot paths, regex compilation in loops, unbounded resource growth +- Concurrency-specific: unprotected shared state, missing synchronization, improper use of locks, goroutine leaks, missing context cancellation +- Prefer structured concurrency libraries (e.g., errgroup, conc) over raw primitives for better error handling and panic recovery + +Example format: +## BUG +- src/handler.go:45 - Dereferencing pointer without nil check will panic when user is not found +- lib/parser.rs:123 - Loop condition uses <= instead of < causing out-of-bounds access + +## IMPROVEMENT +- api/db.go:67 - Database query error ignored, will silently fail and return stale data +- services/auth.py:34 - Password comparison vulnerable to timing attacks, use constant-time comparison +- internal/user.go:15 - Type exported but only used internally, should be unexported + +## NITPICK +- components/processor.ts:12 - Function name 'doStuff' doesn't describe what it actually does (transforms user input to API format) +- utils/cache.go:89 - Error wrapped multiple times making original cause hard to trace + +## PERFORMANCE +- src/repository.go:156 - Linear search through slice on every request, use map for O(1) lookup +- handlers/api.py:45 - Compiling regex inside handler function, compile once at module level +- workers/processor.go:78 - Launching unbounded goroutines without limit, use worker pool or semaphore pattern +- db/queries.go:34 - N+1 query pattern, fetch all related records in single query with join + +Now review the changes.`, + }, + { + ID: "builtin-open-pr", + Name: "open-pr", + Builtin: true, + CreatedAt: now, + UpdatedAt: now, + Content: `Please create and open a Pull Request for the current branch using the GitHub CLI (gh). + +**PR Creation Steps:** +1. **Analyze the branch:** + - Review all commits on this branch + - Identify the changes made + - Understand the purpose and scope of the work + +2. **Check for PR template:** + - Look for a PR template in .github/pull_request_template.md or .github/PULL_REQUEST_TEMPLATE.md + - If a template exists, use it as the structure for the PR description + - If no template exists, use the default format below + +3. **Generate PR description:** + Create a comprehensive PR description that includes: + - **Title:** Clear, concise summary of the changes (50-72 characters) + - **Overview:** Brief description of what this PR does + - **Changes:** Detailed list of changes made + - **Motivation:** Why these changes are needed + - **Testing:** How the changes were tested + - **Screenshots/Examples:** If applicable (for UI changes) + - **Breaking Changes:** Any breaking changes (if applicable) + - **Related Issues:** Link to related issues (e.g., "Closes #123") + +4. **Default PR Description Template (if no template exists):** + - Overview section with brief description + - Changes section with bulleted list + - Motivation section explaining why + - Testing checklist (unit tests, integration tests, manual testing, all tests passing) + - Screenshots/Examples section if applicable + - Breaking Changes section if any + - Related Issues section with issue links + +5. **Create the PR:** + - Use 'gh pr create' command with appropriate flags + - Set the title and body based on the generated description + - Set appropriate labels if needed + - Request reviewers if applicable + - Link to related issues + +6. **Verify:** + - Confirm the PR was created successfully + - Provide the PR URL + - Summarize the PR details + +**Important:** +- Ensure you're on the correct branch before creating the PR +- Make sure all commits are pushed to the remote +- Verify the base branch is correct (usually 'main' or 'develop') +- Check that CI/CD checks are configured to run`, + }, + { + ID: "builtin-merge-base", + Name: "merge-base", + Builtin: true, + CreatedAt: now, + UpdatedAt: now, + Content: `Please merge the base branch into the current branch and resolve any conflicts that arise. + +**Merge Process:** + +1. **Pre-merge checks:** + - Identify the current branch name + - Identify the base branch (usually 'main', 'master', or 'develop') + - Check the current git status + - Ensure working directory is clean (commit or stash changes if needed) + - Fetch latest changes from remote + +2. **Perform the merge:** + - Execute: git fetch origin [base-branch] + - Execute: git merge origin/[base-branch] + - Check if there are any merge conflicts + +3. **If conflicts exist:** + - List all conflicting files + - For each conflicting file: + a. Show the conflict markers and surrounding context + b. Analyze both versions (current branch vs base branch) + c. Understand the intent of both changes + d. Resolve the conflict by: + - Keeping changes from current branch if they're the intended updates + - Keeping changes from base branch if they're necessary updates + - Combining both changes if they're complementary + - Rewriting the section if neither version is correct + e. Remove conflict markers (<<<<<<<, =======, >>>>>>>) + f. Ensure the resolved code is syntactically correct + g. Maintain code style and formatting consistency + +4. **Post-resolution:** + - Stage all resolved files: git add [resolved-files] + - Verify no conflicts remain: git status + - Run tests to ensure nothing broke: + - Run linters if available + - Run unit tests if available + - Run build/compile if applicable + - Complete the merge: git commit (or git merge --continue) + +5. **Verification:** + - Confirm merge was successful + - Show the merge commit + - List all files that were modified during conflict resolution + - Summarize what conflicts were resolved and how + +**Conflict Resolution Strategy:** +- **Understand context:** Always read the surrounding code to understand what each change is trying to accomplish +- **Preserve intent:** Keep changes that align with the feature/fix being developed +- **Maintain compatibility:** Ensure base branch updates (bug fixes, security patches) are preserved +- **Test thoroughly:** After resolution, verify the code works correctly +- **Document if needed:** Add comments if the resolution required complex logic + +**Important Notes:** +- If conflicts are too complex or risky, ask for human review +- Never blindly accept one side without understanding both changes +- Ensure code quality and functionality are maintained +- If tests fail after merge, fix the issues before completing the merge`, + }, + } +} diff --git a/apps/backend/internal/prompts/store/sqlite_test.go b/apps/backend/internal/prompts/store/sqlite_test.go index cafff66d41..eb9cb29858 100644 --- a/apps/backend/internal/prompts/store/sqlite_test.go +++ b/apps/backend/internal/prompts/store/sqlite_test.go @@ -70,11 +70,20 @@ func TestSQLiteRepository_CRUD(t *testing.T) { if err != nil { t.Fatalf("list prompts: %v", err) } - if len(list) != 1 { - t.Fatalf("expected 1 prompt, got %d", len(list)) + // Should have 1 custom prompt + 3 built-in prompts + if len(list) < 1 { + t.Fatalf("expected at least 1 prompt, got %d", len(list)) } - if list[0].Name != "Standup" { - t.Fatalf("expected updated name, got %q", list[0].Name) + // Find our custom prompt (built-in prompts come first due to ORDER BY) + var found bool + for _, p := range list { + if p.ID == prompt.ID && p.Name == "Standup" { + found = true + break + } + } + if !found { + t.Fatalf("expected to find updated prompt with name 'Standup'") } if err := repo.DeletePrompt(ctx, prompt.ID); err != nil { @@ -85,7 +94,41 @@ func TestSQLiteRepository_CRUD(t *testing.T) { if err != nil { t.Fatalf("list prompts after delete: %v", err) } - if len(list) != 0 { - t.Fatalf("expected no prompts after delete, got %d", len(list)) + // Should only have built-in prompts left (3) + builtinCount := 0 + for _, p := range list { + if p.Builtin { + builtinCount++ + } + if p.ID == prompt.ID { + t.Fatalf("expected custom prompt to be deleted, but it still exists") + } + } + if builtinCount != 3 { + t.Fatalf("expected 3 built-in prompts, got %d", builtinCount) + } +} + +func TestSQLiteRepository_BuiltinPrompts(t *testing.T) { + repo, cleanup := createTestRepo(t) + defer cleanup() + ctx := context.Background() + + // List prompts should include built-in prompts + list, err := repo.ListPrompts(ctx) + if err != nil { + t.Fatalf("list prompts: %v", err) + } + + // Should have 3 built-in prompts + builtinCount := 0 + for _, p := range list { + if p.Builtin { + builtinCount++ + } + } + + if builtinCount != 3 { + t.Fatalf("expected 3 built-in prompts, got %d", builtinCount) } } diff --git a/apps/web/components/settings/prompts-settings.tsx b/apps/web/components/settings/prompts-settings.tsx index b1511fb0db..78c9976396 100644 --- a/apps/web/components/settings/prompts-settings.tsx +++ b/apps/web/components/settings/prompts-settings.tsx @@ -1,8 +1,9 @@ 'use client'; import { useCallback, useMemo, useRef, useState, useEffect } from 'react'; -import { IconEdit, IconTrash } from '@tabler/icons-react'; +import { IconEdit, IconTrash, IconLock } from '@tabler/icons-react'; import { Button } from '@kandev/ui/button'; +import { Badge } from '@kandev/ui/badge'; import { Dialog, DialogContent, @@ -195,8 +196,14 @@ export function PromptsSettings() { ref={editingId === prompt.id ? editingRef : null} >