Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 37 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,25 @@ Your AI tools start every session from zero. They don't know your stack, your pa

**TaskWing fixes this.** One command extracts your architecture into a local database. Every AI session after that just *knows*.

## Why TaskWing?

Your AI assistant reads the same files every session. TaskWing remembers so it doesn't have to.

```
Without TaskWing With TaskWing
───────────────── ─────────────
8–12 file reads 1 MCP query
~25,000 tokens ~1,500 tokens
2–3 minutes 42 seconds
Zero persistent context 170+ knowledge nodes
```

**Real session, real numbers** — asked *"What are the bottlenecks in our engineering process?"*:
- **Without TaskWing:** 8 Glob/Grep searches, 12 file reads, 25,000 tokens, 3 minutes
- **With TaskWing MCP:** 1 query, 1,500 tokens, 42 seconds — synthesized answer with code references

That's **90% fewer tokens** and **75% faster** time-to-answer.

## What It Does

| Capability | Description |
Expand All @@ -59,24 +78,6 @@ curl -fsSL https://taskwing.app/install.sh | sh

No signup. No account. Works offline. Everything stays local in SQLite.

## Quick Start

```bash
# 1. Extract your architecture
cd your-project
taskwing bootstrap
# → 22 decisions, 12 patterns, 9 constraints extracted

# 2. Set a goal and generate a plan
taskwing goal "Add Stripe billing"
# → Plan decomposed into 5 executable tasks

# 3. Execute with your AI assistant
/tw-next # Get next task with full context
# ...work...
/tw-done # Mark complete, advance to next
```

## Supported Models

<!-- TASKWING_PROVIDERS_START -->
Expand All @@ -102,6 +103,24 @@ taskwing goal "Add Stripe billing"
Brand names and logos are trademarks of their respective owners; usage here indicates compatibility, not endorsement.
<!-- TASKWING_LEGAL_END -->

## Quick Start

```bash
# 1. Extract your architecture
cd your-project
taskwing bootstrap
# → 22 decisions, 12 patterns, 9 constraints extracted

# 2. Set a goal and generate a plan
taskwing goal "Add Stripe billing"
# → Plan decomposed into 5 executable tasks

# 3. Execute with your AI assistant
/tw-next # Get next task with full context
# ...work...
/tw-done # Mark complete, advance to next
```

## MCP Tools

<!-- TASKWING_MCP_TOOLS_START -->
Expand Down
8 changes: 4 additions & 4 deletions internal/ui/config_menu.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,16 +194,16 @@ func (m configMenuModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var (
configTitleStyle = lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color("39"))
Foreground(lipgloss.AdaptiveColor{Light: "25", Dark: "39"})

configActiveStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("86"))
Foreground(lipgloss.AdaptiveColor{Light: "30", Dark: "86"})

configDimStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("240"))
Foreground(ColorDim)

configValueStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("229"))
Foreground(lipgloss.AdaptiveColor{Light: "136", Dark: "229"})
)

func (m configMenuModel) View() string {
Expand Down
36 changes: 18 additions & 18 deletions internal/ui/context_view.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ func RenderContextResultsWithSymbolsVerbose(query string, scored []knowledge.Sco
func renderContextInternal(query string, scored []knowledge.ScoredNode, answer string, verbose bool) {
// Styles
var (
titleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("205")).Bold(true)
sectionStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("141")).Bold(true)
titleStyle = lipgloss.NewStyle().Foreground(ColorPrimary).Bold(true)
sectionStyle = lipgloss.NewStyle().Foreground(ColorPurple).Bold(true)
)

// Render Answer Panel
Expand Down Expand Up @@ -79,11 +79,11 @@ func renderContextInternal(query string, scored []knowledge.ScoredNode, answer s
func renderScoredNodePanel(index int, s knowledge.ScoredNode, maxScore float32, verbose bool) {
// Styles
var (
headerStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("14")).Bold(true) // Cyan for headers
metaStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241")) // Dim for metadata
contentStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("252")) // Light for content
barFull = lipgloss.NewStyle().Foreground(lipgloss.Color("42")) // Green
barEmpty = lipgloss.NewStyle().Foreground(lipgloss.Color("237")) // Dark gray
headerStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "6", Dark: "14"}).Bold(true)
metaStyle = lipgloss.NewStyle().Foreground(ColorSecondary)
contentStyle = lipgloss.NewStyle().Foreground(ColorText)
barFull = lipgloss.NewStyle().Foreground(ColorSuccess)
barEmpty = lipgloss.NewStyle().Foreground(ColorBarEmpty)
panelBorder = lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).BorderForeground(scoreToColor(s.Score, maxScore)).Padding(0, 1).MarginTop(1)
)

Expand Down Expand Up @@ -167,24 +167,24 @@ func renderScoredNodePanel(index int, s knowledge.ScoredNode, maxScore float32,
}

// scoreToColor returns a border color based on the score (green for high, yellow for medium, gray for low).
func scoreToColor(score, maxScore float32) lipgloss.Color {
func scoreToColor(score, maxScore float32) lipgloss.TerminalColor {
relative := score / maxScore
switch {
case relative >= 0.8:
return lipgloss.Color("42") // Green - high relevance
return ColorSuccess
case relative >= 0.5:
return lipgloss.Color("214") // Orange - medium relevance
return ColorWarning
default:
return lipgloss.Color("241") // Gray - lower relevance
return ColorSecondary
}
}

// renderContextWithSymbolsInternal displays knowledge results and code symbols.
func renderContextWithSymbolsInternal(query string, scored []knowledge.ScoredNode, symbols []app.SymbolResponse, answer string, verbose bool) {
// Styles
var (
titleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("205")).Bold(true)
sectionStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("141")).Bold(true)
titleStyle = lipgloss.NewStyle().Foreground(ColorPrimary).Bold(true)
sectionStyle = lipgloss.NewStyle().Foreground(ColorPurple).Bold(true)
)

// Render Answer Panel
Expand Down Expand Up @@ -233,10 +233,10 @@ func renderContextWithSymbolsInternal(query string, scored []knowledge.ScoredNod
func renderSymbolPanel(index int, sym app.SymbolResponse, verbose bool) {
// Styles
var (
headerStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("14")).Bold(true)
metaStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241"))
locationStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("39"))
panelBorder = lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).BorderForeground(lipgloss.Color("63")).Padding(0, 1).MarginTop(1)
headerStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "6", Dark: "14"}).Bold(true)
metaStyle = lipgloss.NewStyle().Foreground(ColorSecondary)
locationStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "25", Dark: "39"})
panelBorder = lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).BorderForeground(lipgloss.AdaptiveColor{Light: "55", Dark: "63"}).Padding(0, 1).MarginTop(1)
)

icon := symbolKindIcon(sym.Kind)
Expand Down Expand Up @@ -308,7 +308,7 @@ func getContentWithoutSummary(content, summary string) string {
// RenderAskResult displays a complete AskResult from the ask pipeline.
// This is the primary rendering function for the `taskwing ask` command.
func RenderAskResult(result *app.AskResult, verbose bool) {
sectionStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("141")).Bold(true)
sectionStyle := lipgloss.NewStyle().Foreground(ColorPurple).Bold(true)

// Header with query in a styled box
headerBox := lipgloss.NewStyle().
Expand Down
2 changes: 1 addition & 1 deletion internal/ui/eval_report.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ var (
Foreground(ColorSuccess)

scoreBarEmpty = lipgloss.NewStyle().
Foreground(lipgloss.Color("237"))
Foreground(ColorBarEmpty)

sectionStyle = lipgloss.NewStyle().
Foreground(ColorPrimary).
Expand Down
7 changes: 4 additions & 3 deletions internal/ui/explain.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"strings"

"github.com/charmbracelet/lipgloss"
"github.com/josephgoksu/TaskWing/internal/app"
)

Expand Down Expand Up @@ -161,8 +162,8 @@ func truncate(s string, max int) string {
return s[:max-3] + "..."
}

// StyleBold returns the text with bold ANSI codes.
// This is a simple implementation - could use lipgloss for more styling.
// StyleBold returns the text with bold styling via lipgloss.
// Respects NO_COLOR automatically.
func StyleBold(s string) string {
return "\033[1m" + s + "\033[0m"
return lipgloss.NewStyle().Bold(true).Render(s)
}
4 changes: 2 additions & 2 deletions internal/ui/prompt_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ func (m apiKeyModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}

func (m apiKeyModel) View() string {
titleStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("12"))
dimStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
titleStyle := lipgloss.NewStyle().Bold(true).Foreground(ColorHighlight)
dimStyle := lipgloss.NewStyle().Foreground(ColorDim)

s := "\n" + titleStyle.Render("🔑 API Key required") + "\n"
s += dimStyle.Render("It will be stored locally in ~/.taskwing/config.yaml") + "\n\n"
Expand Down
54 changes: 28 additions & 26 deletions internal/ui/styles.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,23 @@ import (
)

var (
// Colors
ColorPrimary = lipgloss.Color("205") // Pink
ColorSecondary = lipgloss.Color("241") // Gray
ColorSuccess = lipgloss.Color("42") // Green
ColorError = lipgloss.Color("160") // Red
ColorWarning = lipgloss.Color("214") // Orange/Yellow
ColorText = lipgloss.Color("252") // White/Gray
ColorCyan = lipgloss.Color("87") // Cyan for strategy
ColorBlue = lipgloss.Color("75") // Blue for answers
ColorHighlight = lipgloss.Color("12") // Blue for titles/highlights
ColorSelected = lipgloss.Color("10") // Green for selected items
ColorDim = lipgloss.Color("240") // Dim gray for secondary text
ColorYellow = lipgloss.Color("11") // Yellow for badges/accents
// Colors — AdaptiveColor auto-selects Light/Dark based on terminal background
ColorPrimary = lipgloss.AdaptiveColor{Light: "161", Dark: "205"} // Pink
ColorSecondary = lipgloss.AdaptiveColor{Light: "244", Dark: "241"} // Gray
ColorSuccess = lipgloss.AdaptiveColor{Light: "28", Dark: "42"} // Green
ColorError = lipgloss.AdaptiveColor{Light: "160", Dark: "160"} // Red
ColorWarning = lipgloss.AdaptiveColor{Light: "172", Dark: "214"} // Orange/Yellow
ColorText = lipgloss.AdaptiveColor{Light: "235", Dark: "252"} // Text
ColorCyan = lipgloss.AdaptiveColor{Light: "30", Dark: "87"} // Cyan for strategy
ColorBlue = lipgloss.AdaptiveColor{Light: "27", Dark: "75"} // Blue for answers
ColorHighlight = lipgloss.AdaptiveColor{Light: "4", Dark: "12"} // Blue for titles/highlights
ColorSelected = lipgloss.AdaptiveColor{Light: "2", Dark: "10"} // Green for selected items
ColorDim = lipgloss.AdaptiveColor{Light: "247", Dark: "240"} // Dim gray for secondary text
ColorYellow = lipgloss.AdaptiveColor{Light: "136", Dark: "11"} // Yellow for badges/accents

// Shared constants used across multiple views
ColorPurple = lipgloss.AdaptiveColor{Light: "97", Dark: "141"} // Purple for sections
ColorBarEmpty = lipgloss.AdaptiveColor{Light: "250", Dark: "237"} // Empty bar segments

// Base Styles
StyleTitle = lipgloss.NewStyle().Foreground(ColorText).Bold(true)
Expand Down Expand Up @@ -84,8 +88,6 @@ var (
StyleSelectBadge = lipgloss.NewStyle().Foreground(ColorYellow).Bold(true)

// Table Styles (alternating rows)
ColorTableRowEven = lipgloss.Color("236") // Subtle dark background
ColorTableRowOdd = lipgloss.Color("234") // Slightly darker
StyleTableRowEven = lipgloss.NewStyle().Foreground(ColorText)
StyleTableRowOdd = lipgloss.NewStyle().Foreground(ColorDim)
StyleTableHeader = lipgloss.NewStyle().Bold(true).Foreground(ColorPrimary).Underline(true)
Expand All @@ -106,24 +108,24 @@ var (

// CategoryBadge returns a styled badge string for a knowledge node type.
func CategoryBadge(nodeType string) string {
colors := map[string]lipgloss.Color{
"decision": lipgloss.Color("205"), // Pink
"feature": lipgloss.Color("75"), // Blue
"constraint": lipgloss.Color("214"), // Orange
"pattern": lipgloss.Color("141"), // Purple
"plan": lipgloss.Color("42"), // Green
"note": lipgloss.Color("252"), // White
"metadata": lipgloss.Color("87"), // Cyan
"documentation": lipgloss.Color("11"), // Yellow
colors := map[string]lipgloss.AdaptiveColor{
"decision": ColorPrimary,
"feature": ColorBlue,
"constraint": ColorWarning,
"pattern": ColorPurple,
"plan": ColorSuccess,
"note": lipgloss.AdaptiveColor{Light: "248", Dark: "252"},
"metadata": ColorCyan,
"documentation": ColorYellow,
}

color, ok := colors[nodeType]
if !ok {
color = lipgloss.Color("241")
color = lipgloss.AdaptiveColor{Light: "244", Dark: "241"}
}

badge := lipgloss.NewStyle().
Foreground(lipgloss.Color("0")).
Foreground(lipgloss.AdaptiveColor{Light: "255", Dark: "0"}).
Background(color).
Padding(0, 1).
Bold(true)
Expand Down
71 changes: 71 additions & 0 deletions internal/ui/table.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package ui

import (
"os"
"strings"

"github.com/charmbracelet/lipgloss"
"golang.org/x/term"
)

// Table renders data in a compact markdown-style table format.
Expand Down Expand Up @@ -41,6 +43,61 @@ func (t *Table) ColumnWidths() []int {
}
}

// Auto-constrain to terminal width when MaxWidth is not set
if t.MaxWidth == 0 {
termWidth := GetTerminalWidth()
// Account for leading space + column separators (2 chars between each column)
overhead := 1
if len(widths) > 1 {
overhead += (len(widths) - 1) * 2
}
available := termWidth - overhead
if available > 0 {
total := 0
for _, w := range widths {
total += w
}
if total > available {
// Proportionally shrink columns, but keep a minimum of 4 chars
ratio := float64(available) / float64(total)
for i := range widths {
newW := int(float64(widths[i]) * ratio)
if newW < 4 {
newW = 4
}
widths[i] = newW
}
// Post-clamp: if min-floor caused overflow, trim widest columns
for {
postTotal := 0
for _, w := range widths {
postTotal += w
}
excess := postTotal - available
if excess <= 0 {
break
}
// Find widest column and shrink it
maxIdx, maxW := 0, 0
for i, w := range widths {
if w > maxW {
maxIdx, maxW = i, w
}
}
// Don't shrink below minimum
if maxW <= 4 {
break
}
shrink := excess
if shrink > maxW-4 {
shrink = maxW - 4
}
widths[maxIdx] -= shrink
}
}
}
}

return widths
}

Expand Down Expand Up @@ -101,6 +158,20 @@ func padRight(s string, width int) string {
return s + strings.Repeat(" ", width-len(s))
}

// GetTerminalWidthFor returns the terminal width for the given file descriptor, defaulting to 80.
func GetTerminalWidthFor(f *os.File) int {
w, _, err := term.GetSize(int(f.Fd()))
if err != nil || w <= 0 {
return 80
}
return w
}

// GetTerminalWidth returns the current stdout terminal width, defaulting to 80.
func GetTerminalWidth() int {
return GetTerminalWidthFor(os.Stdout)
}

// TruncateID shortens an ID for display (first 6 chars).
func TruncateID(id string) string {
if len(id) > 6 {
Expand Down
Loading
Loading