fancy devenv prompt#1183
Conversation
a7fcdd6 to
6ca4285
Compare
There was a problem hiding this comment.
question: can this be used/extended for the test logs as well? For example, we currently have a consistent spam of "failed to get verifier result for messageID" until we get a success, would be nice to clean that up.
There was a problem hiding this comment.
No, I don't think so. Tests should use something else. The way we split test running and success checking into separate CI steps is pretty weird, I think they deserve some specific attention.
A couple options:
- Use 'gotestsum'. It has a bunch of features for splitting up human/machine readable results.
- For the "failed to get verifier result for messageID" logs, we should probably improve the logging.
- Generally, I'd like to see much more precision in the test results. The pass/fail nature of our existing assertions mean that any failure requires a full fledged investigation to figure out whats wrong.
Introduces a Reporter interface in the runtime package plus bubbletea, simple, and noop implementations so ccv up/test/restart render a live spinner TUI on a terminal and plain one-line output in CI or verbose mode. Redirects build/env noise to a log file and prints an environment summary table when the run completes.
Adds a mutex-protected Status() string method to the protocol_contracts, committeeccv, and committeeccv_clnode components so the bubbletea TUI reporter can display fine-grained progress during the two longest phases. The status advances through named steps (e.g. "deploying chain X of N", "configuring lanes") so operators know exactly where a slow run is stuck.
…tions Save the active profile to ~/.ccv/active_config whenever ccv up or ccv test successfully loads a profile. Bare `ccv up` (no --profile arg) now reads that saved path before falling back to standard.profile, so the last profile used is automatically reused.
Handle tea.KeyCtrlC in the model's Update function so the TUI exits cleanly when the user interrupts. After prog.Run() returns, os.Exit(130) kills the background environment goroutine and signals the conventional Ctrl-C exit code to the shell.
Add two-line rendering for in-progress components (phase prefix, aligned duration, status on a second line with └── indent) and a bright-green style to distinguish active from completed. Include aggregator and indexer URLs with labels in the final summary report.
Bind lipgloss styles to the real terminal renderer so colors survive stdout redirection; render checkmarks and component names in green with default-color phase/duration, remove the fake service from the summary, and append total startup time to the final report.
Record a timestamp on each status entry so the TUI can display how long each sub-step took, both while active and in the completed log. Surface the verbose log file path in the post-run summary.
Rename Statuser to StatusGetter to avoid misspell autocorrection, fix an unused-write lint error in simpleReporter.OnFinish, and format all TUI durations to 1 decimal place with fixed 7-char width to prevent text bouncing.
Add a dim parenthesised cumulative run time (e.g. "(1m27.0s)") after each active component's per-component elapsed in the TUI. Rewrite fmtDur to use seconds-only output with zero-padded sub-minute seconds so durations don't shift text as they cross character-width boundaries.
6ca4285 to
09c21a4
Compare
|
Code coverage report:
|
| // newEnvFn is set by PersistentPreRunE (or applyProfile) based on the | ||
| // --env-mode flag. The Reporter receives component-level events in phased | ||
| // mode; legacy mode wraps the monolith with stage-level events only. | ||
| var newEnvFn func(r devenvruntime.Reporter) error |
There was a problem hiding this comment.
I wanted to ensure the TUI is fully decoupled from the devenv, thats accomplished with the Reporter interface. The environment notifies the reporter when something interesting happens. There are several reporters in this PR:
- Verbose: use the same output as today.
- Fancy: full TUI with dynamic output.
- Simple: line-based output, this is the default for non-TTY environments.
| // Status implements the devenvruntime.StatusGetter optional interface so the TUI | ||
| // reporter can poll for fine-grained progress during the long deploy loop. | ||
| func (p *component) Status() string { | ||
| p.mu.Lock() | ||
| defer p.mu.Unlock() | ||
| return p.status | ||
| } |
There was a problem hiding this comment.
Status() string is an optional function that the runtime will call if present on the component. Many components run in a couple seconds or less, so status isn't really necessary. Others run for several minutes, this is how the runtime gets more information about what they're up to.
| type tuiModel struct { | ||
| styles tuiStyles | ||
| log []string // completed / stage lines | ||
| active map[string]*activeComp |
There was a problem hiding this comment.
Currently, the runtime only support a single active component. The TUI supports having multiple.
| if saved := getActiveConfig(); strings.HasSuffix(strings.TrimSpace(saved), ".profile") { | ||
| profilePath = strings.TrimSpace(saved) | ||
| } else { | ||
| profilePath = "standard.profile" | ||
| } |
There was a problem hiding this comment.
This is a small random fix. The "active" config is stored in the home directory but we were ignoring it.
If you run "ccv up --profile phased.profile", a subsequent call to "ccv restart" will stop the current environment and bring the phased profile up again. Without the fix it was reverting to the standard profile.
| // the fancy reporter should render to (the saved real-terminal fd), and a | ||
| // cleanup func that restores the original fds. In verbose mode nothing is | ||
| // redirected and the returned writer is os.Stderr. | ||
| func redirectToLogFile(verbose bool, prefix string) (term *os.File, cleanup func(), logPath string, err error) { |
There was a problem hiding this comment.
Presumably a lot of this can be done by the logger. It would be nice to initialize the logger to a file rather than all this redirect stuff.
There was a problem hiding this comment.
You're not expected to understand this file.
I tried to hide the entirety of BubbleTEA in here. It uses "The Elm Architecture" to create an event loop for the TUI. All state changes happen through the Update(event) function which returns an "immutable" copy of the tuiModel. The TUI is then generated from the model via View() string.
Everything else is related to the Reporter interface which is a crude shim that injects outside events into the Update function.
There was a problem hiding this comment.
This is the simple summary that will be displayed for non-TTY environments.
| func New(verbose bool, out io.Writer) devenvruntime.Reporter { | ||
| if verbose { | ||
| return devenvruntime.NoopReporter{} | ||
| } | ||
| if f, ok := out.(*os.File); ok && term.IsTerminal(f.Fd()) { | ||
| return newBubbletearReporter(out) | ||
| } | ||
| return newSimpleReporter(out) | ||
| } |
There was a problem hiding this comment.
Extra fancy term stuff.
There was a problem hiding this comment.
Pull request overview
Adds a new “fancy” devenv progress UI by introducing a runtime-level reporting interface and implementing both a line-based reporter and a Bubble Tea TUI reporter, then wiring it into phased environment startup and the ccv CLI (including log redirection + post-run summaries).
Changes:
- Introduce
devenvruntime.Reporter(plusNoopReporter) and emit component lifecycle events from the phased runtime. - Add
build/devenv/reporterimplementations (simple + Bubble Tea) and a summary renderer that parsesenv-out.toml. - Update
ccvCLI to select a reporter, redirect logs to a file by default (unless--verbose), and show a post-run summary; add optional per-component status polling viaStatusGetter.
Reviewed changes
Copilot reviewed 14 out of 15 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| build/devenv/runtime/reporter.go | Adds the Reporter interface and NoopReporter. |
| build/devenv/runtime/environment.go | Adds reporter hooks (OnStart/OnFinish) and a new reporter parameter to environment constructors. |
| build/devenv/runtime/environment_test.go | Updates helper to pass a reporter into NewEnvironmentWithRegistry. |
| build/devenv/runtime/component.go | Adds StatusGetter optional interface for fine-grained progress updates. |
| build/devenv/reporter/summary.go | Adds env summary rendering by parsing env-out.toml. |
| build/devenv/reporter/simple.go | Implements a line-based reporter. |
| build/devenv/reporter/factory.go | Adds reporter selection based on verbosity + TTY detection. |
| build/devenv/reporter/bubbletea.go | Implements Bubble Tea TUI reporter with status polling and stage/component views. |
| build/devenv/go.mod | Adds Bubble Tea / Lip Gloss / term dependencies. |
| build/devenv/go.sum | Adds checksums for new dependencies. |
| build/devenv/environment_phased.go | Adds NewPhasedEnvironmentWithReporter and plumbs reporter into runtime startup. |
| build/devenv/components/protocol_contracts/component.go | Adds mutex-protected status updates during long deploy loops. |
| build/devenv/components/committeeccv/component.go | Adds mutex-protected status updates and threads setStatus through phase 3 core. |
| build/devenv/components/committeeccv/component_clnode.go | Adds mutex-protected status updates and threads setStatus through phase 3 core. |
| build/devenv/cli/ccv.go | Integrates reporter selection, redirects logs to file by default, adds --verbose, and prints summaries for up/restart. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| func NewEnvironmentWithRegistry(ctx context.Context, rawConfig map[string]any, r *Registry, effectExecutor EffectExecutor, logger zerolog.Logger, reporter Reporter) (map[string]any, error) { | ||
| if effectExecutor == nil { | ||
| effectExecutor = noopEffectExecutor{} | ||
| } |
| if p1, ok := comp.(Phase1Component); ok { | ||
| reporter.OnStart(phase, key, comp) | ||
| start := time.Now() | ||
| out, effects, err := p1.RunPhase1(ctx, rawConfig, rawConfig[key]) | ||
| out, effects, runErr := p1.RunPhase1(ctx, rawConfig, rawConfig[key]) | ||
| compTimings.Record(phase, key, start, time.Now()) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("phase1 %s: %w", key, err) | ||
| reporter.OnFinish(phase, key, runErr) | ||
| if runErr != nil { |
| // - verbose=true → NoopReporter (caller keeps raw zerolog output) | ||
| // - TTY detected → BubbletearReporter (animated TUI) | ||
| // - otherwise → SimpleFancyReporter (line-based progress) |
| func printSummary(out io.Writer, outTomlPath, logFilePath string, elapsed time.Duration) { | ||
| if outTomlPath == "" { | ||
| return | ||
| } |
| abs, err := resolveTomlPath(outTomlPath) | ||
| if err != nil { | ||
| fmt.Fprintf(out, "\nenv output: %s (not found)\n", outTomlPath) | ||
| return | ||
| } |
| var raw map[string]any | ||
| if _, err := toml.DecodeFile(abs, &raw); err != nil { | ||
| fmt.Fprintf(out, "\nenv output: %s (parse error: %v)\n", abs, err) | ||
| return | ||
| } |
| if tm, ok := m.(tuiModel); ok { | ||
| if tm.cancelled { | ||
| os.Exit(130) | ||
| } |
| term, cleanup, _, err := redirectToLogFile(verbose, "ccv-test") | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| // Stage 1: optional image build. | ||
| if buildEnabled { | ||
| progress(fmt.Sprintf("building images (just %s)...", buildTarget)) | ||
| buildCmd := exec.Command("just", buildTarget) | ||
| buildCmd.Stdout = os.Stdout | ||
| buildCmd.Stderr = os.Stderr | ||
| if err := buildCmd.Run(); err != nil { | ||
| return fmt.Errorf("just %s failed: %w", buildTarget, err) | ||
| defer cleanup() |
| realStdoutFd, _ := syscall.Dup(int(os.Stdout.Fd())) | ||
| realStderrFd, _ := syscall.Dup(int(os.Stderr.Fd())) | ||
| realTerm := os.NewFile(uintptr(realStderrFd), "real_stderr") | ||
|
|
||
| _ = syscall.Dup2(int(lf.Fd()), int(os.Stdout.Fd())) | ||
| _ = syscall.Dup2(int(lf.Fd()), int(os.Stderr.Fd())) |
Description
Testing
Checklist
changelogdirectory)just lint fix- no new lint errorsjust generate- mocks and protobufs are up to date