diff --git a/CLAUDE.md b/CLAUDE.md index 4e7f11c..aced77e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,7 +13,7 @@ After each implementation run tests and pre-commit.sh. 2. `mark_fetch` `/patterns.md` — build commands, code style, workflow 3. `mark_fetch` `/guidelines.md` — hard rules for code quality, must read before writing code 4. Fetch other pages as needed: `/architecture.md`, `/debugging.md`, `/roadmap.md` -5. If MCP is unavailable, stop and ask the user before proceeding +5. If MCP is unavailable, stop and ask the developer before proceeding ### During Work diff --git a/client/cmd/demarkus-tui/graphview.go b/client/cmd/demarkus-tui/graphview.go index baf2dab..1e63436 100644 --- a/client/cmd/demarkus-tui/graphview.go +++ b/client/cmd/demarkus-tui/graphview.go @@ -6,7 +6,7 @@ import ( "sort" "strings" - tea "github.com/charmbracelet/bubbletea" + tea "charm.land/bubbletea/v2" "github.com/latebit/demarkus/client/internal/fetch" "github.com/latebit/demarkus/client/internal/graph" "github.com/latebit/demarkus/client/internal/graphstore" @@ -308,7 +308,7 @@ func renderTopologyView(items []graphListItem, selectedIdx, width int) string { } // handleGraphKey processes key events when the graph view is active. -func (m model) handleGraphKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { +func (m model) handleGraphKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { switch msg.String() { case "q": return m, tea.Quit diff --git a/client/cmd/demarkus-tui/links_test.go b/client/cmd/demarkus-tui/links_test.go index cb7e729..c1bfe64 100644 --- a/client/cmd/demarkus-tui/links_test.go +++ b/client/cmd/demarkus-tui/links_test.go @@ -7,6 +7,11 @@ import ( "github.com/latebit/demarkus/client/internal/links" ) +// osc8 wraps text in an OSC 8 hyperlink sequence, matching Glamour v2 output. +func osc8(url, text string) string { + return "\x1b]8;;" + url + "\x07" + text + "\x1b]8;;\x07" +} + func TestInjectLinkMarkers(t *testing.T) { tests := []struct { name string @@ -25,23 +30,23 @@ func TestInjectLinkMarkers(t *testing.T) { }, }, { - name: "single link gets markers", - body: "see hello rest", + name: "single link gets markers via OSC 8", + body: "see " + osc8("url.md", "hello") + " rest", infos: []links.LinkInfo{ {Dest: "url.md", Text: "hello"}, }, check: func(t *testing.T, result string) { - startM := string(markerStart(0)) - endM := string(markerEnd(0)) - want := "see " + startM + "hello" + endM + " rest" - if result != want { - t.Errorf("got %q, want %q", result, want) + if !strings.Contains(result, string(markerStart(0))) { + t.Error("missing start marker") + } + if !strings.Contains(result, string(markerEnd(0))) { + t.Error("missing end marker") } }, }, { name: "multiple links get unique markers", - body: "first and second end", + body: osc8("a.md", "first") + " and " + osc8("b.md", "second") + " end", infos: []links.LinkInfo{ {Dest: "a.md", Text: "first"}, {Dest: "b.md", Text: "second"}, @@ -62,17 +67,19 @@ func TestInjectLinkMarkers(t *testing.T) { }, }, { - name: "skips ANSI codes when matching", - body: "pre \x1b[35mhello\x1b[0m post", + name: "matches link not preceding plain text with same word", + body: "Hubs link to servers. " + osc8("hubs.md", "Hubs") + " list", infos: []links.LinkInfo{ - {Dest: "url.md", Text: "hello"}, + {Dest: "hubs.md", Text: "Hubs"}, }, check: func(t *testing.T, result string) { + // Marker should be inside the OSC 8 region, not on the plain "Hubs". if !strings.Contains(result, string(markerStart(0))) { t.Error("missing start marker") } - if !strings.Contains(result, string(markerEnd(0))) { - t.Error("missing end marker") + // The plain "Hubs" at position 0 should NOT have a marker before it. + if strings.HasPrefix(result, string(markerStart(0))) { + t.Error("marker incorrectly placed on plain text instead of hyperlink") } }, }, @@ -143,6 +150,20 @@ func TestFindVisibleText(t *testing.T) { wantStart: -1, wantEnd: -1, }, + { + name: "skips OSC 8 hyperlink with BEL terminator", + runes: "pre \x1b]8;;http://example.com\x07hello\x1b]8;;\x07 post", + text: "hello", + wantStart: 28, + wantEnd: 33, + }, + { + name: "skips OSC 8 hyperlink with ST terminator", + runes: "pre \x1b]8;;http://example.com\x1b\\hello\x1b]8;;\x1b\\ post", + text: "hello", + wantStart: 29, + wantEnd: 34, + }, } for _, tt := range tests { diff --git a/client/cmd/demarkus-tui/main.go b/client/cmd/demarkus-tui/main.go index b499eca..1fa5c99 100644 --- a/client/cmd/demarkus-tui/main.go +++ b/client/cmd/demarkus-tui/main.go @@ -4,14 +4,15 @@ import ( "flag" "fmt" "os" + "sort" "strings" "time" + "charm.land/bubbles/v2/textinput" + "charm.land/bubbles/v2/viewport" + tea "charm.land/bubbletea/v2" "charm.land/glamour/v2" - "github.com/charmbracelet/bubbles/textinput" - "github.com/charmbracelet/bubbles/viewport" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" + lipgloss "charm.land/lipgloss/v2" "github.com/latebit/demarkus/client/internal/bookmarks" "github.com/latebit/demarkus/client/internal/cache" "github.com/latebit/demarkus/client/internal/fetch" @@ -20,7 +21,6 @@ import ( "github.com/latebit/demarkus/client/internal/links" "github.com/latebit/demarkus/client/internal/tokens" "github.com/latebit/demarkus/protocol" - "github.com/muesli/termenv" "golang.org/x/term" ) @@ -260,10 +260,12 @@ func (m model) Init() tea.Cmd { func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { - case tea.KeyMsg: + case tea.KeyPressMsg: return m.handleKey(msg) - case tea.MouseMsg: - return m.handleMouse(msg) + case tea.MouseClickMsg: + return m.handleMouseClick(msg) + case tea.MouseMotionMsg: + return m.handleMouseMotion(msg) case tea.WindowSizeMsg: return m.handleWindowSize(msg) case crawlResult: @@ -281,13 +283,13 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } -func (m model) handleMouseHover(msg tea.MouseMsg) model { +func (m model) handleMouseHover(msg tea.MouseMotionMsg) model { if m.markedRendered == "" || m.showHelp || m.err != nil || m.status == "bookmarks" { return m } newHover := -1 if msg.Y >= 2 { - contentLine := msg.Y - 2 + m.viewport.YOffset + contentLine := msg.Y - 2 + m.viewport.YOffset() contentCol := msg.X for _, r := range m.linkRegions { if r.line == contentLine && contentCol >= r.startCol && contentCol < r.endCol { @@ -305,12 +307,20 @@ func (m model) handleMouseHover(msg tea.MouseMsg) model { return m } -func (m model) handleMouse(msg tea.MouseMsg) (tea.Model, tea.Cmd) { - if msg.Action == tea.MouseActionMotion && m.ready && m.viewMode == viewDocument { +func (m model) handleMouseMotion(msg tea.MouseMotionMsg) (tea.Model, tea.Cmd) { + if m.ready && m.viewMode == viewDocument { m = m.handleMouseHover(msg) } + if m.ready { + var cmd tea.Cmd + m.viewport, cmd = m.viewport.Update(msg) + return m, cmd + } + return m, nil +} - if msg.Action == tea.MouseActionPress && msg.Button == tea.MouseButtonLeft { +func (m model) handleMouseClick(msg tea.MouseClickMsg) (tea.Model, tea.Cmd) { + if msg.Button == tea.MouseLeft { if msg.Y == 0 { m.focus = focusAddressBar m.addressBar.Focus() @@ -318,7 +328,7 @@ func (m model) handleMouse(msg tea.MouseMsg) (tea.Model, tea.Cmd) { } if msg.Y >= 2 && m.ready && m.viewMode == viewDocument { // Check if click lands on a link. - contentLine := msg.Y - 2 + m.viewport.YOffset + contentLine := msg.Y - 2 + m.viewport.YOffset() contentCol := msg.X for _, r := range m.linkRegions { if r.line != contentLine || contentCol < r.startCol || contentCol >= r.endCol { @@ -356,25 +366,25 @@ func (m model) handleWindowSize(msg tea.WindowSizeMsg) (tea.Model, tea.Cmd) { viewportHeight := max(m.height-headerHeight-footerHeight, 1) if !m.ready { - m.viewport = viewport.New(m.width, viewportHeight) + m.viewport = viewport.New(viewport.WithWidth(m.width), viewport.WithHeight(viewportHeight)) m.ready = true // Defer pending content to a separate update cycle so the // viewport has a chance to fully initialise before receiving // content. Setting content in the same cycle as creation can // leave the viewport blank until the next event (e.g. scroll). if m.pendingBody != "" || m.err != nil { - m.addressBar.Width = m.width - 2 + m.addressBar.SetWidth(m.width - 2) return m, func() tea.Msg { return viewportReady{} } } } else { - m.viewport.Width = m.width - m.viewport.Height = viewportHeight + m.viewport.SetWidth(m.width) + m.viewport.SetHeight(viewportHeight) // Re-render graph view with new width for correct truncation. if m.viewMode == viewGraph && len(m.graphNodes) > 0 { m.viewport.SetContent(m.renderCurrentGraphSubView()) } } - m.addressBar.Width = m.width - 2 + m.addressBar.SetWidth(m.width - 2) return m, nil } @@ -504,13 +514,13 @@ func (m model) handleFetchResult(msg fetchResult) (tea.Model, tea.Cmd) { return m, nil } -func (m model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { - if msg.Type == tea.KeyCtrlC { +func (m model) handleKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { + if msg.Code == 'c' && msg.Mod == tea.ModCtrl { return m, tea.Quit } if m.focus == focusAddressBar { - switch msg.Type { + switch msg.Code { case tea.KeyEnter: raw := m.addressBar.Value() if raw != "" { @@ -537,7 +547,7 @@ func (m model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { return m.handleViewportKey(msg) } -func (m model) handleViewportKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { +func (m model) handleViewportKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { // Delegate to graph key handler when in graph view. if m.viewMode == viewGraph { return m.handleGraphKey(msg) @@ -622,7 +632,7 @@ func (m model) toggleFocus() model { return m } -func (m model) handleHelpDismiss(msg tea.KeyMsg) (tea.Model, tea.Cmd) { +func (m model) handleHelpDismiss(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { if msg.String() == "q" { return m, tea.Quit } @@ -652,10 +662,10 @@ func (m model) handleTabNavigation() (tea.Model, tea.Cmd) { if r.idx != m.linkIdx { continue } - if r.line < m.viewport.YOffset { + if r.line < m.viewport.YOffset() { m.viewport.SetYOffset(r.line) - } else if r.line >= m.viewport.YOffset+m.viewport.Height { - m.viewport.SetYOffset(r.line - m.viewport.Height + 1) + } else if r.line >= m.viewport.YOffset()+m.viewport.Height() { + m.viewport.SetYOffset(r.line - m.viewport.Height() + 1) } break } @@ -770,9 +780,12 @@ func (m model) handleBookmarkView() (tea.Model, tea.Cmd) { return m, nil } -func (m model) View() string { +func (m model) View() tea.View { if !m.ready { - return "Loading..." + v := tea.NewView("Loading...") + v.AltScreen = true + v.MouseMode = tea.MouseModeAllMotion + return v } var b strings.Builder @@ -798,7 +811,10 @@ func (m model) View() string { // Status bar. b.WriteString(m.statusBarView()) - return b.String() + v := tea.NewView(b.String()) + v.AltScreen = true + v.MouseMode = tea.MouseModeAllMotion + return v } func (m model) statusBarView() string { @@ -919,33 +935,49 @@ func injectLinkMarkers(rendered string, infos []links.LinkInfo) string { } runes := []rune(rendered) + hyperlinks := findHyperlinkRegions(runes) type insertion struct { runePos int marker string } var insertions []insertion - searchFrom := 0 + // Match each link to an OSC 8 hyperlink region by URL and visible text. + used := make([]bool, len(hyperlinks)) for i, info := range infos { if info.Text == "" { continue } - start, end := findVisibleText(runes, []rune(info.Text), searchFrom) - if start < 0 { - continue + for j, hl := range hyperlinks { + if used[j] { + continue + } + hlURL := hl.url + if idx := strings.IndexByte(hlURL, '#'); idx != -1 { + hlURL = hlURL[:idx] + } + urlMatch := hlURL == info.Dest || strings.HasSuffix(hlURL, info.Dest) || strings.HasSuffix(info.Dest, hlURL) + if urlMatch && hl.text == info.Text { + insertions = append(insertions, + insertion{runePos: hl.textStart, marker: string(markerStart(i))}, + insertion{runePos: hl.textEnd, marker: string(markerEnd(i))}, + ) + used[j] = true + break + } } - insertions = append(insertions, - insertion{runePos: start, marker: string(markerStart(i))}, - insertion{runePos: end, marker: string(markerEnd(i))}, - ) - searchFrom = end } if len(insertions) == 0 { return rendered } - // Build result with marker insertions (already sorted by position). + // Sort insertions by position (URL matching may find links out of order). + sort.Slice(insertions, func(a, b int) bool { + return insertions[a].runePos < insertions[b].runePos + }) + + // Build result with marker insertions. var result strings.Builder result.Grow(len(rendered) + len(insertions)*4) prev := 0 @@ -962,9 +994,110 @@ func injectLinkMarkers(rendered string, infos []links.LinkInfo) string { return result.String() } +// hyperlinkRegion describes an OSC 8 hyperlink in Glamour's rendered output. +type hyperlinkRegion struct { + url string + text string // visible text between the hyperlink start and reset + textStart int // rune index of the first visible char of the link text + textEnd int // rune index after the last visible char +} + +// parseOSC extracts the content of an OSC sequence starting at runes[start] +// (the character after \x1b]). Returns the content string and the rune index +// to resume scanning from (past the terminator). +func parseOSC(runes []rune, start int) (content string, resume int) { + end := start + for end < len(runes) { + if runes[end] == '\x07' { + break + } + if runes[end] == '\x1b' && end+1 < len(runes) && runes[end+1] == '\\' { + break + } + end++ + } + content = string(runes[start:end]) + if end < len(runes) && runes[end] == '\x07' { + return content, end + } + if end+1 < len(runes) { + return content, end + 1 + } + return content, end +} + +// textStartAfterOSC returns the rune index where visible text begins after an +// OSC terminator. resume is the value returned by parseOSC: the BEL index or +// the trailing '\' of an ST sequence. +func textStartAfterOSC(runes []rune, resume int) int { + if resume < len(runes) && runes[resume] == '\x07' { + return resume + 1 + } + return resume + 1 // parseOSC already advanced past \x1b +} + +// findHyperlinkRegions scans rendered ANSI output for OSC 8 hyperlink regions. +// Each region contains the URL, visible text, and rune positions. +func findHyperlinkRegions(runes []rune) []hyperlinkRegion { + var regions []hyperlinkRegion + var currentURL string + var visibleText strings.Builder + textStart := 0 + inCSI := false + inLink := false + + for i := 0; i < len(runes); i++ { + r := runes[i] + + if r == '\x1b' && i+1 < len(runes) && runes[i+1] == ']' { + oscContent, resume := parseOSC(runes, i+2) + escPos := i // position of \x1b before the OSC + + if strings.HasPrefix(oscContent, "8;") { + if _, url, ok := strings.Cut(oscContent[2:], ";"); ok { + if url == "" && currentURL != "" { + regions = append(regions, hyperlinkRegion{ + url: currentURL, + text: visibleText.String(), + textStart: textStart, + textEnd: escPos, + }) + currentURL = "" + inLink = false + visibleText.Reset() + } else if url != "" { + currentURL = url + inLink = true + visibleText.Reset() + textStart = textStartAfterOSC(runes, resume) + } + } + } + i = resume + continue + } + + if r == '\x1b' && i+1 < len(runes) && runes[i+1] == '[' { + inCSI = true + continue + } + if inCSI { + if r == 'm' { + inCSI = false + } + continue + } + + if inLink { + visibleText.WriteRune(r) + } + } + return regions +} + // findVisibleText searches for textRunes in runes starting from the given offset, -// skipping ANSI escape sequences during matching. Returns the rune indices -// (start, end) of the match or (-1, -1) if not found. +// skipping ANSI escape sequences (CSI and OSC) during matching. Returns the rune +// indices (start, end) of the match or (-1, -1) if not found. func findVisibleText(runes, textRunes []rune, from int) (startRune, endRune int) { if len(textRunes) == 0 { return -1, -1 @@ -976,16 +1109,32 @@ func findVisibleText(runes, textRunes []rune, from int) (startRune, endRune int) r rune } var visible []visChar - inEscape := false + inCSI := false + inOSC := false for i := from; i < len(runes); i++ { r := runes[i] - if r == '\x1b' && i+1 < len(runes) && runes[i+1] == '[' { - inEscape = true - continue + if r == '\x1b' && i+1 < len(runes) { + switch runes[i+1] { + case '[': + inCSI = true + continue + case ']': + inOSC = true + i++ // skip the ']' + continue + } } - if inEscape { + if inCSI { if r == 'm' { - inEscape = false + inCSI = false + } + continue + } + if inOSC { + if r == '\x07' { + inOSC = false + } else if r == '\\' && i > 0 && runes[i-1] == '\x1b' { + inOSC = false } continue } @@ -1014,6 +1163,47 @@ func isHighlighted(idx, selectedIdx, hoverIdx int) bool { return idx == hoverIdx || idx == selectedIdx } +// escapeState tracks whether the scanner is inside a CSI or OSC escape sequence. +type escapeState struct { + inCSI bool + inOSC bool +} + +// handleEscape checks if rune r (at index i in runes) is part of an ANSI escape +// sequence. If so, it writes the rune to result and returns true. Callers should +// skip all further processing for that rune. +func (es *escapeState) handleEscape(r rune, i int, runes []rune, result *strings.Builder) bool { + if r == '\x1b' && i+1 < len(runes) { + switch runes[i+1] { + case ']': + es.inOSC = true + result.WriteRune(r) + return true + case '[': + es.inCSI = true + result.WriteRune(r) + return true + } + } + if es.inOSC { + result.WriteRune(r) + if r == '\x07' { + es.inOSC = false + } else if r == '\\' && i > 0 && runes[i-1] == '\x1b' { + es.inOSC = false + } + return true + } + if es.inCSI { + result.WriteRune(r) + if r == 'm' { + es.inCSI = false + } + return true + } + return false +} + // processMarkers scans rendered ANSI output for link markers, records their // positions as linkRegions (including the URL glamour renders after the text), // and highlights links matching selectedIdx or hoverIdx with reverse video. @@ -1025,7 +1215,6 @@ func processMarkers(rendered string, selectedIdx, hoverIdx int) (string, []linkR lineNum := 0 visualCol := 0 - inEscape := false openIdx := -1 openCol := 0 @@ -1034,19 +1223,11 @@ func processMarkers(rendered string, selectedIdx, hoverIdx int) (string, []linkR extendingCol := 0 seenURLChar := false + var esc escapeState + runes := []rune(rendered) for i, r := range runes { - // ANSI escape sequence: \x1b[...m - if r == '\x1b' && i+1 < len(runes) && runes[i+1] == '[' { - inEscape = true - result.WriteRune(r) - continue - } - if inEscape { - result.WriteRune(r) - if r == 'm' { - inEscape = false - } + if esc.handleEscape(r, i, runes, &result) { continue } @@ -1161,7 +1342,7 @@ func detectStyle() string { if !term.IsTerminal(int(os.Stdin.Fd())) || !term.IsTerminal(int(os.Stdout.Fd())) { return "dark" } - if termenv.HasDarkBackground() { + if lipgloss.HasDarkBackground(os.Stdin, os.Stdout) { return "dark" } return "light" @@ -1196,8 +1377,6 @@ func main() { p := tea.NewProgram( initialModel(initialURL, client, styleName), - tea.WithAltScreen(), - tea.WithMouseAllMotion(), ) if _, err := p.Run(); err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) diff --git a/client/go.mod b/client/go.mod index 2c7bfe1..d862028 100644 --- a/client/go.mod +++ b/client/go.mod @@ -3,31 +3,27 @@ module github.com/latebit/demarkus/client go 1.26 require ( + charm.land/bubbles/v2 v2.1.0 + charm.land/bubbletea/v2 v2.0.2 charm.land/glamour/v2 v2.0.0 + charm.land/lipgloss/v2 v2.0.2 github.com/BurntSushi/toml v1.6.0 - github.com/charmbracelet/bubbles v1.0.0 - github.com/charmbracelet/bubbletea v1.3.10 - github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 github.com/latebit/demarkus/protocol v0.0.0 github.com/mark3labs/mcp-go v0.44.0 - github.com/muesli/termenv v0.16.0 github.com/quic-go/quic-go v0.59.0 github.com/yuin/goldmark v1.7.8 golang.org/x/term v0.34.0 ) require ( - charm.land/lipgloss/v2 v2.0.0 // indirect github.com/alecthomas/chroma/v2 v2.14.0 // indirect github.com/atotto/clipboard v0.1.4 // indirect - github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/charmbracelet/colorprofile v0.4.2 // indirect - github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 // indirect + github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect github.com/charmbracelet/x/ansi v0.11.6 // indirect - github.com/charmbracelet/x/cellbuf v0.0.15 // indirect github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect github.com/charmbracelet/x/term v0.2.2 // indirect github.com/charmbracelet/x/termios v0.1.1 // indirect @@ -35,17 +31,13 @@ require ( github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/dlclark/regexp2 v1.11.0 // indirect - github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/invopop/jsonschema v0.13.0 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.19 // indirect + github.com/mattn/go-runewidth v0.0.21 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect - github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/spf13/cast v1.7.1 // indirect @@ -55,8 +47,8 @@ require ( github.com/yuin/goldmark-emoji v1.0.5 // indirect golang.org/x/crypto v0.41.0 // indirect golang.org/x/net v0.43.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.41.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.42.0 // indirect golang.org/x/text v0.28.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/client/go.sum b/client/go.sum index 522fd8c..459e4b5 100644 --- a/client/go.sum +++ b/client/go.sum @@ -1,7 +1,11 @@ +charm.land/bubbles/v2 v2.1.0 h1:YSnNh5cPYlYjPxRrzs5VEn3vwhtEn3jVGRBT3M7/I0g= +charm.land/bubbles/v2 v2.1.0/go.mod h1:l97h4hym2hvWBVfmJDtrEHHCtkIKeTEb3TTJ4ZOB3wY= +charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0= +charm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ= charm.land/glamour/v2 v2.0.0 h1:IDBoqLEy7Hdpb9VOXN+khLP/XSxtJy1VsHuW/yF87+U= charm.land/glamour/v2 v2.0.0/go.mod h1:kjq9WB0s8vuUYZNYey2jp4Lgd9f4cKdzAw88FZtpj/w= -charm.land/lipgloss/v2 v2.0.0 h1:sd8N/B3x892oiOjFfBQdXBQp3cAkvjGaU5TvVZC3ivo= -charm.land/lipgloss/v2 v2.0.0/go.mod h1:w6SnmsBFBmEFBodiEDurGS/sdUY/u1+v72DqUzc6J14= +charm.land/lipgloss/v2 v2.0.2 h1:xFolbF8JdpNkM2cEPTfXEcW1p6NRzOWTSamRfYEw8cs= +charm.land/lipgloss/v2 v2.0.2/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= @@ -12,30 +16,20 @@ github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= -github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/aymanbagabas/go-udiff v0.4.0 h1:TKnLPh7IbnizJIBKFWa9mKayRUBQ9Kh1BPCk6w2PnYM= -github.com/aymanbagabas/go-udiff v0.4.0/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= +github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o= +github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= -github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc= -github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E= -github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= -github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY= github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8= -github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE= -github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA= -github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 h1:OqDqxQZliC7C8adA7KjelW3OjtAxREfeHkNcd66wpeI= -github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318/go.mod h1:Y6kE2GzHfkyQQVCSL9r2hwokSrIlHGzZG+71+wDYSZI= +github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA= +github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98= github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= -github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= -github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA= github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I= github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI= @@ -54,8 +48,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= @@ -79,20 +71,12 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mark3labs/mcp-go v0.44.0 h1:OlYfcVviAnwNN40QZUrrzU0QZjq3En7rCU5X09a/B7I= github.com/mark3labs/mcp-go v0.44.0/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= -github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= -github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= -github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w= +github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= -github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= -github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= @@ -124,12 +108,10 @@ golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=