Add workspace onboarding: CLI hints, web checklist, codebase detection, and relation field fix

- Print suggested /pad prompts after `pad init` creates a new workspace
- Add `pad onboard` command that detects project tooling (language, build system,
  test runner, CI, linter) and suggests matching conventions from the library
- Replace empty workspace welcome box with OnboardingChecklist component showing
  a 4-step guided setup with progress bar and /pad prompt hints
- Add contextual tips with /pad prompts to empty collection states
- Add onboarding workflow to /pad skill for agent-driven codebase analysis
- Fix relation fields storing slugs instead of UUIDs: server now resolves
  slugs/refs to UUIDs for relation-type fields on both create and update
This commit is contained in:
xarmian
2026-03-27 19:33:18 +00:00
parent 14fe7fd631
commit d318ecf7fc
8 changed files with 707 additions and 30 deletions
+152
View File
@@ -45,6 +45,7 @@ func main() {
serveCmd(),
stopCmd(),
initCmd(),
onboardCmd(),
workspacesCmd(),
switchCmd(),
skillsCmd(),
@@ -285,6 +286,7 @@ Use --list-templates to see available templates.`,
}
}
newlyCreated := false
if ws != nil {
if err := cli.WriteWorkspaceLink(cwd, ws.Slug); err != nil {
return fmt.Errorf("write .pad.toml: %w", err)
@@ -309,9 +311,14 @@ Use --list-templates to see available templates.`,
}
fmt.Printf("Created workspace %q (slug: %s)%s\n", ws.Name, ws.Slug, tmplMsg)
fmt.Printf("Linked to %s\n", cwd)
newlyCreated = true
}
offerSkillInstall()
if newlyCreated {
printOnboardingHints()
}
return nil
},
}
@@ -386,6 +393,151 @@ func readChoice() string {
return strings.TrimSpace(input)
}
func printOnboardingHints() {
fmt.Println()
fmt.Println("Get started:")
fmt.Println(" /pad scan this codebase and set up my workspace")
fmt.Println(" /pad what conventions should this project follow?")
fmt.Println(" /pad create a phase for what I'm working on")
fmt.Println()
fmt.Println("Or open the web UI at http://localhost:7777")
}
// --- onboard ---
func onboardCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "onboard",
Short: "Analyze the project and suggest items to populate the workspace",
Long: `Analyze the current project directory to detect tooling and suggest
conventions, then optionally create them in the workspace.
This scans for build config, CI setup, linters, and project structure to
recommend conventions from the built-in library.`,
RunE: func(cmd *cobra.Command, args []string) error {
client, _ := getClient()
ws := getWorkspace()
cwd, _ := os.Getwd()
info := cli.DetectProject(cwd)
// Print detection results
fmt.Println("Scanning project...")
if info.Language != "" {
fmt.Printf(" Language: %s\n", info.Language)
}
if info.BuildTool != "" {
fmt.Printf(" Build: %s\n", info.BuildTool)
}
if info.TestCmd != "" {
fmt.Printf(" Tests: %s\n", info.TestCmd)
}
if info.HasCI {
fmt.Printf(" CI: %s\n", info.CIProvider)
}
if info.HasLinter {
fmt.Println(" Linter: detected")
}
if info.Language == "" && info.BuildTool == "" {
fmt.Println(" Could not detect project type.")
fmt.Println()
fmt.Println("Try using /pad to set up your workspace conversationally:")
fmt.Println(" /pad scan this codebase and set up my workspace")
return nil
}
fmt.Println()
// Get suggested conventions
suggestions := cli.SuggestedConventions(info)
// Check which are already active
existingConventions, _ := client.ListCollectionItems(ws, "conventions", nil)
existingTitles := make(map[string]bool)
for _, item := range existingConventions {
existingTitles[item.Title] = true
}
// Filter to new suggestions only
type suggestion struct {
title string
content string
}
var newSuggestions []suggestion
for title, content := range suggestions {
if !existingTitles[title] {
newSuggestions = append(newSuggestions, suggestion{title, content})
}
}
if len(newSuggestions) == 0 {
fmt.Println("All suggested conventions are already active.")
return nil
}
fmt.Printf("Suggested conventions (%d new):\n", len(newSuggestions))
for i, s := range newSuggestions {
fmt.Printf(" %d. %s\n", i+1, s.title)
}
if !cli.IsTerminal() {
// Non-interactive: just print suggestions
fmt.Println()
fmt.Println("Run 'pad onboard' in a terminal to activate, or use:")
fmt.Println(" /pad what conventions should this project follow?")
return nil
}
fmt.Print("\nCreate these conventions? (y/N): ")
choice := readChoice()
if choice != "y" && choice != "Y" {
fmt.Println("Skipped. You can activate conventions from the library:")
fmt.Printf(" http://localhost:7777/%s/library\n", ws)
return nil
}
// Look up library conventions to get proper trigger/scope/priority
libraryConventions := collections.ConventionLibrary()
libraryMap := make(map[string]collections.LibraryConvention)
for _, cat := range libraryConventions {
for _, conv := range cat.Conventions {
libraryMap[conv.Title] = conv
}
}
created := 0
for _, s := range newSuggestions {
// Use library metadata if available, otherwise use sensible defaults
trigger := "on-implement"
scope := "all"
priority := "should"
if lc, ok := libraryMap[s.title]; ok {
trigger = lc.Trigger
scope = lc.Scope
priority = lc.Priority
}
fieldsJSON := fmt.Sprintf(`{"status":"active","trigger":"%s","scope":"%s","priority":"%s"}`, trigger, scope, priority)
_, err := client.CreateItem(ws, "conventions", models.ItemCreate{
Title: s.title,
Content: s.content,
Fields: fieldsJSON,
})
if err != nil {
fmt.Fprintf(os.Stderr, " Failed to create %q: %v\n", s.title, err)
continue
}
fmt.Printf(" Created: %s\n", s.title)
created++
}
fmt.Printf("\n%d conventions created.\n", created)
return nil
},
}
return cmd
}
// --- skills ---
func skillsCmd() *cobra.Command {
+1 -1
View File
@@ -8,4 +8,4 @@ var WebUI embed.FS
//go:embed skills/pad/SKILL.md
var PadSkill []byte
// embed cache bust: 1774577313
// embed cache bust: 1774639009
+182
View File
@@ -0,0 +1,182 @@
package cli
import (
"os"
"path/filepath"
"strings"
)
// ProjectInfo holds detected project metadata.
type ProjectInfo struct {
Language string // go, node, rust, python, java, etc.
BuildTool string // make, npm, cargo, pip, maven, etc.
TestCmd string // detected test command
HasCI bool // CI config detected
CIProvider string // github-actions, gitlab, circleci, etc.
HasLinter bool // linter config detected
Frameworks []string // detected frameworks (e.g., svelte, react, chi)
}
// DetectProject analyzes the current directory to determine project type,
// build system, test runner, CI, and other metadata.
func DetectProject(dir string) ProjectInfo {
info := ProjectInfo{}
// Detect language and build tool
if fileExists(dir, "go.mod") {
info.Language = "go"
if fileExists(dir, "Makefile") {
info.BuildTool = "make"
info.TestCmd = detectMakeTestCmd(dir)
} else {
info.BuildTool = "go"
info.TestCmd = "go test ./..."
}
} else if fileExists(dir, "package.json") {
info.Language = "node"
info.BuildTool = "npm"
info.TestCmd = "npm test"
if fileExists(dir, "yarn.lock") {
info.BuildTool = "yarn"
info.TestCmd = "yarn test"
} else if fileExists(dir, "pnpm-lock.yaml") {
info.BuildTool = "pnpm"
info.TestCmd = "pnpm test"
}
// Check for TypeScript
if fileExists(dir, "tsconfig.json") {
info.Language = "typescript"
}
} else if fileExists(dir, "Cargo.toml") {
info.Language = "rust"
info.BuildTool = "cargo"
info.TestCmd = "cargo test"
} else if fileExists(dir, "pyproject.toml") || fileExists(dir, "setup.py") || fileExists(dir, "requirements.txt") {
info.Language = "python"
if fileExists(dir, "pyproject.toml") {
info.BuildTool = "pip"
info.TestCmd = "pytest"
} else {
info.BuildTool = "pip"
info.TestCmd = "python -m pytest"
}
} else if fileExists(dir, "pom.xml") {
info.Language = "java"
info.BuildTool = "maven"
info.TestCmd = "mvn test"
} else if fileExists(dir, "build.gradle") || fileExists(dir, "build.gradle.kts") {
info.Language = "java"
info.BuildTool = "gradle"
info.TestCmd = "gradle test"
} else if fileExists(dir, "Makefile") {
info.BuildTool = "make"
info.TestCmd = detectMakeTestCmd(dir)
}
// Detect CI
if dirExists(dir, ".github", "workflows") {
info.HasCI = true
info.CIProvider = "github-actions"
} else if fileExists(dir, ".gitlab-ci.yml") {
info.HasCI = true
info.CIProvider = "gitlab"
} else if dirExists(dir, ".circleci") {
info.HasCI = true
info.CIProvider = "circleci"
}
// Detect linter
if fileExists(dir, ".eslintrc.json") || fileExists(dir, ".eslintrc.js") || fileExists(dir, "eslint.config.js") || fileExists(dir, "eslint.config.mjs") {
info.HasLinter = true
} else if fileExists(dir, ".golangci.yml") || fileExists(dir, ".golangci.yaml") {
info.HasLinter = true
} else if fileExists(dir, ".prettierrc") || fileExists(dir, ".prettierrc.json") {
info.HasLinter = true
} else if fileExists(dir, "rustfmt.toml") || fileExists(dir, ".rustfmt.toml") {
info.HasLinter = true
} else if fileExists(dir, ".flake8") || fileExists(dir, "ruff.toml") || fileExists(dir, ".ruff.toml") {
info.HasLinter = true
}
return info
}
// SuggestedConventions returns convention titles from the library that match
// the detected project info. Returns a map of title → customized content.
func SuggestedConventions(info ProjectInfo) map[string]string {
suggestions := make(map[string]string)
// Build convention
if info.BuildTool != "" {
var buildCmd string
switch info.BuildTool {
case "make":
buildCmd = "make build"
case "npm":
buildCmd = "npm run build"
case "yarn":
buildCmd = "yarn build"
case "pnpm":
buildCmd = "pnpm build"
case "cargo":
buildCmd = "cargo build"
case "go":
buildCmd = "go build ./..."
case "maven":
buildCmd = "mvn compile"
case "gradle":
buildCmd = "gradle build"
default:
buildCmd = info.BuildTool + " build"
}
suggestions["Rebuild after code changes"] = "After modifying source code, run `" + buildCmd + "` to verify everything compiles and builds successfully."
}
// Test convention
if info.TestCmd != "" {
suggestions["Run tests before completing tasks"] = "Run the project's test suite (`" + info.TestCmd + "`) before marking any task as done. If tests fail, fix them before completing the task."
}
// Linter convention
if info.HasLinter {
suggestions["Run linter before committing"] = "Run the project's linter/formatter before committing code to ensure consistent code style."
}
// CI convention
if info.HasCI {
suggestions["Verify locally before PR"] = "Before creating a PR, verify the changes work locally: build succeeds, tests pass, and the feature works as expected."
}
// Always suggest these general ones
suggestions["Commit after task completion"] = "Create a git commit with a descriptive message after completing each discrete unit of work. Reference the task slug or item number in the commit message."
suggestions["Update task status when starting work"] = "When starting work on a task, update its status to in-progress: `pad update <slug> --status in-progress`"
return suggestions
}
func fileExists(dir, name string) bool {
_, err := os.Stat(filepath.Join(dir, name))
return err == nil
}
func dirExists(parts ...string) bool {
info, err := os.Stat(filepath.Join(parts...))
return err == nil && info.IsDir()
}
func detectMakeTestCmd(dir string) string {
data, err := os.ReadFile(filepath.Join(dir, "Makefile"))
if err != nil {
return "make test"
}
content := string(data)
// Check if Makefile has a test target
if strings.Contains(content, "test:") || strings.Contains(content, "test :") {
return "make test"
}
// Fall back to language-specific if no test target
if strings.Contains(content, "go test") {
return "go test ./..."
}
return "make test"
}
+64
View File
@@ -3,6 +3,7 @@ package server
import (
"database/sql"
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
@@ -113,6 +114,12 @@ func (s *Server) handleCreateItem(w http.ResponseWriter, r *http.Request) {
}
}
// Resolve relation fields (slugs/refs → UUIDs) before validation
if err := s.resolveRelationFields(workspaceID, fieldMap, schema); err != nil {
writeError(w, http.StatusBadRequest, "bad_request", err.Error())
return
}
if err := items.ValidateFields(fieldMap, schema); err != nil {
writeError(w, http.StatusBadRequest, "validation_error", err.Error())
return
@@ -207,6 +214,12 @@ func (s *Server) handleUpdateItem(w http.ResponseWriter, r *http.Request) {
return
}
// Resolve relation fields (slugs/refs → UUIDs) before validation
if err := s.resolveRelationFields(workspaceID, fieldMap, schema); err != nil {
writeError(w, http.StatusBadRequest, "bad_request", err.Error())
return
}
if err := items.ValidateFields(fieldMap, schema); err != nil {
writeError(w, http.StatusBadRequest, "validation_error", err.Error())
return
@@ -365,6 +378,57 @@ func (s *Server) handleGetItemTasks(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, tasks)
}
// resolveRelationFields resolves slugs, PREFIX-NUMBER refs, and other identifiers
// in relation fields to their canonical UUIDs. This allows clients to send
// human-readable identifiers (e.g. --field phase=workspace-onboarding) and have
// them stored as UUIDs that the dashboard and queries expect.
func (s *Server) resolveRelationFields(workspaceID string, fields map[string]any, schema models.CollectionSchema) error {
for _, def := range schema.Fields {
if def.Type != "relation" {
continue
}
val, exists := fields[def.Key]
if !exists || val == nil {
continue
}
strVal, ok := val.(string)
if !ok || strVal == "" {
continue
}
// Already a UUID — nothing to resolve
if isUUID(strVal) {
continue
}
// Resolve the identifier (slug, PREFIX-NUMBER, etc.) to an item
item, err := s.store.ResolveItem(workspaceID, strVal)
if err != nil {
return fmt.Errorf("field %q: failed to resolve %q: %w", def.Key, strVal, err)
}
if item == nil {
return fmt.Errorf("field %q: item %q not found", def.Key, strVal)
}
fields[def.Key] = item.ID
}
return nil
}
// isUUID checks if a string looks like a UUID (8-4-4-4-12 hex).
func isUUID(s string) bool {
if len(s) != 36 {
return false
}
for i, c := range s {
if i == 8 || i == 13 || i == 18 || i == 23 {
if c != '-' {
return false
}
} else if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
return false
}
}
return true
}
// autoPopulateDates auto-fills start_date/end_date when status changes to active/completed.
// Only sets dates if the schema defines those date fields and the field is currently empty.
func autoPopulateDates(newFields map[string]any, existingFieldsJSON string, schema models.CollectionSchema) {
+23
View File
@@ -69,6 +69,10 @@ Interpret the user's intent and route to the appropriate action. Here are common
**Retrospective:**
- "phase 2 is done, let's retro" → Review completed work, save retrospective
**Onboarding:**
- "scan this codebase" / "set up my workspace" → Codebase analysis + onboarding workflow (see below)
- "what conventions should this project follow?" → Analyze tooling, suggest conventions from the library
## Before Performing Work
When you are about to take action (implement code, complete a task, create a PR, etc.), load the relevant conventions and playbooks FIRST:
@@ -195,6 +199,25 @@ All commands support `--format json` (for parsing) or `--format table` (default,
3. Run `pad status --format json` for blockers/attention items
4. Present as: Yesterday / Today / Blockers format
### Onboarding: "Scan this codebase" / "Set up my workspace"
1. **Check workspace state:** `pad status --format json` — if the workspace already has items, ask if they want to add more or start fresh sections.
2. **Analyze the codebase:** Read key project files to understand the project:
- `README.md` or `README` — project overview, setup instructions
- `CLAUDE.md` — existing AI/agent instructions
- Build config: `Makefile`, `package.json`, `go.mod`, `Cargo.toml`, `pyproject.toml`, `pom.xml`
- CI config: `.github/workflows/`, `.gitlab-ci.yml`, `.circleci/`
- Directory structure: `ls` the top-level directories to understand the layout
3. **Detect project type and tooling:**
- Language: Go, Node/TypeScript, Rust, Python, Java, etc.
- Build system: make, npm, cargo, pip, maven, etc.
- Test runner: what command runs the tests?
- Linter/formatter: what tools enforce code style?
4. **Suggest conventions:** Based on the detected tooling, suggest conventions from the library. Customize the content with the actual commands found in the project (e.g., "Run `make test`" not just "Run the test suite"). Present as a checklist and ask which to activate.
5. **Draft an architecture doc:** Summarize the project structure, tech stack, key directories, and how the pieces fit together. Offer to save as a Doc item.
6. **Propose an initial phase:** Based on recent git activity (`git log --oneline -20`) and any open TODOs, suggest a phase name and a few starter tasks. Ask before creating.
7. **Always confirm before creating each item.** Show what will be created, get approval, then create.
### Retrospective: "Phase X is done, let's retro"
1. Load the phase: `pad show <phase-slug> --format markdown`
@@ -0,0 +1,256 @@
<script lang="ts">
interface Props {
wsSlug: string;
byCollection: Record<string, Record<string, number>>;
}
let { wsSlug, byCollection }: Props = $props();
function collectionHasItems(slug: string): boolean {
const breakdown = byCollection[slug];
if (!breakdown) return false;
return Object.values(breakdown).reduce((sum, n) => sum + n, 0) > 0;
}
function collectionItemCount(slug: string): number {
const breakdown = byCollection[slug];
if (!breakdown) return 0;
return Object.values(breakdown).reduce((sum, n) => sum + n, 0);
}
interface Step {
title: string;
href: string;
done: boolean;
hint: string;
}
let steps = $derived<Step[]>([
{
title: 'Add project conventions',
href: `/${wsSlug}/library`,
done: collectionHasItems('conventions'),
hint: '/pad what conventions should this project follow?'
},
{
title: 'Create your first phase',
href: `/${wsSlug}/new?collection=phases`,
done: collectionHasItems('phases'),
hint: '/pad create a phase for what I\'m working on'
},
{
title: 'Add a few tasks',
href: `/${wsSlug}/new?collection=tasks`,
done: collectionItemCount('tasks') >= 3,
hint: '/pad break down my current work into tasks'
},
{
title: 'Write an architecture doc',
href: `/${wsSlug}/new?collection=docs`,
done: collectionHasItems('docs'),
hint: '/pad document the architecture of this project'
}
]);
let completedCount = $derived(steps.filter((s) => s.done).length);
let progressPct = $derived(Math.round((completedCount / steps.length) * 100));
</script>
<div class="onboarding">
<div class="onboarding-header">
<h2>Set up your workspace</h2>
<p class="subtitle">Complete these steps to get the most out of Pad.</p>
</div>
<div class="progress-section">
<span class="progress-label">{completedCount} of {steps.length} complete</span>
<div class="progress-track">
<div class="progress-fill" style:width="{progressPct}%"></div>
</div>
</div>
<ol class="step-list">
{#each steps as step (step.title)}
<li class="step" class:done={step.done}>
<div class="step-icon">
{#if step.done}
<svg class="check-icon" viewBox="0 0 20 20" fill="currentColor" width="20" height="20">
<circle cx="10" cy="10" r="10" />
<path d="M6 10.5l2.5 2.5 5.5-5.5" stroke="#fff" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" />
</svg>
{:else}
<svg class="empty-icon" viewBox="0 0 20 20" width="20" height="20">
<circle cx="10" cy="10" r="9" stroke="currentColor" stroke-width="1.5" fill="none" />
</svg>
{/if}
</div>
<div class="step-body">
<a href={step.href} class="step-title">{step.title}</a>
{#if !step.done}
<span class="step-hint">Try: <code>{step.hint}</code></span>
{/if}
</div>
</li>
{/each}
</ol>
<div class="onboarding-footer">
<a href="/{wsSlug}/library" class="footer-link">Or open the library to browse conventions and playbooks</a>
<span class="footer-muted">View web UI at http://localhost:7777</span>
</div>
</div>
<style>
.onboarding {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: var(--space-6);
max-width: 600px;
}
.onboarding-header {
margin-bottom: var(--space-5);
}
.onboarding-header h2 {
font-size: 1.2em;
font-weight: 600;
color: var(--text-primary);
margin: 0 0 var(--space-1) 0;
}
.subtitle {
font-size: 0.88em;
color: var(--text-muted);
margin: 0;
}
/* Progress */
.progress-section {
margin-bottom: var(--space-5);
}
.progress-label {
display: block;
font-size: 0.82em;
font-weight: 500;
color: var(--text-secondary);
margin-bottom: var(--space-2);
}
.progress-track {
height: 6px;
background: var(--bg-tertiary);
border-radius: 3px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--accent-green);
border-radius: 3px;
transition: width 0.3s ease;
}
/* Steps */
.step-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.step {
display: flex;
align-items: flex-start;
gap: var(--space-3);
padding: var(--space-3) var(--space-3);
border-radius: var(--radius);
transition: background 0.1s;
}
.step:hover {
background: var(--bg-tertiary);
}
.step-icon {
flex-shrink: 0;
width: 20px;
height: 20px;
margin-top: 1px;
}
.check-icon {
color: var(--accent-green);
}
.empty-icon {
color: var(--text-muted);
}
.step-body {
display: flex;
flex-direction: column;
gap: var(--space-1);
min-width: 0;
}
.step-title {
font-size: 0.92em;
font-weight: 500;
color: var(--text-primary);
text-decoration: none;
}
.step-title:hover {
color: var(--accent-blue);
text-decoration: underline;
}
.done .step-title {
color: var(--text-muted);
}
.step-hint {
font-size: 0.8em;
color: var(--text-muted);
line-height: 1.5;
}
.step-hint code {
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 3px;
padding: 1px 5px;
font-size: 0.92em;
word-break: break-all;
}
/* Footer */
.onboarding-footer {
margin-top: var(--space-5);
padding-top: var(--space-4);
border-top: 1px solid var(--border);
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.footer-link {
font-size: 0.85em;
color: var(--accent-blue);
text-decoration: none;
}
.footer-link:hover {
text-decoration: underline;
}
.footer-muted {
font-size: 0.78em;
color: var(--text-muted);
}
</style>
+2 -29
View File
@@ -4,6 +4,7 @@
import { api } from '$lib/api/client';
import { workspaceStore } from '$lib/stores/workspace.svelte';
import { relativeTime } from '$lib/utils/markdown';
import OnboardingChecklist from '$lib/components/OnboardingChecklist.svelte';
import type { DashboardResponse, Collection } from '$lib/types';
let wsSlug = $derived(page.params.workspace ?? '');
@@ -78,15 +79,7 @@
</header>
{#if totalItems === 0}
<div class="welcome-box">
<div class="welcome-icon">🚀</div>
<h2>Welcome to {workspaceStore.current?.name ?? 'your workspace'}</h2>
<p>Your project is ready. Start by creating items or use <kbd>/pad</kbd> in Claude Code to manage your project conversationally.</p>
<div class="welcome-actions">
<a href="/{wsSlug}/new" class="welcome-btn primary">Create your first item</a>
<a href="/{wsSlug}/tasks" class="welcome-btn secondary">Browse collections</a>
</div>
</div>
<OnboardingChecklist {wsSlug} byCollection={dashboard.summary.by_collection} />
{/if}
<!-- Collection Summary -->
@@ -212,26 +205,6 @@
margin: 0 auto;
padding: var(--space-8) var(--space-6);
}
.welcome-box {
text-align: center;
padding: var(--space-10) var(--space-6);
}
.welcome-icon { font-size: 3em; margin-bottom: var(--space-4); }
.welcome-box h2 { font-size: 1.3em; font-weight: 600; margin: 0 0 var(--space-2) 0; }
.welcome-box p { font-size: 0.9em; color: var(--text-muted); margin: 0 0 var(--space-6) 0; max-width: 480px; margin-left: auto; margin-right: auto; }
.welcome-box kbd {
background: var(--bg-tertiary); border: 1px solid var(--border); border-radius: 3px;
padding: 1px 5px; font-size: 0.9em; font-family: inherit;
}
.welcome-actions { display: flex; gap: var(--space-3); justify-content: center; flex-wrap: wrap; }
.welcome-btn {
padding: var(--space-2) var(--space-5); border-radius: var(--radius); font-weight: 600;
font-size: 0.9em; text-decoration: none; transition: opacity 0.1s;
}
.welcome-btn.primary { background: var(--accent-blue); color: #fff; }
.welcome-btn.secondary { background: var(--bg-tertiary); color: var(--text-primary); border: 1px solid var(--border); }
.welcome-btn:hover { opacity: 0.85; }
.loading {
text-align: center;
padding-top: 20vh;
@@ -236,6 +236,18 @@
return counts;
});
const emptyHintMap: Record<string, string> = {
tasks: '/pad break down my current work into tasks',
ideas: "/pad I have an idea for...",
phases: '/pad create a phase for what I\'m working on',
docs: '/pad document the architecture of this project',
conventions: '/pad what conventions should this project follow?',
playbooks: '/pad set up playbooks for our workflow',
bugs: '/pad triage open issues in this project',
};
let emptyHint = $derived(emptyHintMap[collSlug] ?? null);
function singularName(): string {
if (!collection) return 'item';
const name = collection.name;
@@ -447,6 +459,9 @@
<h2>No {collection.name.toLowerCase()} yet</h2>
<p>Create your first {singularName().toLowerCase()} to get started.</p>
<a href="/{wsSlug}/{collSlug}/new" class="empty-cta">+ Create {singularName()}</a>
{#if emptyHint}
<p class="empty-hint">Or try: <code>{emptyHint}</code></p>
{/if}
</div>
{:else if filteredItems.length === 0 && (searchQuery || Object.keys(activeFilters).length > 0)}
<div class="empty-state-box">
@@ -524,6 +539,18 @@
color: var(--text-muted);
margin: 0 0 var(--space-5) 0;
}
.empty-hint {
font-size: 0.82em !important;
color: var(--text-muted) !important;
margin-top: var(--space-3) !important;
}
.empty-hint code {
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 3px;
padding: 1px 5px;
font-size: 0.95em;
}
.empty-cta {
display: inline-block;
background: var(--accent-blue);