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
+32 -1
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{
+53 -9
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
}
+34 -17
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])))
}
}
+4 -1
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{
+36 -14
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 {
+36 -1
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{