Skip to content

WIP: Cli exec tui prototype#6490

Draft
6543 wants to merge 12 commits into
woodpecker-ci:mainfrom
6543-forks:cli-exec-tui
Draft

WIP: Cli exec tui prototype#6490
6543 wants to merge 12 commits into
woodpecker-ci:mainfrom
6543-forks:cli-exec-tui

Conversation

@6543
Copy link
Copy Markdown
Member

@6543 6543 commented Apr 23, 2026

!! DO NOT CHERRY PICK !!

disclaymer: claude was used to just see how it could look like and get some impressions... we can take some idears but should not copy the code!!!!

this just drafts what is rougth planed in #6488

it needs to be refined a lot -> #6490 (comment)

depends on:

current state...

example pipeline in webui:

image --- image --- image

Issues

  • docker client logs leak (image pull progress is breaking the UI -> it should be schown inside the tui not overwriting it
  • what about the debug logs?!?
  • linter issues are printed before tui starts and so hidden
  • steps not exec jet are not shown in tui
  • show workflows with wrong backend but skip them ... (show message why skkiped)

@woodpecker-bot
Copy link
Copy Markdown
Contributor

woodpecker-bot commented Apr 23, 2026

Surge PR preview deployment succeeded. View it at https://woodpecker-ci-woodpecker-pr-6490.surge.sh

@6543
Copy link
Copy Markdown
Member Author

6543 commented Apr 24, 2026

image

6543 and others added 12 commits May 26, 2026 23:05
… flag

No behavior change. Pure refactor that prepares the exec command for
the upcoming split between TUI and line-mode output paths.

- shared/logger: rename isInteractiveTerminal -> IsInteractiveTerminal
  so it is usable from cli/exec. Two in-package callers updated.
- shared/logger: add SetOutput for programmatic redirection of the
  zerolog global logger after SetupGlobalLogger has run. Returns a
  restore func. Intended for the TUI to route diagnostic log output
  into a ring buffer instead of stderr.
- cli/exec/flags: add --no-tui (WOODPECKER_EXEC_NO_TUI). Flag is
  wired up but not yet consumed; currently a no-op.
Self-contained DAG scheduler for the cli exec command. Consumes the
[]*builder.Item produced by pipeline/frontend/builder and runs ready
workflows in parallel respecting depends_on, up to a worker cap.

The scheduler is not yet wired into runExec; that is chunk 3. This
chunk only adds the package and its tests.

Design notes:
- Workflow-level events only (Pending/Ready/Running/Success/Failure/
  Blocked/Canceled). Step-level tracing and log lines remain the
  responsibility of the pipeline runtime tracer/logger the caller
  configures inside RunFunc. This keeps the scheduler agnostic of
  rendering concerns and lets both the (upcoming) TUI and the line
  writer share the same scheduler.
- No dependency on server/ code. A package-level comment flags the
  eventual refactor to unify with server/queue/fifo.go, but that is
  out of scope for this branch.
- Worker cap: Options.Parallel defaults to runtime.NumCPU(); negative
  means unbounded. No cli flag yet.
- Fail-fast is OFF: a failed workflow blocks its dependents but
  independent siblings keep running. This preserves the multierr
  behavior of the current sequential loop in runExec.
- Context cancellation marks pending workflows Canceled, lets running
  ones drain through their own ctx, then returns the aggregate error.
- Events channel is closed on return so consumers can use range.

Tests cover linear ordering, parallel-cap saturation under race
detection, transitive blocked propagation, fail-fast-off semantics,
context cancel mid-run draining, multi-failure aggregation, empty
input, and the defensive duplicate/unknown-dep validation (the
builder normally strips these before the scheduler sees them).
Coverage: 94.7%.
Replace the sequential for-loop in runExec with a scheduler.New()
call. Workflows now run concurrently where depends_on permits,
bounded by runtime.NumCPU(). This is the observable behavior change
of this chunk: independent workflows finish in parallel on line-mode
output, matching what the scheduler already made possible.

Line output contract (the grep-friendly stream on stderr):

- Prefix is now '[step] line' for single-workflow runs, '[wf/step]
  line' for multi-workflow runs. The wf/ qualifier is necessary
  because interleaved parallel output is otherwise unattributable.
- Dropped the 'Ln' line counter — redundant with the consumer's own
  line counting — and the elapsed-seconds suffix, which collides with
  zerolog's own timestamps when users pipe both streams together.
- Prefix body is capped at 24 chars with an ellipsis, to stop long
  step names from pushing the log body off an 80-column terminal.
- '# <workflow>' banner still emitted when a workflow starts running.
  The trigger is now a scheduler event (StateRunning) rather than
  the loop iterator, so banners appear in actual-start order and not
  in declaration order — matches what parallel execution produces.
- Blocked and canceled workflows emit a short diagnostic line so the
  user understands why a workflow produced no step output.

exec.go:

- runExec now builds one pipelineCtx (timeout + SIGTERM) for the
  whole DAG instead of one per workflow. The scheduler hands each
  RunFunc invocation a child of this ctx so cancellation fans out.
- Logger factory is per-workflow so the multi-workflow prefix is
  injected at LineWriter construction, not at each Write call.
- Dropped the //nolint:contextcheck tag that covered the old
  sequential loop's ctx mismatch; no longer needed.
- Removed the stale TODOs about parallelism and depends_on.

Tests:

- New cli/exec/line_test.go covers prefix format (single + multi
  workflow), truncation, missing-newline fix-up, Close no-op, and
  the '# name' banner contract.

make test-cli, golangci-lint, and gofumpt are all clean. The TUI
path is still not wired — --no-tui is inert; that comes in chunk 6.
Package skeleton for the interactive split-pane display. This chunk
compiles and has unit-tested pure-logic pieces, but the package is
not yet imported from runExec — the TUI mode stays inert until
chunk 6 wires it in.

Files:

- ringbuf.go      Ring is a FIFO line buffer with a byte cap and a
                  truncation counter. Safe for one writer + one
                  reader. Used per step for the log pane and once
                  for the zerolog debug tab.

- budget.go       Budget tracks multiple rings against a shared cap
                  and evicts oldest-from-largest-ring on Enforce.
                  Constants GlobalLogCapBytes = 200 MiB for the
                  shared step budget and DebugLogCapBytes = 5 MiB
                  for the separate zerolog ring.

- ringwriter.go   io.Writer adapter that splits on newlines and
                  appends to a Ring. Installed as the zerolog
                  destination during TUI mode so stderr writes
                  don't tear the alt-screen buffer. Buffers
                  incomplete trailing fragments across Write calls
                  and flushes on teardown.

- messages.go     tea.Msg types: WorkflowStateMsg, StepStateMsg,
                  LogLineMsg, DebugTickMsg, PipelineDoneMsg,
                  CancelingMsg. Data-only structs so model Update
                  is unit-testable without a real pipeline.

- model.go        Model is the bubbletea Model. Seeded with
                  workflow names; exposes DebugRing() and
                  StepRing(workflow, uuid, name) so cli/exec can
                  wire ring buffers without reaching into model
                  internals. Update handles each message type;
                  View returns a placeholderView for now so the
                  program is runnable end-to-end.

- styles.go       Unicode status glyphs and placeholderView. The
                  placeholder is deliberately prose so end-to-end
                  wiring can be verified before committing to a
                  lipgloss-based visual design in chunk 5.

Tests:

- ringbuf_test.go 14 cases: append within/over cap, eviction,
                  oversized-line policy, unbounded cap, snapshot
                  independence, concurrent write+read under -race;
                  budget oldest-from-largest policy, zero cap inert,
                  no-ring enforce; RingWriter newline splitting,
                  cross-Write buffering, Flush with/without data.

- model_test.go   Lifecycle (pending → running → success → done),
                  failure rendering, canceling state, step state +
                  ring routing, unknown-workflow no-op, quit key.

Deferred to chunk 5 and re-added there: Focus type, width/height/
focus fields, cursor, WindowSizeMsg handler — all dead weight in
the skeleton, cleaner to introduce alongside the layout they
actually serve.

No new direct dependencies: bubbletea/v2, bubbles/v2, lipgloss/v2
were already indirect via huh/v2. Lint, vet, tests, and the full
build-cli are all green.
Full split-pane layout. The tui package now renders a real interactive
UI instead of the chunk-4 prose placeholder. It is still not wired
into runExec — chunk 6 does that — so running the cli continues to
produce line-mode output for now.

Layout:

- Left: workflow tree with per-step rows, status glyphs, expand/
  collapse markers, and a cursor highlight when the tree has focus.
- Right: tabbed pane (logs | debug). The log tab is backed by a
  bubbles/v2 viewport populated from the selected step's Ring; auto-
  scrolls to bottom only when already at bottom so scrolling back
  through history isn't snatched away by new lines. The debug tab
  is backed by a separate viewport sourced from the zerolog Ring.
- Footer: focus indicator, N/M step counter, keybind hint. Switches
  to 'canceling…' / 'done' / 'failed' as the pipeline progresses.

State / model changes:

- Re-introduced the Focus enum (FocusTree / FocusLog / FocusDebug)
  and the UI state fields (width, height, cursor, focus, viewReady,
  logView, debugView) that chunk 4 deliberately omitted. The
  placeholder view in styles.go stays as the fallback before the
  first WindowSizeMsg arrives.
- Update now handles WindowSizeMsg (propagates to both viewports +
  flags viewReady), refreshes the active pane on LogLineMsg /
  DebugTickMsg when the change is visible, and routes KeyPressMsg
  through a new focus-aware dispatcher.
- Renamed CancellingMsg -> CancelingMsg to fix the misspell lint hit
  that survived chunk 4.

New file view.go:

- flatten() returns the navigable tree rows in render order —
  single source of truth used by cursor movement AND the renderer,
  so 'j' always moves the cursor to something the user sees.
- layout() computes the pane widths (3/8 tree, 5/8 log, minimum
  22 cols each) and reserves one row for the footer.
- resizeViewports() recomputes bubbles viewport sizes on resize.
- refreshLogView() / refreshDebugView() rebuild viewport content
  from the ring snapshots, prepending '[… N lines truncated]' when
  Ring has evicted anything.
- Per plan §7b: lipgloss JoinHorizontal of tree+right, JoinVertical
  for body+footer. Unicode status glyphs from styles.go. Tree rows
  get a '›' selection prefix on top of the reverse-video style so
  focus survives themes without reverse support.

New file view_test.go (12 cases) covers pane structure, cursor
movement with bounds saturation, tab cycling through all three
focus states, L jump to debug, selected-step log refresh vs
unselected-step storage-only, progress counter transition, the
canceling / failed footer states, and g/G top/bottom navigation.
A plainView helper uses charmbracelet/x/ansi.Strip so assertions
survive lipgloss's per-rune ANSI wrapping in styled output.

Styling (styles.go):

- Two colors from the 16-color ANSI palette: accent (cyan) for
  focused borders and active tabs; muted (bright black) for
  unfocused borders and inactive tabs. State-specific colors
  (success/failure/warning) will land with glyph coloring in the
  polish chunk.
- paneStyle(focused bool) returns a rounded-border style with the
  border color swapped on focus. selectedRowStyle is reverse video
  so it works on any terminal.

Named constants to satisfy mnd: paneBorderWidth,
rightPaneTabsHeight, minTotalWidthMultiple, rowInnerPadding.

shared/logger/logger.go: reworded two lines in the SetOutput
docstring so godot is happy when the wider scope gets linted. No
behavior change.

Verification: gofumpt clean, vet clean, golangci-lint 0 issues on
cli/exec/tui/... and shared/logger/..., 31/31 tests pass under
-race, full build-cli green.
runExec now dispatches between two paths based on '--no-tui' and
whether stdout is a terminal. The existing line-mode path is
unchanged for non-interactive invocations and for users who pass
'--no-tui'; on an interactive terminal, the TUI takes over.

exec.go changes:

- Extract the scheduler-driving logic that existed inline in runExec
  into runLineMode, unchanged in behavior. The mode decision at the
  top of runExec is a single conditional:

    useTUI := !c.Bool("no-tui") && logger.IsInteractiveTerminal()

- schedulerEventBuffer = 64 extracted as a named const. The buffer
  size mattered to lint (mnd) once it showed up in two call sites,
  and naming it lets the rationale live next to the value.

New file exec_tui.go:

- runTUIMode seeds a tui.Model with the workflow names from
  builder.Items, installs a RingWriter as the zerolog destination
  via logger.SetOutput so stderr writes do not tear the alt-screen,
  builds a tea.Program, and starts four cooperating goroutines:

    1. signal handler — two-stage sigint. First signal cancels the
       run context and Sends CancelingMsg so the model flips its
       footer. Second signal os.Exit(130), abandoning the alt-screen
       (the terminal restores on process exit).

    2. scheduler events drainer — forwards each scheduler.Event to
       p.Send(WorkflowStateMsg{Event: ev}).

    3. scheduler.Run — executes the DAG; Sends PipelineDoneMsg when
       it returns so the model can transition to its final state.

    4. the tea event loop (main goroutine via p.Run).

- tuiRunFunc builds a per-workflow tracer that chains
  tracing.DefaultTracer (so env vars still populate) with a
  prog.Send of StepStateMsg, and a per-workflow logger that hands
  CopyLineByLine a tuiStepWriter which forwards each line as
  LogLineMsg. No stderr writes on this path; the tui owns the
  display.

- tuiStepWriter is a trivial io.Writer that packages bytes into a
  LogLineMsg. Write returns len(p) per the io.Writer contract so
  upstream accounting stays correct. Close is a no-op.

- flushDebugRingToStderr runs after the alt-screen is gone and
  dumps any accumulated zerolog output back to stderr, prefixed
  with a truncation marker if the ring evicted anything. This
  preserves diagnostics that would otherwise vanish with the
  buffer.

tui/model.go:

- View now sets v.AltScreen = true, which is how bubbletea v2
  enables the alternate screen buffer (the v1 tea.WithAltScreen
  option no longer exists). This is the only semantic change to
  the tui package in this chunk; it doesn't affect tests because
  they access .Content.

Constants added: sigintExitCode = 130, sigChanBuffer = 2.

context.WithCancel is used inside runTUIMode to layer an explicit
cancel on top of pipelineCtx; it gets a //nolint:forbidigo with a
rationale because the two-stage sigint handler needs to cancel
independently from the outer timeout.

Verification: gofumpt clean, golangci-lint 0 issues on cli/exec/...,
all cli/exec, scheduler, and tui tests pass under -race,
build-cli green.
Two small but load-bearing fixes that close out the plan.

tui/model.go:

- Init now returns a tickDebug command. Previously Init returned
  nil, which meant DebugTickMsg was defined and handled but nothing
  ever produced one — the budget Enforce and the debug pane
  refresh simply never fired at runtime.

- The DebugTickMsg handler now re-arms itself with tickDebug(), so
  once the loop starts it runs until tea.Quit. Interval is 250ms,
  chosen as a named const (debugTickInterval) — fast enough that
  fresh zerolog lines appear interactively, slow enough that the
  budget scan is cheap.

- tickDebug() is a tea.Tick helper shared by Init and the handler.

Two new tests pin the contract: TestModelInitSchedulesDebugTick
verifies Init's returned cmd produces a DebugTickMsg;
TestModelDebugTickReschedules verifies the handler's cmd also
produces one. Without these, a misrefactor that breaks the loop
could land without visible failure.

exec_tui.go:

- The deferred cleanup block now flushes the debug ring to stderr
  in addition to restoring the logger. Previously flushDebugRing
  ToStderr ran only on the happy path, at the bottom of the
  function; if prog.Run panicked — or if any of the surrounding
  setup did — the zerolog output accumulated during the session
  was silently lost along with the alt-screen buffer.

  The defer body documents the ordering: restore the logger first
  so any log calls that happen during the flush itself go to real
  stderr rather than back into the ring we're draining; then
  flush the RingWriter's carried-over fragment; then dump the ring
  to stderr. This runs on success, error, AND panic paths — which
  is the whole point.

- The duplicate flush call near the bottom of runTUIMode is
  removed now that the defer covers it.

Verification: gofumpt clean, vet clean, golangci-lint 0 issues on
cli/exec/..., 34/34 tests pass under -race (tui package: 17 model
+ ring tests + 12 view tests + 5 from chunks 4-5 that the runner
counts separately; scheduler package: 14), build-cli green.
Before this fix, running the CLI with multiple workflows under the
docker backend produced errors like:

    Error response from daemon: network with name
    wp_01KPYB49XKRGNKWNASBTE298FS_default already exists
    Error response from daemon: failed to set up container
    networking: network wp_..._default not found

Because two or more parallel workflows were all trying to create,
use, and remove the same docker network and volume. The same
workflows running sequentially (before chunk 3) never hit this —
each finished its teardown before the next started.

Root cause: cli/exec/exec.go built a single prefix with
    prefix := "wp_" + ulid.Make().String()
and passed it into compiler.WithPrefix(prefix), making it the prefix
for every workflow the builder compiled in that run. The compiler
derives each workflow's network and volume name from the prefix as
    "%s_default"
so all workflows ended up with identical docker resource names.

The pipeline builder (pipeline/frontend/builder/builder.go:220-226)
ALREADY generates a unique per-workflow prefix of the form
    wp_<ULID>_<workflowID>
but the CLI's "caller options win" post-append in the builder
(line 231-233) meant the CLI's shared prefix clobbered the
per-workflow one.

Fix:

1. Remove compiler.WithPrefix from CompilerOptions. The builder's
   per-workflow prefix now wins, and each workflow's Config.Volume
   and Config.Network become genuinely unique.

2. The CLI's local-mode workspace volume mount
   "<prefix>_default:<workspace-base>" used to be added via
   compiler.WithVolumes(...), which applied globally. With
   per-workflow prefixes, the global injection point no longer
   exists, so we inject the mount after the build, walking each
   item's compiled Config and appending to every step's Volumes.

3. The non-local workspace mount is already handled per-workflow
   inside the compiler (convert.go:82), so that path needs no
   changes.

New file cli/exec/exec_workspace_test.go pins the behavior:
per-workflow mount isolation, every step across every stage gets
the mount, appending to existing Volumes not replacing them, safe
handling of items with nil Config or empty Volume, and multi-stage
workflows.

This is the bug the smoke test caught after shipping chunk 7 —
sequential execution hid it for years; the scheduler exposed it on
its first real parallel run.
… pane

Previously, anything printed before the TUI took over stdout — lint
warnings, validator output, the '# workflow' banner scheme — was
written directly to the terminal. On an interactive tty, that text
sat in the scrollback for a few milliseconds before the alt-screen
buffer wiped it on TUI startup. Users who needed to see the warnings
had to either quit the TUI to check the scrollback, or re-run with
--no-tui.

The TUI's earlier split-pane layout also had a 'debug' tab in the
right pane that carried the same mental-model: zerolog diagnostics
that were neither step output nor tree state. In practice the two
categories of content (pre-run warnings vs runtime diagnostics) are
the same thing: everything-else. This change merges them.

Layout change:

  before                              after
  ┌──────┬───────────────┐            ┌──────┬────────────────┐
  │ tree │ log │ debug   │            │ tree │ log            │
  │      │   (tabbed)    │            │      │                │
  └──────┴───────────────┘            ├──────┴────────────────┤
                                      │ messages              │
                                      └───────────────────────┘

The right pane is now dedicated to step log output only. A new
full-width 'messages' strip sits below the top row and above the
keybind footer, with defaultMessagesHeight = 8 rows (clamped against
minTopRowHeight = 6 and minMessagesHeight = 3 on tight terminals).

cli/exec/exec.go:

- The useTUI decision moves up ahead of the lint-warning print site.
  In TUI mode the warning text is captured into a strings.Builder
  and passed to runTUIMode as a new preRunMessages parameter
  instead of hitting stdout. Line mode keeps printing to stdout
  exactly as before — the output contract for CI logs is unchanged.

cli/exec/exec_tui.go:

- runTUIMode takes the new preRunMessages string. Each line is
  appended to the model's messages ring via strings.SplitAfter so
  trailing newlines are preserved. Zerolog's RingWriter is still
  installed into the same ring, so the pane seamlessly transitions
  from pre-run seed lines to runtime diagnostics with no visual
  split.

cli/exec/tui/:

- view.go: new layout() returns four values (tree width, log width,
  top-row height, messages height). resizeViewports sizes both
  bubbles viewports accordingly. renderView composes
  JoinHorizontal(tree, logPane) over messagesPane over footer.
  renderRightPane + renderTabs replaced by renderLogPane and
  renderMessagesPane. The log pane title annotates with the
  selected 'logs: <workflow>/<step>' so the user knows what
  they're reading without cross-referencing the tree cursor.

- styles.go: tabInactiveStyle dropped (no more tabs). Renamed
  tabActiveStyle -> paneTitleStyle to match its new role.

- model.go: Focus enum: FocusDebug -> FocusMessages. Field renames
  m.debug -> m.messages, m.debugView -> m.messagesView,
  refreshDebugView -> refreshMessagesView. DebugRing() ->
  MessagesRing() on the public surface. DebugLogCapBytes ->
  MessagesLogCapBytes. DebugTickMsg kept (still drives refreshes,
  name describes the role not the pane).

- Layout constants added: defaultMessagesHeight = 8,
  minTopRowHeight = 6, minMessagesHeight = 3, paneBorderHeight = 2.
  Obsolete rightPaneTabsHeight removed.

- L keybind still jumps to the messages pane (previously 'debug').
  Footer shows [tree] / [log] / [messages].

Tests:

- view_test.go: existing 'right-pane tabs must render' assertion
  becomes 'messages pane must render' on the word 'messages'.
  TestDebugKeyJumpsToDebugPane renamed to TestLKeyJumpsToMessages
  Pane with the footer assertion updated to '[messages]'. New
  TestPreRunMessagesAppearInMessagesPane seeds the ring with two
  lint-warning lines, drives the WindowSizeMsg + DebugTickMsg
  sequence, and asserts both lines appear in the rendered frame.

Verification: gofumpt clean, vet clean, golangci-lint 0 issues on
cli/exec/..., 32/32 tui tests + all cli/exec tests pass under -race,
build-cli succeeds, line-mode smoke still shows warnings on stdout
as before.
Previously, step rows in the tree materialized lazily: a step only
appeared once its first tracer event or first log line arrived,
which is also the moment it started running. Pending steps were
invisible until they began executing, so the tree effectively
showed 'running' and 'finished' steps but never 'what's coming
next'.

This change pre-seeds the model with the compiled step list for
each workflow so every step is visible from the moment the TUI
starts, with a pending glyph (·). Each step then visibly transitions
through pending → running (●) → success (✓) / failure (✗) / skipped
(⊘) as the pipeline executes — giving the user a real-time picture
of the plan and its progress instead of pop-ins at each step start.

cli/exec/tui/model.go:

- New NewFromSeeds constructor takes []WorkflowSeed, each with a
  Name and a []StepSeed describing the steps the workflow will
  run. Seeds a stepNode with a log ring per step, registered with
  the global budget, in StatePending visually.
- The bare New(workflowNames) constructor is preserved as a thin
  wrapper over NewFromSeeds for tests that don't care about steps.
- WorkflowSeed and StepSeed are tui-local types, not pass-through
  of builder.Item — keeps the tui package from depending on the
  builder and lets tests build fixtures without constructing a full
  backend_types.Config.
- stepNode grows a 'started' bool. It flips true the first time a
  tracer event reports Started != 0, distinguishing pending from
  running (both have Exited=false).
- handleStepState and StepRing now go through a shared
  findOrCreateStep helper that matches by UUID first, then by name,
  then (defensively) creates a new node. The seeded-UUID happy path
  means production runs never create duplicates; the name fallback
  keeps tests that use the bare New() constructor working; the
  create fallback covers unknown steps that might sneak in from
  future runtime sources.

cli/exec/tui/view.go:

- stepGlyph now returns glyphPending (·) when neither terminal
  states nor started are set. Ordering in the switch: terminal
  first (skipped/success/failure/oomKill), then started (running),
  then fallback (pending) — so a brief terminal-state-without-
  started-flip doesn't flicker back through pending.

cli/exec/tui/styles.go:

- The placeholder view's ad-hoc step glyph logic replaced with a
  call to stepGlyph so pre-WindowSizeMsg renders match the full
  layout's treatment.

cli/exec/exec_tui.go:

- runTUIMode now builds []tui.WorkflowSeed from each item's
  Config.Stages[].Steps[] and calls tui.NewFromSeeds instead of
  passing just workflow names. Empty or nil Config is handled
  gracefully (the workflow still seeds with no steps, matching the
  pre-change behavior for that edge case).

Tests (cli/exec/tui/view_test.go):

- TestSeededStepsAppearAsPendingBeforeRunning: three seeded steps
  all render in the tree with no running/success/failure glyphs
  present — they're all pending.
- TestStepTransitionsPendingToRunningToSuccess: walks a single step
  through pending → running (Started=<now>, Exited=false) → success
  (Exited, ExitCode=0), asserting the correct glyph at each stage.
- TestStepSeededByUUIDDoesNotDuplicate: seed a step, then send a
  tracer event with the same UUID; verify the step name appears
  exactly once in the tree region (no duplicate row from the
  lookup-by-uuid path missing the seed).

Verification: gofumpt clean, vet clean, golangci-lint 0 issues, all
tui tests (34/34) pass under -race, full cli/exec tests pass,
build-cli green, line-mode smoke still prints warnings to stdout
unchanged.
@6543
Copy link
Copy Markdown
Member Author

6543 commented May 26, 2026

Port PoC into TODOs:

  • split out DAG workflow exec, impl. it propperly (chunk {2, 3})
    • fix per-workflow prefix
  • create tui (chunk {4, 5}, 909abfc, 1ccf2d4)
  • use tui for exec (chunk {6, 7})

@6543 6543 mentioned this pull request May 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants