From 65172fecd888b713141dc873c5ca44e2d444ca30 Mon Sep 17 00:00:00 2001 From: Ahmed Allam Date: Sat, 3 Oct 2026 20:12:09 +0000 Subject: [PATCH] fix(tui): link every wrapped line of the viewer URL to the full URL The sidebar wraps the viewer URL, and terminals that linkify by text only pick up the first line, so the click opened the viewer with a truncated token. Emit each wrapped line as an OSC 8 hyperlink to the whole URL. --- strix/interface/tui/internal/app/model_test.go | 8 ++++++++ strix/interface/tui/internal/app/view.go | 15 +++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/strix/interface/tui/internal/app/model_test.go b/strix/interface/tui/internal/app/model_test.go index 4a5c4564..40363b39 100644 --- a/strix/interface/tui/internal/app/model_test.go +++ b/strix/interface/tui/internal/app/model_test.go @@ -890,6 +890,14 @@ func TestRunningViewerShowsCompleteWrappedURL(t *testing.T) { if want := strings.Count(model.viewerView(model.viewerContentWidth()), "\n") + 3; model.viewerHeight() != want { t.Fatalf("viewer height = %d, want %d", model.viewerHeight(), want) } + + raw := model.viewerView(18) + if got, want := strings.Count(raw, ansi.SetHyperlink(url)), len(urlLines); got != want { + t.Fatalf("every wrapped line should link to the full URL: got %d links for %d lines in %q", got, want, raw) + } + if got := strings.Count(raw, ansi.ResetHyperlink()); got != len(urlLines) { + t.Fatalf("hyperlinks not closed: %d resets for %d lines", got, len(urlLines)) + } } func TestVerticalScrollbarThumbTracksScrollOffset(t *testing.T) { diff --git a/strix/interface/tui/internal/app/view.go b/strix/interface/tui/internal/app/view.go index 363bd6bd..495b2568 100644 --- a/strix/interface/tui/internal/app/view.go +++ b/strix/interface/tui/internal/app/view.go @@ -177,6 +177,18 @@ func wrapBlock(value string, width int) string { return strings.Join(out, "\n") } +// hyperlinkBlock wraps a URL to the column width and marks every wrapped line +// as an OSC 8 hyperlink to the whole URL. Terminals that linkify by text only +// see the first line of a wrapped URL, which for the viewer meant opening it +// with a truncated token. +func hyperlinkBlock(url string, width int, style lipgloss.Style) string { + lines := strings.Split(wrapBlock(url, width), "\n") + for i, line := range lines { + lines[i] = ansi.SetHyperlink(url) + style.Render(line) + ansi.ResetHyperlink() + } + return strings.Join(lines, "\n") +} + // scrollbarThumb brightens the bar being dragged so the grab reads as taking // hold of it. func (m Model) scrollbarThumb(target scrollbarTarget) lipgloss.Color { @@ -603,8 +615,7 @@ func (m Model) viewerView(width int) string { case "running": status := lipgloss.NewStyle().Foreground(green).Render("● Viewer running") if m.snapshot.ViewerURL != nil && strings.TrimSpace(*m.snapshot.ViewerURL) != "" { - url := wrapBlock(strings.TrimSpace(*m.snapshot.ViewerURL), width) - return status + "\n" + lipgloss.NewStyle().Foreground(dim).Render(url) + return status + "\n" + hyperlinkBlock(strings.TrimSpace(*m.snapshot.ViewerURL), width, lipgloss.NewStyle().Foreground(dim)) } return status case "unavailable":