feat(tui): add self-documenting help with ASCII diagrams and table helpers

TUI improvements for Christmas launch:
- Add phase transition table and lifecycle diagram to `gt molecule --help`
- Add swarm lifecycle diagram to `gt swarm --help`
- Add mail routing diagram to `gt mail --help`
- Add sling mechanics diagram to `gt sling --help`
- Create Lipgloss table helper (internal/style/table.go)
- Migrate mq_list to use styled tables with color-coded priorities
- Migrate molecule list to use styled tables
- Add fuzzy matching "did you mean" suggestions for polecat not found errors
- Add suggest package with Levenshtein distance implementation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-22 17:52:32 -08:00
parent 469ba5c488
commit df0495be32
9 changed files with 822 additions and 43 deletions

View File

@@ -43,7 +43,38 @@ var mailCmd = &cobra.Command{
Long: `Send and receive messages between agents.
The mail system allows Mayor, polecats, and the Refinery to communicate.
Messages are stored in beads as issues with type=message.`,
Messages are stored in beads as issues with type=message.
MAIL ROUTING:
┌─────────────────────────────────────────────────────┐
│ Town (.beads/) │
│ ┌─────────────────────────────────────────────┐ │
│ │ Mayor Inbox │ │
│ │ └── mayor/ │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ gastown/ (rig mailboxes) │ │
│ │ ├── witness ← gastown/witness │ │
│ │ ├── refinery ← gastown/refinery │ │
│ │ ├── Toast ← gastown/Toast │ │
│ │ └── crew/max ← gastown/crew/max │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
ADDRESS FORMATS:
mayor/ → Mayor inbox
<rig>/witness → Rig's Witness
<rig>/refinery → Rig's Refinery
<rig>/<polecat> → Polecat (e.g., gastown/Toast)
<rig>/crew/<name> → Crew worker (e.g., gastown/crew/max)
--human → Special: human overseer
COMMANDS:
inbox View your inbox
send Send a message
read Read a specific message
mark Mark messages read/unread`,
}
var mailSendCmd = &cobra.Command{

View File

@@ -30,7 +30,39 @@ var moleculeCmd = &cobra.Command{
Long: `Manage molecule workflow templates.
Molecules are composable workflow patterns stored as beads issues.
When instantiated on a parent issue, they create child beads forming a DAG.`,
When instantiated on a parent issue, they create child beads forming a DAG.
LIFECYCLE:
Proto (template)
▼ instantiate/bond
┌─────────────────┐
│ Mol (durable) │ ← tracked in .beads/
│ Wisp (ephemeral)│ ← tracked in .beads-wisp/
└────────┬────────┘
┌──────┴──────┐
▼ ▼
burn squash
(no record) (→ digest)
PHASE TRANSITIONS (for pluggable molecules):
┌─────────────┬─────────────┬─────────────┬─────────────────────┐
│ Phase │ Parallelism │ Blocks │ Purpose │
├─────────────┼─────────────┼─────────────┼─────────────────────┤
│ discovery │ full │ (nothing) │ Inventory, gather │
│ structural │ sequential │ discovery │ Big-picture review │
│ tactical │ parallel │ structural │ Detailed work │
│ synthesis │ single │ tactical │ Aggregate results │
└─────────────┴─────────────┴─────────────┴─────────────────────┘
COMMANDS:
catalog List available molecule protos
instantiate Create steps from a molecule template
progress Show execution progress of an instantiated molecule
status Show what's on an agent's hook
burn Discard molecule without creating a digest
squash Complete molecule and create a digest`,
}
var moleculeListCmd = &cobra.Command{
@@ -386,23 +418,35 @@ func runMoleculeList(cmd *cobra.Command, args []string) error {
return nil
}
// Create styled table
table := style.NewTable(
style.Column{Name: "ID", Width: 20},
style.Column{Name: "TITLE", Width: 35},
style.Column{Name: "STEPS", Width: 5, Align: style.AlignRight},
style.Column{Name: "SOURCE", Width: 10},
)
for _, mol := range entries {
sourceMarker := style.Dim.Render(fmt.Sprintf("[%s]", mol.Source))
stepCount := ""
// Format steps count
stepStr := ""
if mol.StepCount > 0 {
stepCount = fmt.Sprintf(" (%d steps)", mol.StepCount)
stepStr = fmt.Sprintf("%d", mol.StepCount)
}
statusMarker := ""
// Format title with status
title := mol.Title
if mol.Status == "closed" {
statusMarker = " " + style.Dim.Render("[closed]")
title = style.Dim.Render(mol.Title + " [closed]")
}
fmt.Printf(" %s: %s%s%s %s\n",
style.Bold.Render(mol.ID), mol.Title, stepCount, statusMarker, sourceMarker)
// Format source
source := style.Dim.Render(mol.Source)
table.AddRow(mol.ID, title, stepStr, source)
}
fmt.Print(table.Render())
return nil
}

View File

@@ -104,12 +104,17 @@ func runMQList(cmd *cobra.Command, args []string) error {
return nil
}
// Print header
fmt.Printf(" %-12s %-12s %-8s %-30s %-10s %s\n",
"ID", "STATUS", "PRIORITY", "BRANCH", "WORKER", "AGE")
fmt.Printf(" %s\n", strings.Repeat("-", 90))
// Create styled table
table := style.NewTable(
style.Column{Name: "ID", Width: 12},
style.Column{Name: "STATUS", Width: 12},
style.Column{Name: "PRI", Width: 4},
style.Column{Name: "BRANCH", Width: 28},
style.Column{Name: "WORKER", Width: 10},
style.Column{Name: "AGE", Width: 6, Align: style.AlignRight},
)
// Print each MR
// Add rows
for _, issue := range filtered {
fields := beads.ParseMRFields(issue)
@@ -127,9 +132,9 @@ func runMQList(cmd *cobra.Command, args []string) error {
styledStatus := displayStatus
switch displayStatus {
case "ready":
styledStatus = style.Bold.Render("ready")
styledStatus = style.Success.Render("ready")
case "in_progress":
styledStatus = style.Bold.Render("in_progress")
styledStatus = style.Warning.Render("active")
case "blocked":
styledStatus = style.Dim.Render("blocked")
case "closed":
@@ -144,13 +149,13 @@ func runMQList(cmd *cobra.Command, args []string) error {
worker = fields.Worker
}
// Truncate branch if too long
if len(branch) > 30 {
branch = branch[:27] + "..."
}
// Format priority
// Format priority with color
priority := fmt.Sprintf("P%d", issue.Priority)
if issue.Priority <= 1 {
priority = style.Error.Render(priority)
} else if issue.Priority == 2 {
priority = style.Warning.Render(priority)
}
// Calculate age
age := formatMRAge(issue.CreatedAt)
@@ -161,12 +166,24 @@ func runMQList(cmd *cobra.Command, args []string) error {
displayID = displayID[:12]
}
fmt.Printf(" %-12s %-12s %-8s %-30s %-10s %s\n",
displayID, styledStatus, priority, branch, worker, style.Dim.Render(age))
table.AddRow(displayID, styledStatus, priority, branch, worker, style.Dim.Render(age))
}
// Show blocking info if blocked
fmt.Print(table.Render())
// Show blocking details below table
for _, issue := range filtered {
displayStatus := issue.Status
if issue.Status == "open" && (len(issue.BlockedBy) > 0 || issue.BlockedByCount > 0) {
displayStatus = "blocked"
}
if displayStatus == "blocked" && len(issue.BlockedBy) > 0 {
fmt.Printf(" %s\n", style.Dim.Render(fmt.Sprintf(" (waiting on %s)", issue.BlockedBy[0])))
displayID := issue.ID
if len(displayID) > 12 {
displayID = displayID[:12]
}
fmt.Printf(" %s %s\n", style.Dim.Render(displayID+":"),
style.Dim.Render(fmt.Sprintf("waiting on %s", issue.BlockedBy[0])))
}
}

View File

@@ -15,6 +15,7 @@ import (
"github.com/steveyegge/gastown/internal/rig"
"github.com/steveyegge/gastown/internal/session"
"github.com/steveyegge/gastown/internal/style"
"github.com/steveyegge/gastown/internal/suggest"
"github.com/steveyegge/gastown/internal/tmux"
"github.com/steveyegge/gastown/internal/workspace"
)
@@ -212,7 +213,9 @@ func runSessionStart(cmd *cobra.Command, args []string) error {
}
}
if !found {
return fmt.Errorf("polecat '%s' not found in rig '%s'", polecatName, rigName)
suggestions := suggest.FindSimilar(polecatName, r.Polecats, 3)
hint := fmt.Sprintf("Create with: gt polecat add %s/%s", rigName, polecatName)
return fmt.Errorf("%s", suggest.FormatSuggestion("Polecat", polecatName, suggestions, hint))
}
opts := session.StartOptions{

View File

@@ -19,6 +19,7 @@ import (
"github.com/steveyegge/gastown/internal/rig"
"github.com/steveyegge/gastown/internal/session"
"github.com/steveyegge/gastown/internal/style"
"github.com/steveyegge/gastown/internal/suggest"
"github.com/steveyegge/gastown/internal/tmux"
"github.com/steveyegge/gastown/internal/workspace"
)
@@ -43,21 +44,39 @@ Based on the Universal Gas Town Propulsion Principle:
"If you find something on your hook, YOU RUN IT."
Arguments:
thing What to sling: proto name, issue ID, or epic ID
target Who to sling at: agent address (polecat/name, deacon/, etc.)
SLING MECHANICS:
┌─────────┐ ┌───────────────────────────────────────────┐
│ THING │─────▶│ SLING PIPELINE │
└─────────┘ │ │
proto │ 1. SPAWN Proto → Molecule instance │
issue │ 2. ASSIGN Molecule → Target agent │
epic │ 3. PIN Work → Agent's hook │
│ 4. IGNITE Session starts automatically │
│ │
│ ┌─────────────────────────────┐ │
│ │ 🪝 TARGET's HOOK │ │
│ │ └── [work lands here] │ │
│ └─────────────────────────────┘ │
└───────────────────────────────────────────┘
Agent runs the work!
THING TYPES:
proto Molecule template name (e.g., "feature", "bugfix")
issue Beads issue ID (e.g., "gt-abc123")
epic Epic ID for batch dispatch
TARGET FORMATS:
gastown/Toast → Polecat in rig
gastown/witness → Rig's Witness
gastown/refinery → Rig's Refinery
deacon/ → Global Deacon
Examples:
gt sling feature polecat/alpha # Spawn feature mol, sling to alpha
gt sling gt-xyz polecat/beta -m bugfix # Sling issue with bugfix workflow
gt sling patrol deacon/ --wisp # Ephemeral patrol wisp
gt sling gt-epic-batch refinery/ # Batch work to refinery
What Happens When You Sling:
1. SPAWN (if proto) - Create molecule from template
2. ASSIGN - Assign molecule/issue to target agent
3. PIN - Put work on agent's hook (pinned bead)
4. IGNITION - Agent wakes and runs the work`,
gt sling feature gastown/Toast # Spawn feature, sling to Toast
gt sling gt-abc gastown/Nux -m bugfix # Issue with workflow
gt sling patrol deacon/ --wisp # Ephemeral patrol wisp`,
Args: cobra.ExactArgs(2),
RunE: runSling,
}
@@ -361,7 +380,10 @@ func slingToPolecat(townRoot string, target *SlingTarget, thing *SlingThing) err
fmt.Printf("%s Fresh worktree created\n", style.Bold.Render("✓"))
} else if err == polecat.ErrPolecatNotFound {
if !slingCreate {
return fmt.Errorf("polecat '%s' not found (use --create to create)", polecatName)
suggestions := suggest.FindSimilar(polecatName, r.Polecats, 3)
hint := fmt.Sprintf("Or use --create to create: gt sling %s %s/%s --create",
thing.ID, target.Rig, polecatName)
return fmt.Errorf("%s", suggest.FormatSuggestion("Polecat", polecatName, suggestions, hint))
}
fmt.Printf("Creating polecat %s...\n", polecatName)
if _, err = polecatMgr.Add(polecatName); err != nil {

View File

@@ -39,7 +39,42 @@ var swarmCmd = &cobra.Command{
Long: `Manage coordinated multi-agent work units (swarms).
A swarm coordinates multiple polecats working on related tasks from a shared
base commit. Work is merged to an integration branch, then landed to main.`,
base commit. Work is merged to an integration branch, then landed to main.
SWARM LIFECYCLE:
epic (tasks)
┌────────────────────────────────────────────┐
│ SWARM │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Polecat │ │ Polecat │ │ Polecat │ │
│ │ Toast │ │ Nux │ │ Capable │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ integration/<epic> │ │
│ └───────────────────┬──────────────────┘ │
└──────────────────────┼────────────────────┘
▼ land
main
STATES:
creating → Swarm being set up
active → Workers executing tasks
merging → Work being integrated
landed → Successfully merged to main
cancelled → Swarm aborted
COMMANDS:
create Create a new swarm from an epic
status Show swarm progress
list List swarms in a rig
land Manually land completed swarm
cancel Cancel an active swarm
dispatch Assign next ready task to a worker`,
}
var swarmCreateCmd = &cobra.Command{