@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/bin/dashboard.sh
ADDED
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# dashboard.sh — Live TUI dashboard with vim keybindings and forge animation
|
|
3
|
+
# Usage: clawforge dashboard [--no-anim]
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
7
|
+
source "${SCRIPT_DIR}/../lib/common.sh"
|
|
8
|
+
|
|
9
|
+
# ── Help ───────────────────────────────────────────────────────────────
|
|
10
|
+
usage() {
|
|
11
|
+
cat <<EOF
|
|
12
|
+
Usage: clawforge dashboard [options]
|
|
13
|
+
|
|
14
|
+
Live terminal UI for monitoring all ClawForge agents.
|
|
15
|
+
|
|
16
|
+
Options:
|
|
17
|
+
--no-anim Skip startup animation
|
|
18
|
+
--help Show this help
|
|
19
|
+
|
|
20
|
+
Keybindings:
|
|
21
|
+
j/k Navigate agent list
|
|
22
|
+
Enter Attach to selected agent's tmux session
|
|
23
|
+
s Steer selected agent (prompts for message)
|
|
24
|
+
x Stop selected agent
|
|
25
|
+
/ Filter agents
|
|
26
|
+
r Force refresh
|
|
27
|
+
? Show help overlay
|
|
28
|
+
q Quit dashboard
|
|
29
|
+
EOF
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# ── Parse args ─────────────────────────────────────────────────────────
|
|
33
|
+
SKIP_ANIM=false
|
|
34
|
+
while [[ $# -gt 0 ]]; do
|
|
35
|
+
case "$1" in
|
|
36
|
+
--no-anim) SKIP_ANIM=true; shift ;;
|
|
37
|
+
--help|-h) usage; exit 0 ;;
|
|
38
|
+
--*) log_error "Unknown option: $1"; usage; exit 1 ;;
|
|
39
|
+
*) shift ;;
|
|
40
|
+
esac
|
|
41
|
+
done
|
|
42
|
+
|
|
43
|
+
# ── Terminal setup ─────────────────────────────────────────────────────
|
|
44
|
+
COLS=$(tput cols 2>/dev/null || echo 80)
|
|
45
|
+
ROWS=$(tput lines 2>/dev/null || echo 24)
|
|
46
|
+
SELECTED=0
|
|
47
|
+
FILTER=""
|
|
48
|
+
SHOW_HELP=false
|
|
49
|
+
REFRESH_INTERVAL=2
|
|
50
|
+
|
|
51
|
+
# Amber/orange forge colors via tput
|
|
52
|
+
COLOR_RESET=$(tput sgr0 2>/dev/null || echo "")
|
|
53
|
+
COLOR_AMBER=$(tput setaf 208 2>/dev/null || tput setaf 3 2>/dev/null || echo "")
|
|
54
|
+
COLOR_ORANGE=$(tput setaf 166 2>/dev/null || tput setaf 1 2>/dev/null || echo "")
|
|
55
|
+
COLOR_DIM=$(tput dim 2>/dev/null || echo "")
|
|
56
|
+
COLOR_BOLD=$(tput bold 2>/dev/null || echo "")
|
|
57
|
+
COLOR_REV=$(tput rev 2>/dev/null || echo "")
|
|
58
|
+
COLOR_GREEN=$(tput setaf 2 2>/dev/null || echo "")
|
|
59
|
+
COLOR_RED=$(tput setaf 1 2>/dev/null || echo "")
|
|
60
|
+
COLOR_YELLOW=$(tput setaf 3 2>/dev/null || echo "")
|
|
61
|
+
COLOR_CYAN=$(tput setaf 6 2>/dev/null || echo "")
|
|
62
|
+
|
|
63
|
+
# ── Forge ASCII art frames ────────────────────────────────────────────
|
|
64
|
+
FORGE_FRAME_1() {
|
|
65
|
+
cat <<'ART'
|
|
66
|
+
╔═══════════════════════════════╗
|
|
67
|
+
║ ClawForge ║
|
|
68
|
+
╚═══════════════════════════════╝
|
|
69
|
+
|
|
70
|
+
_______________
|
|
71
|
+
/ \
|
|
72
|
+
/ ███████████ \
|
|
73
|
+
│ ███████████████ │
|
|
74
|
+
│ ████ ▓▓ █████│
|
|
75
|
+
│ ███████████████ │
|
|
76
|
+
\ ███████████ /
|
|
77
|
+
\_____ _____/
|
|
78
|
+
│ │
|
|
79
|
+
═════╧═══╧═════
|
|
80
|
+
/ ░░░░░░░░░░░░ \
|
|
81
|
+
/_________________ \
|
|
82
|
+
╔═══════════════════╗
|
|
83
|
+
║ ▒▒▒▒▒▒▒▒▒▒▒▒▒ ║
|
|
84
|
+
╚═══════════════════╝
|
|
85
|
+
ART
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
FORGE_FRAME_2() {
|
|
89
|
+
cat <<'ART'
|
|
90
|
+
╔═══════════════════════════════╗
|
|
91
|
+
║ ClawForge ⚒ ║
|
|
92
|
+
╚═══════════════════════════════╝
|
|
93
|
+
|
|
94
|
+
_______________
|
|
95
|
+
/ \
|
|
96
|
+
/ ▓▓▓▓▓▓▓▓▓▓▓ \
|
|
97
|
+
│ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓│
|
|
98
|
+
│ ▓▓▓▓ ██ ▓▓▓▓▓│
|
|
99
|
+
│ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓│
|
|
100
|
+
\ ▓▓▓▓▓▓▓▓▓▓▓ /
|
|
101
|
+
\_____ _____/
|
|
102
|
+
│ ⚡│
|
|
103
|
+
═════╧═══╧═════
|
|
104
|
+
/ ▓▓▓▓▓▓▓▓▓▓▓▓ \
|
|
105
|
+
/_________________ \
|
|
106
|
+
╔═══════════════════╗
|
|
107
|
+
║ ████████████████ ║
|
|
108
|
+
╚═══════════════════╝
|
|
109
|
+
ART
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
FORGE_FRAME_3() {
|
|
113
|
+
cat <<'ART'
|
|
114
|
+
╔═══════════════════════════════╗
|
|
115
|
+
║ ClawForge ⚒ ⚒ ║
|
|
116
|
+
╚═══════════════════════════════╝
|
|
117
|
+
* * *
|
|
118
|
+
_______________
|
|
119
|
+
/ * * * * \
|
|
120
|
+
/ ████████████ \
|
|
121
|
+
│ █████████████████│
|
|
122
|
+
│ █████ ░░ ██████│
|
|
123
|
+
│ █████████████████│
|
|
124
|
+
\ ████████████ /
|
|
125
|
+
\_____ _____/
|
|
126
|
+
│⚡⚡│
|
|
127
|
+
═════╧═══╧═════
|
|
128
|
+
/ ████████████████\
|
|
129
|
+
/_________________ \
|
|
130
|
+
╔═══════════════════╗
|
|
131
|
+
║ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ║
|
|
132
|
+
╚═══════════════════╝
|
|
133
|
+
ART
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
# ── Startup animation ─────────────────────────────────────────────────
|
|
137
|
+
_forge_animation() {
|
|
138
|
+
tput civis 2>/dev/null || true # hide cursor
|
|
139
|
+
local frames=("1" "2" "3" "2" "3" "1" "3" "2")
|
|
140
|
+
for frame in "${frames[@]}"; do
|
|
141
|
+
tput clear 2>/dev/null || clear
|
|
142
|
+
echo ""
|
|
143
|
+
echo "${COLOR_AMBER}"
|
|
144
|
+
case "$frame" in
|
|
145
|
+
1) FORGE_FRAME_1 ;;
|
|
146
|
+
2) FORGE_FRAME_2 ;;
|
|
147
|
+
3) FORGE_FRAME_3 ;;
|
|
148
|
+
esac
|
|
149
|
+
echo "${COLOR_RESET}"
|
|
150
|
+
sleep 0.2
|
|
151
|
+
done
|
|
152
|
+
# Final flash
|
|
153
|
+
tput clear 2>/dev/null || clear
|
|
154
|
+
echo ""
|
|
155
|
+
echo "${COLOR_ORANGE}${COLOR_BOLD}"
|
|
156
|
+
FORGE_FRAME_3
|
|
157
|
+
echo "${COLOR_RESET}"
|
|
158
|
+
sleep 0.4
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
# ── Data fetching ──────────────────────────────────────────────────────
|
|
162
|
+
TASK_DATA="[]"
|
|
163
|
+
TASK_COUNT=0
|
|
164
|
+
|
|
165
|
+
_refresh_data() {
|
|
166
|
+
_ensure_registry
|
|
167
|
+
TASK_DATA=$(jq '.tasks' "$REGISTRY_FILE" 2>/dev/null || echo "[]")
|
|
168
|
+
|
|
169
|
+
# Apply filter
|
|
170
|
+
if [[ -n "$FILTER" ]]; then
|
|
171
|
+
TASK_DATA=$(echo "$TASK_DATA" | jq --arg f "$FILTER" \
|
|
172
|
+
'[.[] | select(
|
|
173
|
+
(.id | ascii_downcase | contains($f | ascii_downcase)) or
|
|
174
|
+
(.description // "" | ascii_downcase | contains($f | ascii_downcase)) or
|
|
175
|
+
(.mode // "" | ascii_downcase | contains($f | ascii_downcase)) or
|
|
176
|
+
(.status // "" | ascii_downcase | contains($f | ascii_downcase))
|
|
177
|
+
)]' 2>/dev/null || echo "$TASK_DATA")
|
|
178
|
+
fi
|
|
179
|
+
|
|
180
|
+
TASK_COUNT=$(echo "$TASK_DATA" | jq 'length' 2>/dev/null || echo 0)
|
|
181
|
+
|
|
182
|
+
# Clamp selection
|
|
183
|
+
if [[ "$TASK_COUNT" -gt 0 ]]; then
|
|
184
|
+
if [[ "$SELECTED" -ge "$TASK_COUNT" ]]; then
|
|
185
|
+
SELECTED=$((TASK_COUNT - 1))
|
|
186
|
+
fi
|
|
187
|
+
[[ "$SELECTED" -lt 0 ]] && SELECTED=0
|
|
188
|
+
else
|
|
189
|
+
SELECTED=0
|
|
190
|
+
fi
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
# ── Cost data ──────────────────────────────────────────────────────────
|
|
194
|
+
_get_task_cost() {
|
|
195
|
+
local task_id="$1"
|
|
196
|
+
local costs_file="${CLAWFORGE_DIR}/registry/costs.jsonl"
|
|
197
|
+
if [[ -f "$costs_file" ]]; then
|
|
198
|
+
grep "\"taskId\":\"${task_id}\"" "$costs_file" 2>/dev/null | tail -1 | jq -r '.totalCost // 0' 2>/dev/null || echo "—"
|
|
199
|
+
else
|
|
200
|
+
echo "—"
|
|
201
|
+
fi
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
# ── Conflict data ──────────────────────────────────────────────────────
|
|
205
|
+
_get_conflict_count() {
|
|
206
|
+
local conflicts_file="${CLAWFORGE_DIR}/registry/conflicts.jsonl"
|
|
207
|
+
if [[ -f "$conflicts_file" ]]; then
|
|
208
|
+
wc -l < "$conflicts_file" 2>/dev/null | tr -d ' '
|
|
209
|
+
else
|
|
210
|
+
echo "0"
|
|
211
|
+
fi
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
# ── Render functions ───────────────────────────────────────────────────
|
|
215
|
+
_render_header() {
|
|
216
|
+
local ver
|
|
217
|
+
ver=$(cat "${CLAWFORGE_DIR}/VERSION" 2>/dev/null || echo "?")
|
|
218
|
+
echo "${COLOR_AMBER}${COLOR_BOLD}╔══════════════════════════════════════════════════════════════════════════╗${COLOR_RESET}"
|
|
219
|
+
printf "${COLOR_AMBER}${COLOR_BOLD}║${COLOR_RESET} ${COLOR_ORANGE}${COLOR_BOLD}ClawForge Dashboard${COLOR_RESET} ${COLOR_DIM}v${ver}${COLOR_RESET}"
|
|
220
|
+
# Right-align timestamp
|
|
221
|
+
local ts
|
|
222
|
+
ts=$(date +"%H:%M:%S")
|
|
223
|
+
local pad=$((72 - 22 - ${#ver} - ${#ts}))
|
|
224
|
+
printf "%*s" "$pad" ""
|
|
225
|
+
printf "${COLOR_DIM}${ts}${COLOR_RESET} ${COLOR_AMBER}${COLOR_BOLD}║${COLOR_RESET}\n"
|
|
226
|
+
echo "${COLOR_AMBER}${COLOR_BOLD}╚══════════════════════════════════════════════════════════════════════════╝${COLOR_RESET}"
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
_render_status_bar() {
|
|
230
|
+
local running=0 spawned=0 done_count=0 failed=0
|
|
231
|
+
running=$(echo "$TASK_DATA" | jq '[.[] | select(.status == "running")] | length' 2>/dev/null || echo 0)
|
|
232
|
+
spawned=$(echo "$TASK_DATA" | jq '[.[] | select(.status == "spawned")] | length' 2>/dev/null || echo 0)
|
|
233
|
+
done_count=$(echo "$TASK_DATA" | jq '[.[] | select(.status == "done")] | length' 2>/dev/null || echo 0)
|
|
234
|
+
failed=$(echo "$TASK_DATA" | jq '[.[] | select(.status == "failed")] | length' 2>/dev/null || echo 0)
|
|
235
|
+
local conflicts
|
|
236
|
+
conflicts=$(_get_conflict_count)
|
|
237
|
+
|
|
238
|
+
printf " ${COLOR_GREEN}●${COLOR_RESET} Running: ${running} "
|
|
239
|
+
printf "${COLOR_YELLOW}○${COLOR_RESET} Spawned: ${spawned} "
|
|
240
|
+
printf "${COLOR_CYAN}✓${COLOR_RESET} Done: ${done_count} "
|
|
241
|
+
printf "${COLOR_RED}✗${COLOR_RESET} Failed: ${failed}"
|
|
242
|
+
if [[ "$conflicts" -gt 0 ]]; then
|
|
243
|
+
printf " ${COLOR_RED}${COLOR_BOLD}⚠ Conflicts: ${conflicts}${COLOR_RESET}"
|
|
244
|
+
fi
|
|
245
|
+
if [[ -n "$FILTER" ]]; then
|
|
246
|
+
printf " ${COLOR_DIM}Filter: ${FILTER}${COLOR_RESET}"
|
|
247
|
+
fi
|
|
248
|
+
echo ""
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
_status_color() {
|
|
252
|
+
case "$1" in
|
|
253
|
+
running) echo "${COLOR_GREEN}" ;;
|
|
254
|
+
spawned) echo "${COLOR_YELLOW}" ;;
|
|
255
|
+
pr-created) echo "${COLOR_CYAN}" ;;
|
|
256
|
+
ci-passing) echo "${COLOR_GREEN}" ;;
|
|
257
|
+
done) echo "${COLOR_DIM}" ;;
|
|
258
|
+
failed) echo "${COLOR_RED}" ;;
|
|
259
|
+
stopped) echo "${COLOR_RED}" ;;
|
|
260
|
+
*) echo "" ;;
|
|
261
|
+
esac
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
_render_table() {
|
|
265
|
+
echo ""
|
|
266
|
+
# Table header
|
|
267
|
+
printf " ${COLOR_DIM}%-5s %-8s %-12s %-12s %-30s %-10s %-8s${COLOR_RESET}\n" \
|
|
268
|
+
"ID" "Mode" "Status" "Branch" "Description" "Cost" "CI"
|
|
269
|
+
printf " ${COLOR_DIM}%-5s %-8s %-12s %-12s %-30s %-10s %-8s${COLOR_RESET}\n" \
|
|
270
|
+
"─────" "────────" "────────────" "────────────" "──────────────────────────────" "──────────" "────────"
|
|
271
|
+
|
|
272
|
+
if [[ "$TASK_COUNT" -eq 0 ]]; then
|
|
273
|
+
echo ""
|
|
274
|
+
echo " ${COLOR_DIM}No active tasks.${COLOR_RESET}"
|
|
275
|
+
echo " ${COLOR_DIM}Start one: clawforge sprint \"<task>\"${COLOR_RESET}"
|
|
276
|
+
return
|
|
277
|
+
fi
|
|
278
|
+
|
|
279
|
+
local i=0
|
|
280
|
+
while IFS= read -r task_line; do
|
|
281
|
+
local id mode status branch desc cost ci_status
|
|
282
|
+
id=$(echo "$task_line" | jq -r 'if .short_id then "#\(.short_id)" else .id[0:8] end' 2>/dev/null)
|
|
283
|
+
mode=$(echo "$task_line" | jq -r '.mode // "—"' 2>/dev/null)
|
|
284
|
+
status=$(echo "$task_line" | jq -r '.status // "?"' 2>/dev/null)
|
|
285
|
+
branch=$(echo "$task_line" | jq -r '.branch // "" | split("/") | last | .[0:12]' 2>/dev/null)
|
|
286
|
+
desc=$(echo "$task_line" | jq -r '.description // "" | .[0:30]' 2>/dev/null)
|
|
287
|
+
local task_id
|
|
288
|
+
task_id=$(echo "$task_line" | jq -r '.id' 2>/dev/null)
|
|
289
|
+
cost=$(_get_task_cost "$task_id")
|
|
290
|
+
ci_status=$(echo "$task_line" | jq -r 'if .ci_retries and .ci_retries > 0 then "retry/\(.ci_retries)" else "—" end' 2>/dev/null)
|
|
291
|
+
|
|
292
|
+
local sc
|
|
293
|
+
sc=$(_status_color "$status")
|
|
294
|
+
if [[ "$i" -eq "$SELECTED" ]]; then
|
|
295
|
+
printf " ${COLOR_REV}${COLOR_BOLD}%-5s %-8s ${sc}%-12s${COLOR_RESET}${COLOR_REV} %-12s %-30s %-10s %-8s${COLOR_RESET}\n" \
|
|
296
|
+
"$id" "$mode" "$status" "$branch" "$desc" "$cost" "$ci_status"
|
|
297
|
+
else
|
|
298
|
+
printf " %-5s %-8s ${sc}%-12s${COLOR_RESET} %-12s %-30s %-10s %-8s\n" \
|
|
299
|
+
"$id" "$mode" "$status" "$branch" "$desc" "$cost" "$ci_status"
|
|
300
|
+
fi
|
|
301
|
+
((i++)) || true
|
|
302
|
+
done < <(echo "$TASK_DATA" | jq -c '.[]' 2>/dev/null)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
_render_system_info() {
|
|
306
|
+
echo ""
|
|
307
|
+
echo " ${COLOR_DIM}── System ──────────────────────────────────────────────────────────${COLOR_RESET}"
|
|
308
|
+
local tmux_count
|
|
309
|
+
tmux_count=$(tmux list-sessions 2>/dev/null | grep -c "agent" 2>/dev/null || echo "0")
|
|
310
|
+
local daemon_status="off"
|
|
311
|
+
local pid_file="${CLAWFORGE_DIR}/watch.pid"
|
|
312
|
+
if [[ -f "$pid_file" ]] && kill -0 "$(cat "$pid_file")" 2>/dev/null; then
|
|
313
|
+
daemon_status="${COLOR_GREEN}on${COLOR_RESET}"
|
|
314
|
+
fi
|
|
315
|
+
printf " tmux agents: %s │ watch daemon: %s │ tasks: %s\n" \
|
|
316
|
+
"${tmux_count:-0}" "$daemon_status" "$TASK_COUNT"
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
_render_help_overlay() {
|
|
320
|
+
echo ""
|
|
321
|
+
echo " ${COLOR_AMBER}${COLOR_BOLD}╔═══════════════════════════╗${COLOR_RESET}"
|
|
322
|
+
echo " ${COLOR_AMBER}${COLOR_BOLD}║ Dashboard Help ║${COLOR_RESET}"
|
|
323
|
+
echo " ${COLOR_AMBER}${COLOR_BOLD}╠═══════════════════════════╣${COLOR_RESET}"
|
|
324
|
+
echo " ${COLOR_AMBER}║${COLOR_RESET} j/k Navigate up/down ${COLOR_AMBER}║${COLOR_RESET}"
|
|
325
|
+
echo " ${COLOR_AMBER}║${COLOR_RESET} Enter Attach to agent ${COLOR_AMBER}║${COLOR_RESET}"
|
|
326
|
+
echo " ${COLOR_AMBER}║${COLOR_RESET} s Steer agent ${COLOR_AMBER}║${COLOR_RESET}"
|
|
327
|
+
echo " ${COLOR_AMBER}║${COLOR_RESET} x Stop agent ${COLOR_AMBER}║${COLOR_RESET}"
|
|
328
|
+
echo " ${COLOR_AMBER}║${COLOR_RESET} / Filter agents ${COLOR_AMBER}║${COLOR_RESET}"
|
|
329
|
+
echo " ${COLOR_AMBER}║${COLOR_RESET} r Force refresh ${COLOR_AMBER}║${COLOR_RESET}"
|
|
330
|
+
echo " ${COLOR_AMBER}║${COLOR_RESET} ? Toggle this help ${COLOR_AMBER}║${COLOR_RESET}"
|
|
331
|
+
echo " ${COLOR_AMBER}║${COLOR_RESET} q Quit ${COLOR_AMBER}║${COLOR_RESET}"
|
|
332
|
+
echo " ${COLOR_AMBER}${COLOR_BOLD}╚═══════════════════════════╝${COLOR_RESET}"
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
_render_footer() {
|
|
336
|
+
echo ""
|
|
337
|
+
echo " ${COLOR_DIM}[j/k] navigate [Enter] attach [s] steer [x] stop [/] filter [?] help [q] quit${COLOR_RESET}"
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
# ── Full render ────────────────────────────────────────────────────────
|
|
341
|
+
_render() {
|
|
342
|
+
tput cup 0 0 2>/dev/null || true
|
|
343
|
+
tput ed 2>/dev/null || true # clear to end of screen
|
|
344
|
+
_render_header
|
|
345
|
+
_render_status_bar
|
|
346
|
+
_render_table
|
|
347
|
+
if $SHOW_HELP; then
|
|
348
|
+
_render_help_overlay
|
|
349
|
+
fi
|
|
350
|
+
_render_system_info
|
|
351
|
+
_render_footer
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
# ── Selected task helpers ──────────────────────────────────────────────
|
|
355
|
+
_selected_task_id() {
|
|
356
|
+
if [[ "$TASK_COUNT" -eq 0 ]]; then
|
|
357
|
+
echo ""
|
|
358
|
+
return
|
|
359
|
+
fi
|
|
360
|
+
echo "$TASK_DATA" | jq -r ".[$SELECTED].id // empty" 2>/dev/null || echo ""
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
_selected_short_id() {
|
|
364
|
+
if [[ "$TASK_COUNT" -eq 0 ]]; then
|
|
365
|
+
echo ""
|
|
366
|
+
return
|
|
367
|
+
fi
|
|
368
|
+
echo "$TASK_DATA" | jq -r ".[$SELECTED].short_id // empty" 2>/dev/null || echo ""
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
_selected_tmux_session() {
|
|
372
|
+
if [[ "$TASK_COUNT" -eq 0 ]]; then
|
|
373
|
+
echo ""
|
|
374
|
+
return
|
|
375
|
+
fi
|
|
376
|
+
echo "$TASK_DATA" | jq -r ".[$SELECTED].tmuxSession // empty" 2>/dev/null || echo ""
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
# ── Action handlers ────────────────────────────────────────────────────
|
|
380
|
+
_action_attach() {
|
|
381
|
+
local session
|
|
382
|
+
session=$(_selected_tmux_session)
|
|
383
|
+
if [[ -n "$session" ]] && tmux has-session -t "$session" 2>/dev/null; then
|
|
384
|
+
# Restore terminal before attaching
|
|
385
|
+
tput cnorm 2>/dev/null || true
|
|
386
|
+
tput rmcup 2>/dev/null || true
|
|
387
|
+
stty echo 2>/dev/null || true
|
|
388
|
+
tmux attach-session -t "$session"
|
|
389
|
+
# Re-enter alt screen after detach
|
|
390
|
+
tput smcup 2>/dev/null || true
|
|
391
|
+
tput civis 2>/dev/null || true
|
|
392
|
+
stty -echo 2>/dev/null || true
|
|
393
|
+
fi
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
_action_steer() {
|
|
397
|
+
local sid
|
|
398
|
+
sid=$(_selected_short_id)
|
|
399
|
+
if [[ -z "$sid" ]]; then return; fi
|
|
400
|
+
|
|
401
|
+
# Restore terminal for input
|
|
402
|
+
tput cnorm 2>/dev/null || true
|
|
403
|
+
stty echo 2>/dev/null || true
|
|
404
|
+
tput cup $((ROWS - 2)) 0 2>/dev/null || true
|
|
405
|
+
tput el 2>/dev/null || true
|
|
406
|
+
printf " Steer #${sid}: "
|
|
407
|
+
local msg
|
|
408
|
+
read -r msg
|
|
409
|
+
stty -echo 2>/dev/null || true
|
|
410
|
+
tput civis 2>/dev/null || true
|
|
411
|
+
|
|
412
|
+
if [[ -n "$msg" ]]; then
|
|
413
|
+
"${SCRIPT_DIR}/steer.sh" "$sid" "$msg" 2>/dev/null || true
|
|
414
|
+
fi
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
_action_stop() {
|
|
418
|
+
local sid
|
|
419
|
+
sid=$(_selected_short_id)
|
|
420
|
+
if [[ -z "$sid" ]]; then return; fi
|
|
421
|
+
|
|
422
|
+
tput cnorm 2>/dev/null || true
|
|
423
|
+
stty echo 2>/dev/null || true
|
|
424
|
+
tput cup $((ROWS - 2)) 0 2>/dev/null || true
|
|
425
|
+
tput el 2>/dev/null || true
|
|
426
|
+
printf " Stop #${sid}? [y/N]: "
|
|
427
|
+
local confirm
|
|
428
|
+
read -r -n 1 confirm
|
|
429
|
+
stty -echo 2>/dev/null || true
|
|
430
|
+
tput civis 2>/dev/null || true
|
|
431
|
+
|
|
432
|
+
if [[ "$confirm" == "y" || "$confirm" == "Y" ]]; then
|
|
433
|
+
"${SCRIPT_DIR}/stop.sh" "$sid" --yes 2>/dev/null || true
|
|
434
|
+
_refresh_data
|
|
435
|
+
fi
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
_action_filter() {
|
|
439
|
+
tput cnorm 2>/dev/null || true
|
|
440
|
+
stty echo 2>/dev/null || true
|
|
441
|
+
tput cup $((ROWS - 2)) 0 2>/dev/null || true
|
|
442
|
+
tput el 2>/dev/null || true
|
|
443
|
+
printf " Filter: "
|
|
444
|
+
local f
|
|
445
|
+
read -r f
|
|
446
|
+
stty -echo 2>/dev/null || true
|
|
447
|
+
tput civis 2>/dev/null || true
|
|
448
|
+
FILTER="$f"
|
|
449
|
+
SELECTED=0
|
|
450
|
+
_refresh_data
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
# ── Static (non-interactive) dashboard ─────────────────────────────────
|
|
454
|
+
_static_dashboard() {
|
|
455
|
+
_refresh_data
|
|
456
|
+
echo "╔══════════════════════════════════════════════════════════════╗"
|
|
457
|
+
echo "║ ClawForge Dashboard ║"
|
|
458
|
+
echo "╚══════════════════════════════════════════════════════════════╝"
|
|
459
|
+
echo ""
|
|
460
|
+
echo "── Active Tasks ──────────────────────────────────────────────"
|
|
461
|
+
if [[ "$TASK_COUNT" -eq 0 ]]; then
|
|
462
|
+
echo " No active tasks."
|
|
463
|
+
else
|
|
464
|
+
echo "$TASK_DATA" | jq -r '.[] |
|
|
465
|
+
def sid: if .short_id then "#\(.short_id)" else .id end;
|
|
466
|
+
def mode: .mode // "—";
|
|
467
|
+
" \(sid) \(mode) [\(.status // "unknown")] \(.description // "no description")[0:55]"
|
|
468
|
+
' 2>/dev/null || echo " (error reading tasks)"
|
|
469
|
+
fi
|
|
470
|
+
echo ""
|
|
471
|
+
echo " Tip: Run in a terminal for interactive TUI with vim keybindings"
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
# ── Terminal cleanup ───────────────────────────────────────────────────
|
|
475
|
+
_cleanup() {
|
|
476
|
+
tput cnorm 2>/dev/null || true # show cursor
|
|
477
|
+
tput rmcup 2>/dev/null || true # exit alt screen
|
|
478
|
+
stty echo 2>/dev/null || true # restore echo
|
|
479
|
+
echo ""
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
# ── Main ───────────────────────────────────────────────────────────────
|
|
483
|
+
|
|
484
|
+
# Non-interactive: print static dashboard and exit
|
|
485
|
+
if [[ ! -t 1 ]]; then
|
|
486
|
+
_static_dashboard
|
|
487
|
+
exit 0
|
|
488
|
+
fi
|
|
489
|
+
|
|
490
|
+
trap _cleanup EXIT INT TERM
|
|
491
|
+
|
|
492
|
+
# Run startup animation
|
|
493
|
+
if ! $SKIP_ANIM; then
|
|
494
|
+
_forge_animation
|
|
495
|
+
fi
|
|
496
|
+
|
|
497
|
+
# Enter alt screen buffer
|
|
498
|
+
tput smcup 2>/dev/null || true
|
|
499
|
+
tput civis 2>/dev/null || true # hide cursor
|
|
500
|
+
stty -echo 2>/dev/null || true # disable echo
|
|
501
|
+
|
|
502
|
+
# Initial data load
|
|
503
|
+
_refresh_data
|
|
504
|
+
|
|
505
|
+
# Main loop
|
|
506
|
+
LAST_REFRESH=$(date +%s)
|
|
507
|
+
while true; do
|
|
508
|
+
_render
|
|
509
|
+
|
|
510
|
+
# Non-blocking read with timeout for auto-refresh
|
|
511
|
+
if read -t "$REFRESH_INTERVAL" -n 1 key 2>/dev/null; then
|
|
512
|
+
case "$key" in
|
|
513
|
+
q)
|
|
514
|
+
break
|
|
515
|
+
;;
|
|
516
|
+
j)
|
|
517
|
+
if [[ "$TASK_COUNT" -gt 0 && "$SELECTED" -lt $((TASK_COUNT - 1)) ]]; then
|
|
518
|
+
SELECTED=$((SELECTED + 1))
|
|
519
|
+
fi
|
|
520
|
+
;;
|
|
521
|
+
k)
|
|
522
|
+
if [[ "$SELECTED" -gt 0 ]]; then
|
|
523
|
+
SELECTED=$((SELECTED - 1))
|
|
524
|
+
fi
|
|
525
|
+
;;
|
|
526
|
+
"") # Enter key
|
|
527
|
+
_action_attach
|
|
528
|
+
;;
|
|
529
|
+
s)
|
|
530
|
+
_action_steer
|
|
531
|
+
;;
|
|
532
|
+
x)
|
|
533
|
+
_action_stop
|
|
534
|
+
;;
|
|
535
|
+
/)
|
|
536
|
+
_action_filter
|
|
537
|
+
;;
|
|
538
|
+
r)
|
|
539
|
+
_refresh_data
|
|
540
|
+
;;
|
|
541
|
+
"?")
|
|
542
|
+
if $SHOW_HELP; then
|
|
543
|
+
SHOW_HELP=false
|
|
544
|
+
else
|
|
545
|
+
SHOW_HELP=true
|
|
546
|
+
fi
|
|
547
|
+
;;
|
|
548
|
+
esac
|
|
549
|
+
fi
|
|
550
|
+
|
|
551
|
+
# Auto-refresh data
|
|
552
|
+
NOW_TS=$(date +%s)
|
|
553
|
+
if [[ $((NOW_TS - LAST_REFRESH)) -ge "$REFRESH_INTERVAL" ]]; then
|
|
554
|
+
_refresh_data
|
|
555
|
+
LAST_REFRESH=$NOW_TS
|
|
556
|
+
fi
|
|
557
|
+
done
|
package/bin/diff.sh
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# diff.sh — Show what an agent has changed without attaching
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
6
|
+
source "${SCRIPT_DIR}/../lib/common.sh"
|
|
7
|
+
|
|
8
|
+
usage() {
|
|
9
|
+
cat <<EOF
|
|
10
|
+
Usage: clawforge diff <id> [options]
|
|
11
|
+
|
|
12
|
+
Show git diff for a task's worktree without attaching to the agent.
|
|
13
|
+
|
|
14
|
+
Arguments:
|
|
15
|
+
<id> Task ID or short ID
|
|
16
|
+
|
|
17
|
+
Options:
|
|
18
|
+
--stat Show diffstat only (files changed summary)
|
|
19
|
+
--staged Show staged changes only
|
|
20
|
+
--name-only Show only file names
|
|
21
|
+
--save <path> Save diff to file
|
|
22
|
+
--help Show this help
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
clawforge diff 1
|
|
26
|
+
clawforge diff 1 --stat
|
|
27
|
+
clawforge diff sprint-jwt --save /tmp/changes.diff
|
|
28
|
+
EOF
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
TASK_REF="" STAT=false STAGED=false NAME_ONLY=false SAVE_PATH=""
|
|
32
|
+
|
|
33
|
+
while [[ $# -gt 0 ]]; do
|
|
34
|
+
case "$1" in
|
|
35
|
+
--stat) STAT=true; shift ;;
|
|
36
|
+
--staged) STAGED=true; shift ;;
|
|
37
|
+
--name-only) NAME_ONLY=true; shift ;;
|
|
38
|
+
--save) SAVE_PATH="$2"; shift 2 ;;
|
|
39
|
+
--help|-h) usage; exit 0 ;;
|
|
40
|
+
--*) log_error "Unknown option: $1"; usage; exit 1 ;;
|
|
41
|
+
*) TASK_REF="$1"; shift ;;
|
|
42
|
+
esac
|
|
43
|
+
done
|
|
44
|
+
|
|
45
|
+
[[ -z "$TASK_REF" ]] && { log_error "Task ID required"; usage; exit 1; }
|
|
46
|
+
|
|
47
|
+
_ensure_registry
|
|
48
|
+
|
|
49
|
+
# Resolve task
|
|
50
|
+
TASK_DATA=""
|
|
51
|
+
if [[ "$TASK_REF" =~ ^[0-9]+$ ]]; then
|
|
52
|
+
TASK_DATA=$(jq -r --argjson sid "$TASK_REF" '.tasks[] | select(.short_id == $sid)' "$REGISTRY_FILE" 2>/dev/null || true)
|
|
53
|
+
fi
|
|
54
|
+
if [[ -z "$TASK_DATA" ]]; then
|
|
55
|
+
TASK_DATA=$(registry_get "$TASK_REF" 2>/dev/null || true)
|
|
56
|
+
fi
|
|
57
|
+
if [[ -z "$TASK_DATA" ]]; then
|
|
58
|
+
log_error "Task '$TASK_REF' not found"
|
|
59
|
+
exit 1
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
WORKTREE=$(echo "$TASK_DATA" | jq -r '.worktree // empty')
|
|
63
|
+
BRANCH=$(echo "$TASK_DATA" | jq -r '.branch // empty')
|
|
64
|
+
SHORT_ID=$(echo "$TASK_DATA" | jq -r '.short_id // 0')
|
|
65
|
+
TASK_ID=$(echo "$TASK_DATA" | jq -r '.id')
|
|
66
|
+
|
|
67
|
+
if [[ -z "$WORKTREE" || ! -d "$WORKTREE" ]]; then
|
|
68
|
+
log_error "Worktree not found: ${WORKTREE:-'(none)'}. Task may have been cleaned."
|
|
69
|
+
exit 1
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
# Build git diff args
|
|
73
|
+
DIFF_ARGS=()
|
|
74
|
+
$STAT && DIFF_ARGS+=(--stat)
|
|
75
|
+
$STAGED && DIFF_ARGS+=(--staged)
|
|
76
|
+
$NAME_ONLY && DIFF_ARGS+=(--name-only)
|
|
77
|
+
|
|
78
|
+
# Get default branch for comparison
|
|
79
|
+
DEFAULT_BRANCH=$(git -C "$WORKTREE" symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/origin/||' || echo "main")
|
|
80
|
+
|
|
81
|
+
echo "── Diff for #${SHORT_ID} ($TASK_ID) ──"
|
|
82
|
+
echo "Branch: $BRANCH"
|
|
83
|
+
echo "Worktree: $WORKTREE"
|
|
84
|
+
echo ""
|
|
85
|
+
|
|
86
|
+
# Show uncommitted changes
|
|
87
|
+
UNCOMMITTED=$(git -C "$WORKTREE" diff "${DIFF_ARGS[@]}" 2>/dev/null || true)
|
|
88
|
+
COMMITTED=$(git -C "$WORKTREE" diff "${DIFF_ARGS[@]}" "${DEFAULT_BRANCH}...HEAD" 2>/dev/null || true)
|
|
89
|
+
|
|
90
|
+
OUTPUT=""
|
|
91
|
+
if [[ -n "$COMMITTED" ]]; then
|
|
92
|
+
OUTPUT+="── Committed changes (vs ${DEFAULT_BRANCH}) ──"$'\n'
|
|
93
|
+
OUTPUT+="$COMMITTED"$'\n'
|
|
94
|
+
fi
|
|
95
|
+
if [[ -n "$UNCOMMITTED" ]]; then
|
|
96
|
+
OUTPUT+="── Uncommitted changes ──"$'\n'
|
|
97
|
+
OUTPUT+="$UNCOMMITTED"$'\n'
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
if [[ -z "$OUTPUT" ]]; then
|
|
101
|
+
echo "(no changes detected)"
|
|
102
|
+
else
|
|
103
|
+
if [[ -n "$SAVE_PATH" ]]; then
|
|
104
|
+
echo "$OUTPUT" > "$SAVE_PATH"
|
|
105
|
+
echo "Saved diff to $SAVE_PATH"
|
|
106
|
+
else
|
|
107
|
+
echo "$OUTPUT"
|
|
108
|
+
fi
|
|
109
|
+
fi
|