From baa05a7c541cba4aa613f3db8d37f5afe0fc19b4 Mon Sep 17 00:00:00 2001 From: simon Date: Fri, 13 Mar 2026 07:36:17 +0100 Subject: [PATCH 1/7] Add TUI table overrides for 5 high-traffic list commands --- cmd/workspace/cluster-policies/overrides.go | 18 +++++++++++++ cmd/workspace/lakeview/overrides.go | 25 ++++++++++++++++++ cmd/workspace/pipelines/overrides.go | 26 ++++++++++++++++++ cmd/workspace/secrets/overrides.go | 29 +++++++++++++++++++++ 4 files changed, 98 insertions(+) diff --git a/cmd/workspace/cluster-policies/overrides.go b/cmd/workspace/cluster-policies/overrides.go index 9278b29c39..8bc320aa5f 100644 --- a/cmd/workspace/cluster-policies/overrides.go +++ b/cmd/workspace/cluster-policies/overrides.go @@ -2,6 +2,7 @@ package cluster_policies import ( "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/tableview" "github.com/databricks/databricks-sdk-go/service/compute" "github.com/spf13/cobra" ) @@ -10,6 +11,23 @@ func listOverride(listCmd *cobra.Command, _ *compute.ListClusterPoliciesRequest) listCmd.Annotations["template"] = cmdio.Heredoc(` {{range .}}{{.PolicyId | green}} {{.Name}} {{end}}`) + + columns := []tableview.ColumnDef{ + {Header: "Policy ID", Extract: func(v any) string { + return v.(compute.Policy).PolicyId + }}, + {Header: "Name", Extract: func(v any) string { + return v.(compute.Policy).Name + }}, + {Header: "Default", Extract: func(v any) string { + if v.(compute.Policy).IsDefault { + return "yes" + } + return "" + }}, + } + + tableview.RegisterConfig(listCmd, tableview.TableConfig{Columns: columns}) } func getOverride(getCmd *cobra.Command, _ *compute.GetClusterPolicyRequest) { diff --git a/cmd/workspace/lakeview/overrides.go b/cmd/workspace/lakeview/overrides.go index 6ffb641aa9..17953123a4 100644 --- a/cmd/workspace/lakeview/overrides.go +++ b/cmd/workspace/lakeview/overrides.go @@ -1,10 +1,34 @@ package lakeview import ( + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/tableview" "github.com/databricks/databricks-sdk-go/service/dashboards" "github.com/spf13/cobra" ) +func listOverride(listCmd *cobra.Command, listReq *dashboards.ListDashboardsRequest) { + listCmd.Annotations["headerTemplate"] = cmdio.Heredoc(` + {{header "Dashboard ID"}} {{header "Name"}} {{header "State"}}`) + listCmd.Annotations["template"] = cmdio.Heredoc(` + {{range .}}{{green "%s" .DashboardId}} {{.DisplayName}} {{blue "%s" .LifecycleState}} + {{end}}`) + + columns := []tableview.ColumnDef{ + {Header: "Dashboard ID", Extract: func(v any) string { + return v.(dashboards.Dashboard).DashboardId + }}, + {Header: "Name", Extract: func(v any) string { + return v.(dashboards.Dashboard).DisplayName + }}, + {Header: "State", Extract: func(v any) string { + return string(v.(dashboards.Dashboard).LifecycleState) + }}, + } + + tableview.RegisterConfig(listCmd, tableview.TableConfig{Columns: columns}) +} + func publishOverride(cmd *cobra.Command, req *dashboards.PublishRequest) { originalRunE := cmd.RunE cmd.RunE = func(cmd *cobra.Command, args []string) error { @@ -15,5 +39,6 @@ func publishOverride(cmd *cobra.Command, req *dashboards.PublishRequest) { } func init() { + listOverrides = append(listOverrides, listOverride) publishOverrides = append(publishOverrides, publishOverride) } diff --git a/cmd/workspace/pipelines/overrides.go b/cmd/workspace/pipelines/overrides.go index 361c834bfa..64c7669440 100644 --- a/cmd/workspace/pipelines/overrides.go +++ b/cmd/workspace/pipelines/overrides.go @@ -62,8 +62,34 @@ func listPipelinesOverride(listCmd *cobra.Command, listReq *pipelines.ListPipeli } } +func listPipelineEventsOverride(listCmd *cobra.Command, listReq *pipelines.ListPipelineEventsRequest) { + listCmd.Annotations["headerTemplate"] = cmdio.Heredoc(` + {{header "Timestamp"}} {{header "Level"}} {{header "Event Type"}} {{header "Message"}}`) + listCmd.Annotations["template"] = cmdio.Heredoc(` + {{range .}}{{.Timestamp}} {{.Level}} {{.EventType}} {{.Message}} + {{end}}`) + + columns := []tableview.ColumnDef{ + {Header: "Timestamp", Extract: func(v any) string { + return v.(pipelines.PipelineEvent).Timestamp + }}, + {Header: "Level", Extract: func(v any) string { + return string(v.(pipelines.PipelineEvent).Level) + }}, + {Header: "Event Type", Extract: func(v any) string { + return v.(pipelines.PipelineEvent).EventType + }}, + {Header: "Message", MaxWidth: 60, Extract: func(v any) string { + return v.(pipelines.PipelineEvent).Message + }}, + } + + tableview.RegisterConfig(listCmd, tableview.TableConfig{Columns: columns}) +} + func init() { listPipelinesOverrides = append(listPipelinesOverrides, listPipelinesOverride) + listPipelineEventsOverrides = append(listPipelineEventsOverrides, listPipelineEventsOverride) cmdOverrides = append(cmdOverrides, func(cli *cobra.Command) { // all auto-generated commands apart from nonManagementCommands go into 'management' group diff --git a/cmd/workspace/secrets/overrides.go b/cmd/workspace/secrets/overrides.go index b215f17a7f..8403ac4699 100644 --- a/cmd/workspace/secrets/overrides.go +++ b/cmd/workspace/secrets/overrides.go @@ -1,7 +1,10 @@ package secrets import ( + "strconv" + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/tableview" "github.com/databricks/databricks-sdk-go/service/workspace" "github.com/spf13/cobra" ) @@ -16,6 +19,17 @@ func listScopesOverride(listScopesCmd *cobra.Command) { listScopesCmd.Annotations["template"] = cmdio.Heredoc(` {{range .}}{{.Name|green}} {{.BackendType}} {{end}}`) + + columns := []tableview.ColumnDef{ + {Header: "Scope", Extract: func(v any) string { + return v.(workspace.SecretScope).Name + }}, + {Header: "Backend Type", Extract: func(v any) string { + return string(v.(workspace.SecretScope).BackendType) + }}, + } + + tableview.RegisterConfig(listScopesCmd, tableview.TableConfig{Columns: columns}) } func listSecretsOverride(listSecretsCommand *cobra.Command, _ *workspace.ListSecretsRequest) { @@ -24,6 +38,21 @@ func listSecretsOverride(listSecretsCommand *cobra.Command, _ *workspace.ListSec listSecretsCommand.Annotations["template"] = cmdio.Heredoc(` {{range .}}{{.Key|green}} {{.LastUpdatedTimestamp}} {{end}}`) + + columns := []tableview.ColumnDef{ + {Header: "Key", Extract: func(v any) string { + return v.(workspace.SecretMetadata).Key + }}, + {Header: "Last Updated", Extract: func(v any) string { + ts := v.(workspace.SecretMetadata).LastUpdatedTimestamp + if ts == 0 { + return "" + } + return strconv.FormatInt(ts, 10) + }}, + } + + tableview.RegisterConfig(listSecretsCommand, tableview.TableConfig{Columns: columns}) } func init() { From 510f2e8a7dbc30e894b4d457a396bd0fcd81d921 Mon Sep 17 00:00:00 2001 From: simon Date: Fri, 13 Mar 2026 13:34:17 +0100 Subject: [PATCH 2/7] Format secret timestamps as human-readable dates --- cmd/workspace/secrets/overrides.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/workspace/secrets/overrides.go b/cmd/workspace/secrets/overrides.go index 8403ac4699..5de7268905 100644 --- a/cmd/workspace/secrets/overrides.go +++ b/cmd/workspace/secrets/overrides.go @@ -1,7 +1,7 @@ package secrets import ( - "strconv" + "time" "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/tableview" @@ -48,7 +48,7 @@ func listSecretsOverride(listSecretsCommand *cobra.Command, _ *workspace.ListSec if ts == 0 { return "" } - return strconv.FormatInt(ts, 10) + return time.UnixMilli(ts).UTC().Format("2006-01-02 15:04:05") }}, } From 326069c5710e4c04c84e19b954dbec2ebb0b016a Mon Sep 17 00:00:00 2001 From: simon Date: Fri, 13 Mar 2026 15:40:26 +0100 Subject: [PATCH 3/7] Replace unused listReq parameters with _ in list override functions Several list override functions declared a named request parameter (e.g. listReq) that was never used after flag binding. Replace these with blank identifiers to satisfy go vet and make intent clearer. The parameter is kept named in clusters, jobs, pipelines, and workspace overrides where it is actively used for flag binding or search closures. --- cmd/workspace/alerts/overrides.go | 2 +- cmd/workspace/apps/overrides.go | 4 ++-- cmd/workspace/catalogs/overrides.go | 2 +- cmd/workspace/external-locations/overrides.go | 2 +- cmd/workspace/jobs/overrides.go | 2 +- cmd/workspace/lakeview/overrides.go | 2 +- cmd/workspace/pipelines/overrides.go | 2 +- cmd/workspace/repos/overrides.go | 2 +- cmd/workspace/schemas/overrides.go | 2 +- cmd/workspace/tables/overrides.go | 2 +- cmd/workspace/volumes/overrides.go | 2 +- cmd/workspace/warehouses/overrides.go | 2 +- 12 files changed, 13 insertions(+), 13 deletions(-) diff --git a/cmd/workspace/alerts/overrides.go b/cmd/workspace/alerts/overrides.go index ed5fdaa8db..b7d3a3e3f4 100644 --- a/cmd/workspace/alerts/overrides.go +++ b/cmd/workspace/alerts/overrides.go @@ -7,7 +7,7 @@ import ( "github.com/spf13/cobra" ) -func listOverride(listCmd *cobra.Command, listReq *sql.ListAlertsRequest) { +func listOverride(listCmd *cobra.Command, _ *sql.ListAlertsRequest) { listCmd.Annotations["template"] = cmdio.Heredoc(` {{range .}}{{green "%s" .Id}} {{.DisplayName}} {{.State}} {{end}}`) diff --git a/cmd/workspace/apps/overrides.go b/cmd/workspace/apps/overrides.go index ec6a25b803..480f271a94 100644 --- a/cmd/workspace/apps/overrides.go +++ b/cmd/workspace/apps/overrides.go @@ -10,7 +10,7 @@ import ( "github.com/spf13/cobra" ) -func listOverride(listCmd *cobra.Command, listReq *apps.ListAppsRequest) { +func listOverride(listCmd *cobra.Command, _ *apps.ListAppsRequest) { listCmd.Annotations["headerTemplate"] = cmdio.Heredoc(` {{header "Name"}} {{header "Url"}} {{header "ComputeStatus"}} {{header "DeploymentStatus"}}`) listCmd.Annotations["template"] = cmdio.Heredoc(` @@ -45,7 +45,7 @@ func listOverride(listCmd *cobra.Command, listReq *apps.ListAppsRequest) { tableview.RegisterConfig(listCmd, tableview.TableConfig{Columns: columns}) } -func listDeploymentsOverride(listDeploymentsCmd *cobra.Command, listDeploymentsReq *apps.ListAppDeploymentsRequest) { +func listDeploymentsOverride(listDeploymentsCmd *cobra.Command, _ *apps.ListAppDeploymentsRequest) { listDeploymentsCmd.Annotations["headerTemplate"] = cmdio.Heredoc(` {{header "DeploymentId"}} {{header "State"}} {{header "CreatedAt"}}`) listDeploymentsCmd.Annotations["template"] = cmdio.Heredoc(` diff --git a/cmd/workspace/catalogs/overrides.go b/cmd/workspace/catalogs/overrides.go index 46d66a08b2..d86af1eeea 100644 --- a/cmd/workspace/catalogs/overrides.go +++ b/cmd/workspace/catalogs/overrides.go @@ -7,7 +7,7 @@ import ( "github.com/spf13/cobra" ) -func listOverride(listCmd *cobra.Command, listReq *catalog.ListCatalogsRequest) { +func listOverride(listCmd *cobra.Command, _ *catalog.ListCatalogsRequest) { listCmd.Annotations["headerTemplate"] = cmdio.Heredoc(` {{header "Name"}} {{header "Type"}} {{header "Comment"}}`) listCmd.Annotations["template"] = cmdio.Heredoc(` diff --git a/cmd/workspace/external-locations/overrides.go b/cmd/workspace/external-locations/overrides.go index 9d9108f5be..607550da3a 100644 --- a/cmd/workspace/external-locations/overrides.go +++ b/cmd/workspace/external-locations/overrides.go @@ -7,7 +7,7 @@ import ( "github.com/spf13/cobra" ) -func listOverride(listCmd *cobra.Command, listReq *catalog.ListExternalLocationsRequest) { +func listOverride(listCmd *cobra.Command, _ *catalog.ListExternalLocationsRequest) { listCmd.Annotations["headerTemplate"] = cmdio.Heredoc(` {{header "Name"}} {{header "Credential"}} {{header "URL"}}`) listCmd.Annotations["template"] = cmdio.Heredoc(` diff --git a/cmd/workspace/jobs/overrides.go b/cmd/workspace/jobs/overrides.go index d9786601ba..d20efd6195 100644 --- a/cmd/workspace/jobs/overrides.go +++ b/cmd/workspace/jobs/overrides.go @@ -44,7 +44,7 @@ func listOverride(listCmd *cobra.Command, listReq *jobs.ListJobsRequest) { }) } -func listRunsOverride(listRunsCmd *cobra.Command, listRunsReq *jobs.ListRunsRequest) { +func listRunsOverride(listRunsCmd *cobra.Command, _ *jobs.ListRunsRequest) { listRunsCmd.Annotations["headerTemplate"] = cmdio.Heredoc(` {{header "Job ID"}} {{header "Run ID"}} {{header "Result State"}} URL`) listRunsCmd.Annotations["template"] = cmdio.Heredoc(` diff --git a/cmd/workspace/lakeview/overrides.go b/cmd/workspace/lakeview/overrides.go index 17953123a4..55357f703d 100644 --- a/cmd/workspace/lakeview/overrides.go +++ b/cmd/workspace/lakeview/overrides.go @@ -7,7 +7,7 @@ import ( "github.com/spf13/cobra" ) -func listOverride(listCmd *cobra.Command, listReq *dashboards.ListDashboardsRequest) { +func listOverride(listCmd *cobra.Command, _ *dashboards.ListDashboardsRequest) { listCmd.Annotations["headerTemplate"] = cmdio.Heredoc(` {{header "Dashboard ID"}} {{header "Name"}} {{header "State"}}`) listCmd.Annotations["template"] = cmdio.Heredoc(` diff --git a/cmd/workspace/pipelines/overrides.go b/cmd/workspace/pipelines/overrides.go index 64c7669440..a0d495f814 100644 --- a/cmd/workspace/pipelines/overrides.go +++ b/cmd/workspace/pipelines/overrides.go @@ -62,7 +62,7 @@ func listPipelinesOverride(listCmd *cobra.Command, listReq *pipelines.ListPipeli } } -func listPipelineEventsOverride(listCmd *cobra.Command, listReq *pipelines.ListPipelineEventsRequest) { +func listPipelineEventsOverride(listCmd *cobra.Command, _ *pipelines.ListPipelineEventsRequest) { listCmd.Annotations["headerTemplate"] = cmdio.Heredoc(` {{header "Timestamp"}} {{header "Level"}} {{header "Event Type"}} {{header "Message"}}`) listCmd.Annotations["template"] = cmdio.Heredoc(` diff --git a/cmd/workspace/repos/overrides.go b/cmd/workspace/repos/overrides.go index 85bdb1a6d6..65cc353dd7 100644 --- a/cmd/workspace/repos/overrides.go +++ b/cmd/workspace/repos/overrides.go @@ -17,7 +17,7 @@ import ( "github.com/spf13/cobra" ) -func listOverride(listCmd *cobra.Command, listReq *workspace.ListReposRequest) { +func listOverride(listCmd *cobra.Command, _ *workspace.ListReposRequest) { listCmd.Annotations["template"] = cmdio.Heredoc(` {{range .}}{{green "%d" .Id}} {{.Path}} {{.Branch|blue}} {{.Url|cyan}} {{end}}`) diff --git a/cmd/workspace/schemas/overrides.go b/cmd/workspace/schemas/overrides.go index 625c92f3d7..0e9b1b03b9 100644 --- a/cmd/workspace/schemas/overrides.go +++ b/cmd/workspace/schemas/overrides.go @@ -7,7 +7,7 @@ import ( "github.com/spf13/cobra" ) -func listOverride(listCmd *cobra.Command, listReq *catalog.ListSchemasRequest) { +func listOverride(listCmd *cobra.Command, _ *catalog.ListSchemasRequest) { listCmd.Annotations["headerTemplate"] = cmdio.Heredoc(` {{header "Full Name"}} {{header "Owner"}} {{header "Comment"}}`) listCmd.Annotations["template"] = cmdio.Heredoc(` diff --git a/cmd/workspace/tables/overrides.go b/cmd/workspace/tables/overrides.go index 157d62daf9..8e0987d469 100644 --- a/cmd/workspace/tables/overrides.go +++ b/cmd/workspace/tables/overrides.go @@ -7,7 +7,7 @@ import ( "github.com/spf13/cobra" ) -func listOverride(listCmd *cobra.Command, listReq *catalog.ListTablesRequest) { +func listOverride(listCmd *cobra.Command, _ *catalog.ListTablesRequest) { listCmd.Annotations["headerTemplate"] = cmdio.Heredoc(` {{header "Full Name"}} {{header "Table Type"}}`) listCmd.Annotations["template"] = cmdio.Heredoc(` diff --git a/cmd/workspace/volumes/overrides.go b/cmd/workspace/volumes/overrides.go index 0a4f645de3..66b946f2ea 100644 --- a/cmd/workspace/volumes/overrides.go +++ b/cmd/workspace/volumes/overrides.go @@ -7,7 +7,7 @@ import ( "github.com/spf13/cobra" ) -func listOverride(listCmd *cobra.Command, listReq *catalog.ListVolumesRequest) { +func listOverride(listCmd *cobra.Command, _ *catalog.ListVolumesRequest) { listCmd.Annotations["template"] = cmdio.Heredoc(` {{range .}}{{green "%s" .Name}} {{.VolumeType}} {{.FullName}} {{end}}`) diff --git a/cmd/workspace/warehouses/overrides.go b/cmd/workspace/warehouses/overrides.go index edc58ad681..14b2635a04 100644 --- a/cmd/workspace/warehouses/overrides.go +++ b/cmd/workspace/warehouses/overrides.go @@ -7,7 +7,7 @@ import ( "github.com/spf13/cobra" ) -func listOverride(listCmd *cobra.Command, listReq *sql.ListWarehousesRequest) { +func listOverride(listCmd *cobra.Command, _ *sql.ListWarehousesRequest) { listCmd.Annotations["headerTemplate"] = cmdio.Heredoc(` {{header "ID"}} {{header "Name"}} {{header "Size"}} {{header "State"}}`) listCmd.Annotations["template"] = cmdio.Heredoc(` From 96525f2856c6b3d2c39568c941bdae556de3bdc9 Mon Sep 17 00:00:00 2001 From: simon Date: Fri, 13 Mar 2026 16:01:41 +0100 Subject: [PATCH 4/7] Propagate iterator fetch errors from TUI model to command exit code Previously, RenderIterator and RunPaginated only returned the error from tea.Program.Run(), ignoring any fetch error stored in the model. An API error mid-stream would display an error screen in the TUI but the command would still exit 0. Now both functions inspect the final model via the new Err() accessor and return the fetch error if set. Also documents the destructive MaxWidth truncation behavior on ColumnDef and renderContent. --- libs/tableview/config.go | 7 +++++-- libs/tableview/paginated.go | 16 +++++++++++++++- libs/tableview/paginated_test.go | 8 ++++++++ 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/libs/tableview/config.go b/libs/tableview/config.go index c933f08e87..f6ba32a41a 100644 --- a/libs/tableview/config.go +++ b/libs/tableview/config.go @@ -4,8 +4,11 @@ import "context" // ColumnDef defines a column in the TUI table. type ColumnDef struct { - Header string // Display name in header row. - MaxWidth int // Max cell width; 0 = default (50). + Header string // Display name in header row. + // MaxWidth caps cell display width; 0 = default (50). Values exceeding + // this limit are destructively truncated with "..." in the rendered + // output. Horizontal scrolling does not recover the hidden portion. + MaxWidth int Extract func(v any) string // Extracts cell value from typed SDK struct. } diff --git a/libs/tableview/paginated.go b/libs/tableview/paginated.go index b72ac1ce01..615b010324 100644 --- a/libs/tableview/paginated.go +++ b/libs/tableview/paginated.go @@ -41,6 +41,10 @@ type searchDebounceMsg struct { seq int } +// PaginatedModel is the exported alias used by callers (e.g. RenderIterator) +// to inspect the final model returned by tea.Program.Run(). +type PaginatedModel = paginatedModel + type paginatedModel struct { cfg *TableConfig headers []string @@ -78,6 +82,11 @@ type paginatedModel struct { limitReached bool } +// Err returns the error recorded during data fetching, if any. +func (m paginatedModel) Err() error { + return m.err +} + // newFetchCmdFunc returns a closure that creates fetch commands, capturing ctx. func newFetchCmdFunc(ctx context.Context) func(paginatedModel) tea.Cmd { return func(m paginatedModel) tea.Cmd { @@ -275,7 +284,12 @@ func (m paginatedModel) renderContent() string { } fmt.Fprintln(tw, strings.Join(seps, "\t")) - // Data rows + // Data rows. + // NOTE: MaxWidth truncation here is destructive, not display wrapping. + // Values exceeding MaxWidth are cut and suffixed with "..." in the + // rendered output. Horizontal scrolling cannot recover the hidden tail. + // A future improvement could store full values and only truncate the + // visible slice, but that requires per-cell width tracking. for _, row := range m.rows { vals := make([]string, len(m.headers)) for i := range m.headers { diff --git a/libs/tableview/paginated_test.go b/libs/tableview/paginated_test.go index 0b683aef1b..081325feaf 100644 --- a/libs/tableview/paginated_test.go +++ b/libs/tableview/paginated_test.go @@ -116,6 +116,14 @@ func TestPaginatedFetchError(t *testing.T) { assert.Equal(t, "network error", pm.err.Error()) } +func TestPaginatedErrAccessor(t *testing.T) { + m := newTestModel(t, nil, 0) + assert.NoError(t, m.Err()) + + m.err = errors.New("api timeout") + assert.EqualError(t, m.Err(), "api timeout") +} + func TestPaginatedCursorMovement(t *testing.T) { m := newTestModel(t, nil, 0) m.ready = true From 108964b94a0a3fb3bfa3d256172aac2655e050e2 Mon Sep 17 00:00:00 2001 From: simon Date: Sat, 14 Mar 2026 22:53:28 +0100 Subject: [PATCH 5/7] Remove duplicate Err() method on paginatedModel --- libs/tableview/paginated.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/libs/tableview/paginated.go b/libs/tableview/paginated.go index 615b010324..d9a4b30a17 100644 --- a/libs/tableview/paginated.go +++ b/libs/tableview/paginated.go @@ -173,11 +173,6 @@ func RunPaginated(ctx context.Context, w io.Writer, cfg *TableConfig, iter RowIt return nil } -// Err returns any error that occurred during data fetching. -func (m paginatedModel) Err() error { - return m.err -} - func (m paginatedModel) Init() tea.Cmd { return m.makeFetchCmd(m) } From cda931b7bd78c7023060c0b11ecd8faf7ed04cbc Mon Sep 17 00:00:00 2001 From: simon Date: Mon, 16 Mar 2026 20:24:34 +0100 Subject: [PATCH 6/7] Sanitize pipeline event messages and increase MaxWidth Pipeline event messages can contain embedded newlines, carriage returns, and tabs that corrupt tab-delimited text output and TUI table rows. Add a `sanitize` template function to cmdio's renderFuncMap and use it in the text template. Also sanitize in the TUI Extract function. Increase MaxWidth from 60 to 200 so diagnostic payloads are not truncated destructively before the actionable part of the error. Co-authored-by: Isaac --- cmd/workspace/pipelines/overrides.go | 14 +++++++++++--- libs/cmdio/render.go | 11 ++++++++++- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/cmd/workspace/pipelines/overrides.go b/cmd/workspace/pipelines/overrides.go index a0d495f814..083c77c591 100644 --- a/cmd/workspace/pipelines/overrides.go +++ b/cmd/workspace/pipelines/overrides.go @@ -66,7 +66,7 @@ func listPipelineEventsOverride(listCmd *cobra.Command, _ *pipelines.ListPipelin listCmd.Annotations["headerTemplate"] = cmdio.Heredoc(` {{header "Timestamp"}} {{header "Level"}} {{header "Event Type"}} {{header "Message"}}`) listCmd.Annotations["template"] = cmdio.Heredoc(` - {{range .}}{{.Timestamp}} {{.Level}} {{.EventType}} {{.Message}} + {{range .}}{{.Timestamp}} {{.Level}} {{.EventType}} {{.Message | sanitize}} {{end}}`) columns := []tableview.ColumnDef{ @@ -79,8 +79,8 @@ func listPipelineEventsOverride(listCmd *cobra.Command, _ *pipelines.ListPipelin {Header: "Event Type", Extract: func(v any) string { return v.(pipelines.PipelineEvent).EventType }}, - {Header: "Message", MaxWidth: 60, Extract: func(v any) string { - return v.(pipelines.PipelineEvent).Message + {Header: "Message", MaxWidth: 200, Extract: func(v any) string { + return sanitizeWhitespace(v.(pipelines.PipelineEvent).Message) }}, } @@ -161,6 +161,14 @@ func disableSearchIfFilterSet(cmd *cobra.Command) { } } +var controlWhitespaceReplacer = strings.NewReplacer("\n", " ", "\r", " ", "\t", " ") + +// sanitizeWhitespace replaces control whitespace (newlines, tabs) with spaces +// to prevent corrupting tab-delimited or TUI table output. +func sanitizeWhitespace(s string) string { + return controlWhitespaceReplacer.Replace(s) +} + var uuidRegex = regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`) // looksLikeUUID checks if a string matches the UUID format with lowercase hex digits diff --git a/libs/cmdio/render.go b/libs/cmdio/render.go index f4289dbfd7..7a91f8ef8f 100644 --- a/libs/cmdio/render.go +++ b/libs/cmdio/render.go @@ -313,6 +313,14 @@ func RenderIteratorJson[T any](ctx context.Context, i listing.Iterator[T]) error return renderWithTemplate(ctx, newIteratorRenderer(i), c.outputFormat, c.out, c.headerTemplate, c.template) } +var controlWhitespaceReplacer = strings.NewReplacer("\n", " ", "\r", " ", "\t", " ") + +// sanitizeControlWhitespace replaces newlines and tabs with spaces to prevent +// corrupting tab-delimited text output. +func sanitizeControlWhitespace(s string) string { + return controlWhitespaceReplacer.Replace(s) +} + var renderFuncMap = template.FuncMap{ // we render colored output if stdout is TTY, otherwise we render text. // in the future we'll check if we can explicitly check for stderr being @@ -330,7 +338,8 @@ var renderFuncMap = template.FuncMap{ "italic": func(format string, a ...any) string { return color.New(color.Italic).Sprintf(format, a...) }, - "replace": strings.ReplaceAll, + "replace": strings.ReplaceAll, + "sanitize": sanitizeControlWhitespace, "join": strings.Join, "sub": func(a, b int) int { return a - b From 6388a6e5b41bf1e73285d50be8b563448b7b21f5 Mon Sep 17 00:00:00 2001 From: simon Date: Mon, 16 Mar 2026 20:24:43 +0100 Subject: [PATCH 7/7] Clean up PaginatedModel alias, duplicate test, and verbose comment Remove the PaginatedModel type alias (FinalModel interface suffices). Remove the duplicate TestPaginatedErrAccessor test that overlaps with TestPaginatedModelErr. Reduce the 5-line MaxWidth truncation comment to a single line. Co-authored-by: Isaac --- libs/tableview/paginated.go | 16 ++++------------ libs/tableview/paginated_test.go | 8 -------- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/libs/tableview/paginated.go b/libs/tableview/paginated.go index d9a4b30a17..b9d3be3f07 100644 --- a/libs/tableview/paginated.go +++ b/libs/tableview/paginated.go @@ -41,10 +41,6 @@ type searchDebounceMsg struct { seq int } -// PaginatedModel is the exported alias used by callers (e.g. RenderIterator) -// to inspect the final model returned by tea.Program.Run(). -type PaginatedModel = paginatedModel - type paginatedModel struct { cfg *TableConfig headers []string @@ -165,9 +161,9 @@ func RunPaginated(ctx context.Context, w io.Writer, cfg *TableConfig, iter RowIt if err != nil { return err } - if pm, ok := finalModel.(FinalModel); ok { - if modelErr := pm.Err(); modelErr != nil { - return modelErr + if m, ok := finalModel.(FinalModel); ok { + if fetchErr := m.Err(); fetchErr != nil { + return fetchErr } } return nil @@ -280,11 +276,7 @@ func (m paginatedModel) renderContent() string { fmt.Fprintln(tw, strings.Join(seps, "\t")) // Data rows. - // NOTE: MaxWidth truncation here is destructive, not display wrapping. - // Values exceeding MaxWidth are cut and suffixed with "..." in the - // rendered output. Horizontal scrolling cannot recover the hidden tail. - // A future improvement could store full values and only truncate the - // visible slice, but that requires per-cell width tracking. + // MaxWidth truncation is destructive; horizontal scroll won't recover hidden text. for _, row := range m.rows { vals := make([]string, len(m.headers)) for i := range m.headers { diff --git a/libs/tableview/paginated_test.go b/libs/tableview/paginated_test.go index 081325feaf..0b683aef1b 100644 --- a/libs/tableview/paginated_test.go +++ b/libs/tableview/paginated_test.go @@ -116,14 +116,6 @@ func TestPaginatedFetchError(t *testing.T) { assert.Equal(t, "network error", pm.err.Error()) } -func TestPaginatedErrAccessor(t *testing.T) { - m := newTestModel(t, nil, 0) - assert.NoError(t, m.Err()) - - m.err = errors.New("api timeout") - assert.EqualError(t, m.Err(), "api timeout") -} - func TestPaginatedCursorMovement(t *testing.T) { m := newTestModel(t, nil, 0) m.ready = true