a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
268 lines
9.0 KiB
Go
268 lines
9.0 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"sync"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
|
|
"github.com/zzet/gortex/internal/progress"
|
|
"github.com/zzet/gortex/internal/tui"
|
|
)
|
|
|
|
// initProgress abstracts the two ways `gortex init` renders work-in-progress:
|
|
// the legacy single-line mesh Spinner (default / non-TTY) and the new
|
|
// multi-pane Dashboard (wizard path). The orchestration body in runInit calls
|
|
// into this interface so the same code drives both surfaces.
|
|
//
|
|
// The dashboard variant ignores Start (the dashboard owns its own banner) and
|
|
// treats Stage / Sub as a stage transition; the spinner variant treats Stage
|
|
// as the new top label and Sub as the sub-status, preserving today's UX for
|
|
// non-interactive callers.
|
|
type initProgress interface {
|
|
// Start opens the surface with an initial label. No-op for the dashboard.
|
|
Start(label string)
|
|
// Stage advances to a new high-level stage. sub is an optional detail.
|
|
Stage(name, sub string)
|
|
// Sub updates the sub-status of the current stage.
|
|
Sub(sub string)
|
|
// StageDone marks a stage complete, optionally with a final sub-line.
|
|
StageDone(name, sub string)
|
|
// Reporter returns a progress.Reporter ready to attach to the indexer.
|
|
Reporter() progress.Reporter
|
|
// Enabled reports whether the surface is rendering an animation. Used to
|
|
// gate the chatter-capture redirect that hides adapter logs.
|
|
Enabled() bool
|
|
// Done shuts down cleanly.
|
|
Done()
|
|
// Fail shuts down with an error mark.
|
|
Fail(err error)
|
|
}
|
|
|
|
// trackerProgress drives the banner-preset Tracker: the gortex mark lighting
|
|
// up across the planned stages, a live status line, and one checklist row per
|
|
// stage. It is the default init / install surface; with --no-progress (or on
|
|
// a pipe) the same driver degrades to clean line output.
|
|
type trackerProgress struct {
|
|
t *progress.Tracker
|
|
steps map[string]*progress.Step
|
|
}
|
|
|
|
// newTrackerProgress plans the given stages up front so the checklist shows
|
|
// the whole run from the first frame — pending rows fill in as stages start,
|
|
// and the planned count is what lets the logo light up proportionally.
|
|
func newTrackerProgress(w io.Writer, stages []string, plain bool) *trackerProgress {
|
|
opts := []progress.TrackerOption{progress.WithLogo()}
|
|
if plain {
|
|
opts = append(opts, progress.WithoutAnimation())
|
|
}
|
|
t := progress.NewTracker(w, opts...)
|
|
tp := &trackerProgress{t: t, steps: make(map[string]*progress.Step, len(stages))}
|
|
for _, s := range stages {
|
|
tp.steps[s] = t.AddStep(s)
|
|
}
|
|
return tp
|
|
}
|
|
|
|
func (p *trackerProgress) Start(label string) { p.t.Start(label) }
|
|
|
|
func (p *trackerProgress) Stage(name, sub string) {
|
|
p.steps[name] = p.t.StartStep(name)
|
|
p.t.SetStatus(sub)
|
|
}
|
|
|
|
func (p *trackerProgress) Sub(sub string) { p.t.SetStatus(sub) }
|
|
|
|
func (p *trackerProgress) StageDone(name, sub string) {
|
|
if s, ok := p.steps[name]; ok {
|
|
s.Done()
|
|
}
|
|
if sub != "" {
|
|
p.t.SetStatus(sub)
|
|
}
|
|
}
|
|
|
|
func (p *trackerProgress) Reporter() progress.Reporter { return p.t }
|
|
func (p *trackerProgress) Enabled() bool { return p.t.Animated() }
|
|
func (p *trackerProgress) Done() { p.t.Done("ready", "") }
|
|
func (p *trackerProgress) Fail(err error) { p.t.Fail(err) }
|
|
|
|
// dashboardProgress wraps a running Dashboard tea.Program so it satisfies
|
|
// initProgress. Wait blocks until the program exits.
|
|
type dashboardProgress struct {
|
|
session *initDashboardSession
|
|
rep progress.Reporter
|
|
}
|
|
|
|
func newDashboardProgress(s *initDashboardSession) *dashboardProgress {
|
|
return &dashboardProgress{
|
|
session: s,
|
|
rep: reporterFromSession(s),
|
|
}
|
|
}
|
|
|
|
func (d *dashboardProgress) Start(_ string) {} // dashboard renders its own header
|
|
func (d *dashboardProgress) Stage(name, sub string) {
|
|
if d.session != nil {
|
|
d.session.controller.SetActive(name, sub)
|
|
}
|
|
}
|
|
func (d *dashboardProgress) Sub(sub string) {
|
|
if d.session != nil {
|
|
d.session.controller.Sub(sub)
|
|
}
|
|
}
|
|
func (d *dashboardProgress) StageDone(name, sub string) {
|
|
if d.session != nil {
|
|
d.session.controller.Done(name, sub)
|
|
}
|
|
}
|
|
func (d *dashboardProgress) Reporter() progress.Reporter { return d.rep }
|
|
func (d *dashboardProgress) Enabled() bool { return d.session != nil }
|
|
func (d *dashboardProgress) Done() {
|
|
if d.session != nil {
|
|
d.session.controller.Finish(nil)
|
|
<-d.session.done
|
|
}
|
|
}
|
|
func (d *dashboardProgress) Fail(err error) {
|
|
if d.session != nil {
|
|
d.session.controller.Finish(err)
|
|
<-d.session.done
|
|
}
|
|
}
|
|
|
|
// initDashboardStages is the high-level checklist the dashboard renders down
|
|
// the right column. Each entry maps to a single wizard "phase" — the indexer
|
|
// emits many finer-grained sub-stages that ride on top via dashReporter.
|
|
//
|
|
// Stages are added / removed dynamically by buildInitStages depending on
|
|
// which optional passes (analyze, skills) the user picked.
|
|
var (
|
|
stageIndex = "Index repository"
|
|
stageAnalyze = "Analyze codebase"
|
|
stageSkills = "Generate skills"
|
|
stageAdapters = "Configure adapters"
|
|
)
|
|
|
|
// buildInitStages returns the ordered stage labels the dashboard will show
|
|
// given the per-pass flags. The orchestration code calls SetActive on each
|
|
// stage in this exact order; an empty list means "no dashboard, fall back to
|
|
// the legacy plain-text path."
|
|
func buildInitStages(analyze, skills bool) []string {
|
|
stages := make([]string, 0, 4)
|
|
if analyze || skills {
|
|
stages = append(stages, stageIndex)
|
|
}
|
|
if analyze {
|
|
stages = append(stages, stageAnalyze)
|
|
}
|
|
if skills {
|
|
stages = append(stages, stageSkills)
|
|
}
|
|
stages = append(stages, stageAdapters)
|
|
return stages
|
|
}
|
|
|
|
// initDashboardSession bundles the running tea.Program + its controller and
|
|
// the goroutine that hosts it. Closing the session cleanly tears down the
|
|
// program and waits for it to flush, so subsequent stderr writes (the agent
|
|
// summary card) start on a clean line.
|
|
type initDashboardSession struct {
|
|
prog *tea.Program
|
|
controller *tui.DashboardController
|
|
done chan struct{}
|
|
}
|
|
|
|
// startInitDashboard spawns the dashboard tea.Program against w and returns
|
|
// a session the orchestration code uses to drive it. Returns (nil, nil) when
|
|
// w isn't a TTY — the caller falls back to the legacy Spinner.
|
|
func startInitDashboard(w io.Writer, stages []string) *initDashboardSession {
|
|
if !progress.IsTTY(w) {
|
|
return nil
|
|
}
|
|
model := tui.NewDashboard("gortex init", stages)
|
|
prog := tea.NewProgram(model,
|
|
tea.WithOutput(w),
|
|
tea.WithoutSignalHandler(),
|
|
)
|
|
controller := tui.NewDashboardController(prog)
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
_, _ = prog.Run()
|
|
}()
|
|
return &initDashboardSession{prog: prog, controller: controller, done: done}
|
|
}
|
|
|
|
// dashReporter adapts the progress.Reporter interface onto a running dashboard
|
|
// controller. Stage names from the indexer become the *sub-label* of whatever
|
|
// the dashboard considers the active stage — the orchestration code owns the
|
|
// SetActive transitions.
|
|
type dashReporter struct {
|
|
mu sync.Mutex
|
|
ctl *tui.DashboardController
|
|
}
|
|
|
|
func (r *dashReporter) Report(stage string, current, total int) {
|
|
if r == nil || r.ctl == nil || stage == "" {
|
|
return
|
|
}
|
|
sub := stage
|
|
switch {
|
|
case total > 0:
|
|
sub = fmt.Sprintf("%s · %d / %d", stage, current, total)
|
|
case current > 0:
|
|
sub = fmt.Sprintf("%s · %d", stage, current)
|
|
}
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
r.ctl.Sub(sub)
|
|
}
|
|
|
|
// reporterFromSession returns a progress.Reporter that drives the dashboard
|
|
// session. Returns Nop when session is nil so callers can install the reporter
|
|
// unconditionally with WithReporter.
|
|
func reporterFromSession(s *initDashboardSession) progress.Reporter {
|
|
if s == nil || s.controller == nil {
|
|
return progress.Nop{}
|
|
}
|
|
return &dashReporter{ctl: s.controller}
|
|
}
|
|
|
|
// wizardSelectedDashboard is set to true when the bubbletea wizard finishes
|
|
// successfully — selectInitProgress reads it to decide whether to start the
|
|
// multi-pane Dashboard (wizard path) or fall back to the legacy Spinner.
|
|
// It's a package-level flag rather than threading a parameter because every
|
|
// path that mutates the init globals also mutates this flag, and runInit
|
|
// reads it once just before kicking off orchestration.
|
|
var wizardSelectedDashboard bool
|
|
|
|
// selectInitProgress returns the right initProgress surface for the current
|
|
// run. Order of precedence:
|
|
// - --no-progress: the tracker in plain line mode (no animation, no dashboard).
|
|
// - Wizard ran successfully: dashboard.
|
|
// - Otherwise: the banner tracker — gortex mark, status line, and the
|
|
// planned stage checklist (TTY-detected internally).
|
|
//
|
|
// The dashboard is only worth standing up after the wizard has cleared the
|
|
// alt-screen — otherwise it competes with the wizard's own draw cycle.
|
|
func selectInitProgress(w io.Writer) initProgress {
|
|
stages := buildInitStages(initAnalyze, initSkills)
|
|
if noProgress {
|
|
return newTrackerProgress(w, stages, true)
|
|
}
|
|
if wizardSelectedDashboard {
|
|
if s := startInitDashboard(w, stages); s != nil {
|
|
return newDashboardProgress(s)
|
|
}
|
|
}
|
|
return newTrackerProgress(w, stages, false)
|
|
}
|
|
|
|
// ensure unused-import linter doesn't trip on context until callers wire it
|
|
// back in. context is still part of the package; this keeps the import.
|
|
var _ = context.Background
|