@cyperx/clawforge 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +312 -0
- package/VERSION +1 -0
- package/bin/attach.sh +98 -0
- package/bin/check-agents.sh +343 -0
- package/bin/clawforge +257 -0
- package/bin/clawforge-dashboard +0 -0
- package/bin/clean.sh +257 -0
- package/bin/config.sh +111 -0
- package/bin/conflicts.sh +224 -0
- package/bin/cost.sh +273 -0
- package/bin/dashboard.sh +557 -0
- package/bin/diff.sh +109 -0
- package/bin/doctor.sh +196 -0
- package/bin/eval.sh +217 -0
- package/bin/history.sh +91 -0
- package/bin/init.sh +182 -0
- package/bin/learn.sh +230 -0
- package/bin/logs.sh +126 -0
- package/bin/memory.sh +207 -0
- package/bin/merge-helper.sh +174 -0
- package/bin/multi-review.sh +215 -0
- package/bin/notify.sh +93 -0
- package/bin/on-complete.sh +149 -0
- package/bin/parse-cost.sh +205 -0
- package/bin/pr.sh +167 -0
- package/bin/resume.sh +183 -0
- package/bin/review-mode.sh +163 -0
- package/bin/review-pr.sh +145 -0
- package/bin/routing.sh +88 -0
- package/bin/scope-task.sh +169 -0
- package/bin/spawn-agent.sh +190 -0
- package/bin/sprint.sh +320 -0
- package/bin/steer.sh +107 -0
- package/bin/stop.sh +136 -0
- package/bin/summary.sh +182 -0
- package/bin/swarm.sh +525 -0
- package/bin/templates.sh +244 -0
- package/lib/common.sh +302 -0
- package/lib/templates/bugfix.json +6 -0
- package/lib/templates/migration.json +7 -0
- package/lib/templates/refactor.json +6 -0
- package/lib/templates/security-audit.json +5 -0
- package/lib/templates/test-coverage.json +6 -0
- package/package.json +31 -0
- package/registry/conflicts.jsonl +0 -0
- package/registry/costs.jsonl +0 -0
- package/tui/PRD.md +106 -0
- package/tui/agent.go +266 -0
- package/tui/animation.go +192 -0
- package/tui/dashboard.go +219 -0
- package/tui/filter.go +68 -0
- package/tui/go.mod +25 -0
- package/tui/go.sum +46 -0
- package/tui/keybindings.go +229 -0
- package/tui/main.go +61 -0
- package/tui/model.go +166 -0
- package/tui/steer.go +69 -0
- package/tui/styles.go +69 -0
package/tui/model.go
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"time"
|
|
5
|
+
|
|
6
|
+
tea "charm.land/bubbletea/v2"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// RefreshTickMsg triggers periodic data reload.
|
|
10
|
+
type RefreshTickMsg time.Time
|
|
11
|
+
|
|
12
|
+
const refreshInterval = 2 * time.Second
|
|
13
|
+
|
|
14
|
+
// Model is the top-level Bubble Tea model for the ClawForge TUI dashboard.
|
|
15
|
+
type Model struct {
|
|
16
|
+
agents []Agent
|
|
17
|
+
selected int
|
|
18
|
+
filter string
|
|
19
|
+
filterMode bool
|
|
20
|
+
viewMode string // all | running | finished
|
|
21
|
+
showHelp bool
|
|
22
|
+
animating bool
|
|
23
|
+
width int
|
|
24
|
+
height int
|
|
25
|
+
steerMode bool
|
|
26
|
+
showPreview bool
|
|
27
|
+
steerInput string
|
|
28
|
+
confirmStop bool
|
|
29
|
+
frame int
|
|
30
|
+
noAnim bool
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// filteredAgents returns the agents matching the current filter.
|
|
34
|
+
func (m Model) filteredAgents() []Agent {
|
|
35
|
+
agents := m.agents
|
|
36
|
+
switch m.viewMode {
|
|
37
|
+
case "running":
|
|
38
|
+
var tmp []Agent
|
|
39
|
+
for _, a := range agents {
|
|
40
|
+
if a.Status == "running" || a.Status == "spawned" {
|
|
41
|
+
tmp = append(tmp, a)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
agents = tmp
|
|
45
|
+
case "finished":
|
|
46
|
+
var tmp []Agent
|
|
47
|
+
for _, a := range agents {
|
|
48
|
+
if a.Status == "done" || a.Status == "archived" || a.Status == "failed" || a.Status == "cancelled" || a.Status == "timeout" {
|
|
49
|
+
tmp = append(tmp, a)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
agents = tmp
|
|
53
|
+
}
|
|
54
|
+
return FilterAgents(agents, m.filter)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// NewModel creates a new Model. If noAnim is true, the startup animation is skipped.
|
|
58
|
+
func NewModel(noAnim bool) Model {
|
|
59
|
+
return Model{
|
|
60
|
+
animating: !noAnim,
|
|
61
|
+
noAnim: noAnim,
|
|
62
|
+
viewMode: "all",
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Init starts the animation tick (or loads agents directly) and kicks off refresh.
|
|
67
|
+
func (m Model) Init() tea.Cmd {
|
|
68
|
+
if m.animating {
|
|
69
|
+
return animationTick()
|
|
70
|
+
}
|
|
71
|
+
// No animation: load agents immediately and start refresh cycle.
|
|
72
|
+
m.agents = LoadAgents()
|
|
73
|
+
return refreshTick()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// refreshTick returns a command that sends a RefreshTickMsg after refreshInterval.
|
|
77
|
+
func refreshTick() tea.Cmd {
|
|
78
|
+
return tea.Tick(refreshInterval, func(t time.Time) tea.Msg {
|
|
79
|
+
return RefreshTickMsg(t)
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Update handles all incoming messages and dispatches to the appropriate handler.
|
|
84
|
+
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
85
|
+
switch msg := msg.(type) {
|
|
86
|
+
case tea.WindowSizeMsg:
|
|
87
|
+
m.width = msg.Width
|
|
88
|
+
m.height = msg.Height
|
|
89
|
+
return m, nil
|
|
90
|
+
|
|
91
|
+
case AnimationTickMsg:
|
|
92
|
+
if m.animating {
|
|
93
|
+
m.frame++
|
|
94
|
+
if m.frame >= len(forgeFrames) {
|
|
95
|
+
m.animating = false
|
|
96
|
+
m.agents = LoadAgents()
|
|
97
|
+
return m, refreshTick()
|
|
98
|
+
}
|
|
99
|
+
return m, animationTick()
|
|
100
|
+
}
|
|
101
|
+
return m, nil
|
|
102
|
+
|
|
103
|
+
case AnimationDoneMsg:
|
|
104
|
+
// Animation was skipped via keypress — load agents and start refresh.
|
|
105
|
+
m.agents = LoadAgents()
|
|
106
|
+
return m, refreshTick()
|
|
107
|
+
|
|
108
|
+
case RefreshTickMsg:
|
|
109
|
+
if !m.animating {
|
|
110
|
+
m.agents = LoadAgents()
|
|
111
|
+
// Clamp selection after refresh.
|
|
112
|
+
filtered := m.filteredAgents()
|
|
113
|
+
if m.selected >= len(filtered) && len(filtered) > 0 {
|
|
114
|
+
m.selected = len(filtered) - 1
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return m, refreshTick()
|
|
118
|
+
|
|
119
|
+
case steerDoneMsg:
|
|
120
|
+
// Steer command finished — refresh data.
|
|
121
|
+
m.agents = LoadAgents()
|
|
122
|
+
return m, nil
|
|
123
|
+
|
|
124
|
+
case stopDoneMsg:
|
|
125
|
+
// Stop command finished — refresh data.
|
|
126
|
+
m.agents = LoadAgents()
|
|
127
|
+
return m, nil
|
|
128
|
+
|
|
129
|
+
case attachDoneMsg:
|
|
130
|
+
// Returned from tmux attach — refresh data.
|
|
131
|
+
m.agents = LoadAgents()
|
|
132
|
+
return m, nil
|
|
133
|
+
|
|
134
|
+
case nudgeDoneMsg:
|
|
135
|
+
// Nudge command finished — refresh data.
|
|
136
|
+
m.agents = LoadAgents()
|
|
137
|
+
return m, nil
|
|
138
|
+
|
|
139
|
+
case tea.KeyPressMsg:
|
|
140
|
+
if m.animating {
|
|
141
|
+
// Skip animation on any key press.
|
|
142
|
+
m.animating = false
|
|
143
|
+
m.frame = 0
|
|
144
|
+
return m, func() tea.Msg { return AnimationDoneMsg{} }
|
|
145
|
+
}
|
|
146
|
+
var cmd tea.Cmd
|
|
147
|
+
m, cmd = handleKeyPress(m, msg)
|
|
148
|
+
return m, cmd
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return m, nil
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// View renders either the animation or the dashboard based on current state.
|
|
155
|
+
func (m Model) View() tea.View {
|
|
156
|
+
var content string
|
|
157
|
+
if m.animating {
|
|
158
|
+
content = renderAnimation(m.frame, m.width, m.height)
|
|
159
|
+
} else {
|
|
160
|
+
content = renderDashboard(m)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
v := tea.NewView(content)
|
|
164
|
+
v.AltScreen = true
|
|
165
|
+
return v
|
|
166
|
+
}
|
package/tui/steer.go
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"os/exec"
|
|
6
|
+
|
|
7
|
+
tea "charm.land/bubbletea/v2"
|
|
8
|
+
"charm.land/lipgloss/v2"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// steerDoneMsg is sent when the steer command finishes.
|
|
12
|
+
type steerDoneMsg struct{ err error }
|
|
13
|
+
|
|
14
|
+
// handleSteerKey processes key input while in steer mode.
|
|
15
|
+
func handleSteerKey(m Model, key string) (Model, tea.Cmd) {
|
|
16
|
+
switch key {
|
|
17
|
+
case "esc":
|
|
18
|
+
m.steerMode = false
|
|
19
|
+
m.steerInput = ""
|
|
20
|
+
return m, nil
|
|
21
|
+
case "enter":
|
|
22
|
+
if m.steerInput != "" {
|
|
23
|
+
agents := m.filteredAgents()
|
|
24
|
+
if len(agents) > 0 {
|
|
25
|
+
agent := agents[m.selected]
|
|
26
|
+
msg := m.steerInput
|
|
27
|
+
m.steerMode = false
|
|
28
|
+
m.steerInput = ""
|
|
29
|
+
cmd := exec.Command("clawforge", "steer", agent.ID, msg)
|
|
30
|
+
return m, tea.ExecProcess(cmd, func(err error) tea.Msg {
|
|
31
|
+
return steerDoneMsg{err}
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
m.steerMode = false
|
|
36
|
+
m.steerInput = ""
|
|
37
|
+
return m, nil
|
|
38
|
+
case "backspace":
|
|
39
|
+
if len(m.steerInput) > 0 {
|
|
40
|
+
m.steerInput = m.steerInput[:len(m.steerInput)-1]
|
|
41
|
+
}
|
|
42
|
+
return m, nil
|
|
43
|
+
default:
|
|
44
|
+
// Only append printable single characters.
|
|
45
|
+
if len(key) == 1 {
|
|
46
|
+
m.steerInput += key
|
|
47
|
+
} else if key == "space" {
|
|
48
|
+
m.steerInput += " "
|
|
49
|
+
}
|
|
50
|
+
return m, nil
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// RenderSteerInput renders the steer prompt line at the bottom of the screen.
|
|
55
|
+
func RenderSteerInput(m Model) string {
|
|
56
|
+
agents := m.filteredAgents()
|
|
57
|
+
id := "?"
|
|
58
|
+
if len(agents) > 0 && m.selected < len(agents) {
|
|
59
|
+
id = agents[m.selected].ID
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
prompt := fmt.Sprintf("⚒️ Steer agent #%s: %s", id, m.steerInput)
|
|
63
|
+
cursor := "█"
|
|
64
|
+
|
|
65
|
+
return lipgloss.NewStyle().
|
|
66
|
+
Foreground(lipgloss.Color("#FF8C00")).
|
|
67
|
+
Bold(true).
|
|
68
|
+
Render(prompt + cursor)
|
|
69
|
+
}
|
package/tui/styles.go
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"charm.land/lipgloss/v2"
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
// Forge theme colors.
|
|
8
|
+
var (
|
|
9
|
+
colorPrimary = lipgloss.Color("#FF8C00") // dark orange / amber
|
|
10
|
+
colorSecondary = lipgloss.Color("#FFA500") // orange
|
|
11
|
+
colorAccent = lipgloss.Color("#FF6600") // red-orange
|
|
12
|
+
colorMuted = lipgloss.Color("#CC5500") // brown-orange
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
// Header style: bold + amber.
|
|
16
|
+
var headerStyle = lipgloss.NewStyle().
|
|
17
|
+
Bold(true).
|
|
18
|
+
Foreground(colorPrimary)
|
|
19
|
+
|
|
20
|
+
// Table header style: bold + secondary.
|
|
21
|
+
var tableHeaderStyle = lipgloss.NewStyle().
|
|
22
|
+
Bold(true).
|
|
23
|
+
Foreground(colorSecondary)
|
|
24
|
+
|
|
25
|
+
// Separator style: muted.
|
|
26
|
+
var separatorStyle = lipgloss.NewStyle().
|
|
27
|
+
Foreground(colorMuted)
|
|
28
|
+
|
|
29
|
+
// Selected row: reverse + amber foreground.
|
|
30
|
+
var selectedRowStyle = lipgloss.NewStyle().
|
|
31
|
+
Reverse(true).
|
|
32
|
+
Foreground(colorPrimary)
|
|
33
|
+
|
|
34
|
+
// Status bar: dim + muted.
|
|
35
|
+
var statusBarStyle = lipgloss.NewStyle().
|
|
36
|
+
Faint(true).
|
|
37
|
+
Foreground(colorMuted)
|
|
38
|
+
|
|
39
|
+
// Borders: rounded, amber.
|
|
40
|
+
var borderStyle = lipgloss.NewStyle().
|
|
41
|
+
Border(lipgloss.RoundedBorder()).
|
|
42
|
+
BorderForeground(colorPrimary)
|
|
43
|
+
|
|
44
|
+
// Mode badge style.
|
|
45
|
+
var modeBadgeStyle = lipgloss.NewStyle().
|
|
46
|
+
Bold(true).
|
|
47
|
+
Padding(0, 1).
|
|
48
|
+
Foreground(lipgloss.Color("#000000")).
|
|
49
|
+
Background(colorSecondary)
|
|
50
|
+
|
|
51
|
+
// Status indicator styles.
|
|
52
|
+
var (
|
|
53
|
+
statusRunningStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#00FF00"))
|
|
54
|
+
statusIdleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFF00"))
|
|
55
|
+
statusFailedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF0000"))
|
|
56
|
+
statusDoneStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#808080"))
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
// Help overlay style.
|
|
60
|
+
var helpOverlayStyle = lipgloss.NewStyle().
|
|
61
|
+
Border(lipgloss.RoundedBorder()).
|
|
62
|
+
BorderForeground(colorPrimary).
|
|
63
|
+
Padding(1, 2).
|
|
64
|
+
Foreground(colorSecondary)
|
|
65
|
+
|
|
66
|
+
// Animation frame style: centered, amber.
|
|
67
|
+
var animFrameStyle = lipgloss.NewStyle().
|
|
68
|
+
Foreground(colorAccent).
|
|
69
|
+
Bold(true)
|