From c5cb7a036a92d01304fb53ff6bbc9a46dd79b83d Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Fri, 10 Jul 2026 16:21:17 +0000 Subject: [PATCH] feat(cli): colorize sync timeline lanes per unit Give each unit in `coder exp sync timeline` its own color, cycled from a fixed palette by lane index (git-log style). The lane glyphs (*, |, \, /) and the unit name in each row are drawn in the lane's color, making it easier to follow a unit across the graph. Color is resolved via cliui.Color, which returns a colorless profile in tests and on non-TTY output, so golden files are unaffected and no gating is required. Generated by Coder Agents on behalf of @SasSwart. --- cli/sync_timeline_render.go | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/cli/sync_timeline_render.go b/cli/sync_timeline_render.go index 863b54b101..cc28267315 100644 --- a/cli/sync_timeline_render.go +++ b/cli/sync_timeline_render.go @@ -6,8 +6,16 @@ import ( "time" "github.com/coder/coder/v2/agent/unit" + "github.com/coder/coder/v2/cli/cliui" + "github.com/coder/pretty" ) +// laneColors is the palette used to give each timeline lane (unit) its own +// color, cycled by lane index. cliui.Color yields a colorless profile in +// tests and on non-TTY output, so colored lanes do not affect golden +// files. +var laneColors = []string{"1", "2", "3", "4", "5", "6", "9", "10", "11", "12", "13", "14"} + // syncEventDescription renders a unit event as a short human-readable // phrase, shared by the sync status history and sync timeline views. func syncEventDescription(ev unit.Event) string { @@ -152,16 +160,28 @@ func (r *timelineRenderer) isReady(u unit.ID) bool { return true } +// laneStyle returns the color style for the lane at the given index. +func laneStyle(lane int) pretty.Style { + return pretty.Style{pretty.FgColor(cliui.Color(laneColors[lane%len(laneColors)]))} +} + +// colorizeLane colors s with the lane's color. In tests and on non-TTY +// output this returns s unchanged. +func colorizeLane(lane int, s string) string { + return pretty.Sprint(laneStyle(lane), s) +} + // glyphRow renders one graph row: mark in the target unit's lane, "|" in -// other active lanes, and spaces elsewhere. +// other active lanes, and spaces elsewhere. Each lane's glyph is drawn in +// that lane's color. func (r *timelineRenderer) glyphRow(target unit.ID, mark string) string { glyphs := make([]string, len(r.lanes)) for i, u := range r.lanes { switch { case u == target: - glyphs[i] = mark + glyphs[i] = colorizeLane(i, mark) case r.active[u]: - glyphs[i] = "|" + glyphs[i] = colorizeLane(i, "|") default: glyphs[i] = " " } @@ -170,17 +190,17 @@ func (r *timelineRenderer) glyphRow(target unit.ID, mark string) string { } // connectorRow draws the dependency-satisfaction edge from lane "from" -// toward lane "to". +// toward lane "to". Each lane's glyph is drawn in that lane's color. func (r *timelineRenderer) connectorRow(from, to unit.ID) string { glyphs := make([]string, len(r.lanes)) for i, u := range r.lanes { switch { case u == from && r.laneOf[from] < r.laneOf[to]: - glyphs[i] = `\` + glyphs[i] = colorizeLane(i, `\`) case u == from: - glyphs[i] = "/" + glyphs[i] = colorizeLane(i, "/") case r.active[u]: - glyphs[i] = "|" + glyphs[i] = colorizeLane(i, "|") default: glyphs[i] = " " } @@ -189,5 +209,6 @@ func (r *timelineRenderer) connectorRow(from, to unit.ID) string { } func (r *timelineRenderer) writeRow(sb *strings.Builder, graph string, at time.Time, u unit.ID, description string) { - _, _ = sb.WriteString(fmt.Sprintf("%s [%s] %s %s\n", graph, syncElapsed(r.base, at), u, description)) + name := colorizeLane(r.laneOf[u], string(u)) + _, _ = sb.WriteString(fmt.Sprintf("%s [%s] %s %s\n", graph, syncElapsed(r.base, at), name, description)) }