Skip to content

Commit 895ef47

Browse files
shivamstaqclaude
andcommitted
Fix PR'd item retry + responsive TUI with selection and scrollable events
Fix 1: HandedOff eligibility check - IsEligible() now checks state.HandedOff map — items with PRs are never re-dispatched, even if project status update fails - HandedOff persisted to bbolt (survives restarts) - Loaded on startup via RestoreHandoff() Fix 2: Responsive TUI layout - Column widths calculated dynamically from terminal width (35%/20%/fixed) - Events panel uses remaining vertical space (adapts to terminal height) - Agent table expands to fill available width - Event messages use proportional widths Fix 3: Scrollable events panel - All events stored (up to 200 ring buffer), not just last 10 - Scroll with ↑/↓ when events panel is focused - PgUp/PgDn for fast scrolling - Auto-scroll to bottom on new events (unless user scrolled up) - Shows "↑ more above" / "↓ more below" indicators Fix 4: Agent selection + detail view - Tab switches focus between Agents table and Events panel - ↑/↓ navigates agent rows when agents panel focused - Enter opens full-screen detail view for selected agent - Detail view shows: issue, phase, session, runtime, tokens - Detail view shows filtered events for that specific agent - Esc returns to overview Keyboard: Tab Switch focus (agents ↔ events) ↑↓/jk Navigate (context-dependent) Enter Open agent detail view Esc Back to overview PgUp/Dn Fast scroll q Quit Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0b72a9a commit 895ef47

6 files changed

Lines changed: 400 additions & 144 deletions

File tree

cmd/symphony/main.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,19 @@ func main() {
293293
}
294294
}
295295

296+
// Restore handed-off items
297+
handoffs, err := store.LoadHandoffs()
298+
if err != nil {
299+
logger.Warn("failed to load persisted handoffs", "error", err)
300+
} else {
301+
for _, id := range handoffs {
302+
orch.RestoreHandoff(id)
303+
}
304+
if len(handoffs) > 0 {
305+
logger.Info("restored persisted handoffs", "count", len(handoffs))
306+
}
307+
}
308+
296309
// Restore totals
297310
totals, err := store.LoadTotals()
298311
if err != nil {

internal/orchestrator/eligibility.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,16 @@ func IsEligible(item WorkItem, cfg EligibilityConfig, state *State, maxConcurren
8686
}
8787
}
8888

89-
// Not already claimed or running
89+
// Not already claimed, running, or handed off
9090
if state.Claimed[item.WorkItemID] {
9191
return false, "already claimed"
9292
}
9393
if _, running := state.Running[item.WorkItemID]; running {
9494
return false, "already running"
9595
}
96+
if state.HandedOff != nil && state.HandedOff[item.WorkItemID] {
97+
return false, "already handed off (PR created)"
98+
}
9699

97100
// Global concurrency
98101
if len(state.Running) >= maxConcurrent {

internal/orchestrator/orchestrator.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,16 @@ func (o *Orchestrator) RestoreRetry(entry RetryEntry) {
149149
o.state.Claimed[entry.WorkItemID] = true
150150
}
151151

152+
// RestoreHandoff marks an item as handed off (from bbolt on startup).
153+
func (o *Orchestrator) RestoreHandoff(workItemID string) {
154+
o.mu.Lock()
155+
defer o.mu.Unlock()
156+
if o.state.HandedOff == nil {
157+
o.state.HandedOff = make(map[string]bool)
158+
}
159+
o.state.HandedOff[workItemID] = true
160+
}
161+
152162
// RunOnce executes one poll-and-dispatch tick per spec Section 8.1:
153163
// 1. Reconcile running work items
154164
// 2. Fire due retries

internal/orchestrator/worker.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,11 @@ func (r *Runner) Run(ctx context.Context, item WorkItem, attempt *int) WorkerRes
250250
handoffReached = true
251251
logger.Info("PR created/updated, marking as handed off")
252252
r.emitEvent(item, EventHandoff, "PR created, handed off for review")
253+
254+
// Persist handoff to bbolt so it survives restarts
255+
if r.deps.StateStore != nil {
256+
_ = r.deps.StateStore.SaveHandoff(item.WorkItemID)
257+
}
253258
}
254259

255260
// 7. Run after_run hook (best-effort)

internal/state/store.go

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@ import (
99
)
1010

1111
var (
12-
bucketRetries = []byte("retries")
13-
bucketTotals = []byte("totals")
14-
bucketSessions = []byte("sessions")
15-
keyTotals = []byte("agent_totals")
12+
bucketRetries = []byte("retries")
13+
bucketTotals = []byte("totals")
14+
bucketSessions = []byte("sessions")
15+
bucketHandedOff = []byte("handedoff")
16+
keyTotals = []byte("agent_totals")
1617
)
1718

1819
// RetryRecord is a persistent retry entry.
@@ -58,6 +59,9 @@ func Open(path string) (*Store, error) {
5859
if _, err := tx.CreateBucketIfNotExists(bucketSessions); err != nil {
5960
return err
6061
}
62+
if _, err := tx.CreateBucketIfNotExists(bucketHandedOff); err != nil {
63+
return err
64+
}
6165
return nil
6266
})
6367
if err != nil {
@@ -173,3 +177,22 @@ func (s *Store) LoadSessions() ([]SessionRecord, error) {
173177
})
174178
return records, err
175179
}
180+
181+
// SaveHandoff persists a handed-off work item ID.
182+
func (s *Store) SaveHandoff(workItemID string) error {
183+
return s.db.Update(func(tx *bolt.Tx) error {
184+
return tx.Bucket(bucketHandedOff).Put([]byte(workItemID), []byte("1"))
185+
})
186+
}
187+
188+
// LoadHandoffs returns all persisted handed-off work item IDs.
189+
func (s *Store) LoadHandoffs() ([]string, error) {
190+
var ids []string
191+
err := s.db.View(func(tx *bolt.Tx) error {
192+
return tx.Bucket(bucketHandedOff).ForEach(func(k, _ []byte) error {
193+
ids = append(ids, string(k))
194+
return nil
195+
})
196+
})
197+
return ids, err
198+
}

0 commit comments

Comments
 (0)