From 65ff318ae3f3d88e8969c4e878578eba2a568336 Mon Sep 17 00:00:00 2001 From: lpf Date: Tue, 10 Mar 2026 18:28:32 +0800 Subject: [PATCH] add tui to no-channel build variant --- Makefile | 5 +- cmd/cli_common.go | 3 + cmd/cmd_tui.go | 1513 +++++++++++++++++++++++++++++++++++++++++++ cmd/cmd_tui_stub.go | 12 + cmd/main.go | 2 + go.mod | 22 +- go.sum | 112 ++-- 7 files changed, 1600 insertions(+), 69 deletions(-) create mode 100644 cmd/cmd_tui.go create mode 100644 cmd/cmd_tui_stub.go diff --git a/Makefile b/Makefile index 1a7bd20..d3468b6 100644 --- a/Makefile +++ b/Makefile @@ -39,6 +39,7 @@ empty:= space:=$(empty) $(empty) comma:=, ALL_CHANNEL_OMIT_TAGS=$(subst $(space),$(comma),$(addprefix omit_,$(CHANNELS))) +NOCHANNELS_TAGS=$(ALL_CHANNEL_OMIT_TAGS),with_tui # Installation INSTALL_PREFIX?=/usr/local @@ -123,7 +124,7 @@ build-variants: sync-embed-workspace tags=""; \ suffix=""; \ if [ "$$variant" = "none" ]; then \ - tags="$(ALL_CHANNEL_OMIT_TAGS)"; \ + tags="$(NOCHANNELS_TAGS)"; \ suffix="-nochannels"; \ elif [ "$$variant" != "full" ]; then \ for ch in $(CHANNELS); do \ @@ -192,7 +193,7 @@ build-all-variants: sync-embed-workspace tags=""; \ suffix=""; \ if [ "$$variant" = "none" ]; then \ - tags="$(ALL_CHANNEL_OMIT_TAGS)"; \ + tags="$(NOCHANNELS_TAGS)"; \ suffix="-nochannels"; \ elif [ "$$variant" != "full" ]; then \ for ch in $(CHANNELS); do \ diff --git a/cmd/cli_common.go b/cmd/cli_common.go index 745ae74..fa6dd5d 100644 --- a/cmd/cli_common.go +++ b/cmd/cli_common.go @@ -98,6 +98,9 @@ func printHelp() { fmt.Println(" channel Test and manage messaging channels") fmt.Println(" node Register remote node metadata and heartbeat") fmt.Println(" skills Manage skills (install, list, remove)") + if tuiEnabled { + fmt.Println(" tui Chat in terminal using the gateway chat API") + } fmt.Println(" uninstall Uninstall clawgo components") fmt.Println(" version Show version information") fmt.Println() diff --git a/cmd/cmd_tui.go b/cmd/cmd_tui.go new file mode 100644 index 0000000..c0ed5d8 --- /dev/null +++ b/cmd/cmd_tui.go @@ -0,0 +1,1513 @@ +//go:build with_tui + +package main + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "os" + "sort" + "strings" + "time" + + "github.com/YspCoder/clawgo/pkg/config" + "github.com/charmbracelet/bubbles/textinput" + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/gorilla/websocket" +) + +type tuiOptions struct { + baseURL string + token string + session string + noHistory bool +} + +var tuiEnabled = true + +type tuiMessage struct { + Role string `json:"role"` + Content string `json:"content"` + ToolCalls []interface{} `json:"tool_calls"` +} + +type tuiSession struct { + Key string `json:"key"` +} + +type tuiChatEntry struct { + Role string + Content string + Streaming bool +} + +type tuiPane struct { + ID int + Session string + Viewport viewport.Model + Entries []tuiChatEntry + Loading bool + Sending bool + ErrText string + Unread bool +} + +type tuiRuntime struct { + program *tea.Program +} + +func (r *tuiRuntime) send(msg tea.Msg) { + if r == nil || r.program == nil { + return + } + r.program.Send(msg) +} + +type tuiHistoryLoadedMsg struct { + PaneID int + Session string + Messages []tuiMessage + Err error +} + +type tuiSessionsLoadedMsg struct { + Sessions []tuiSession + Err error +} + +type tuiChatChunkMsg struct { + PaneID int + Delta string +} + +type tuiChatDoneMsg struct { + PaneID int +} + +type tuiChatErrorMsg struct { + PaneID int + Err error +} + +type tuiModel struct { + client *tuiClient + runtime *tuiRuntime + input textinput.Model + + width int + height int + bodyHeight int + sidebarWidth int + + panes []tuiPane + focus int + nextPaneID int + sessions []tuiSession + sessionCursor int + sessionFilter string + sessionActive map[string]time.Time + status string + globalErr string +} + +func tuiCmd() { + opts, err := parseTUIOptions(os.Args[2:]) + if err != nil { + fmt.Printf("Error: %v\n\n", err) + printTUIHelp() + os.Exit(1) + } + + client := &tuiClient{ + baseURL: strings.TrimRight(opts.baseURL, "/"), + token: strings.TrimSpace(opts.token), + http: &http.Client{ + Timeout: 20 * time.Second, + }, + } + if err := client.ping(); err != nil { + fmt.Printf("Error connecting to gateway: %v\n", err) + os.Exit(1) + } + + runtime := &tuiRuntime{} + model := newTUIModel(client, runtime, opts) + program := tea.NewProgram(model, tea.WithAltScreen()) + runtime.program = program + + if _, err := program.Run(); err != nil { + fmt.Printf("Error running TUI: %v\n", err) + os.Exit(1) + } +} + +func newTUIModel(client *tuiClient, runtime *tuiRuntime, opts tuiOptions) tuiModel { + input := textinput.New() + input.Placeholder = "Type a message for the focused pane or /help" + input.Focus() + input.CharLimit = 0 + input.Prompt = "› " + + pane := newTUIPane(1, opts.session) + if opts.noHistory { + pane.Entries = []tuiChatEntry{{Role: "system", Content: "History loading skipped. Start chatting."}} + } + + return tuiModel{ + client: client, + runtime: runtime, + input: input, + panes: []tuiPane{pane}, + focus: 0, + nextPaneID: 2, + sessionActive: map[string]time.Time{ + strings.TrimSpace(opts.session): time.Now(), + }, + status: "Tab/Shift+Tab: focus • Ctrl+O: open selected session • Ctrl+R: replace pane • Enter: send • Ctrl+C: quit", + } +} + +func newTUIPane(id int, session string) tuiPane { + vp := viewport.New(0, 0) + vp.SetContent("") + return tuiPane{ + ID: id, + Session: strings.TrimSpace(session), + Viewport: vp, + } +} + +func (m tuiModel) Init() tea.Cmd { + cmds := []tea.Cmd{textinput.Blink} + cmds = append(cmds, m.loadSessionsCmd()) + for _, pane := range m.panes { + if len(pane.Entries) == 0 { + cmds = append(cmds, m.loadHistoryCmd(pane.ID, pane.Session)) + } + } + return tea.Batch(cmds...) +} + +func (m tuiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.width = msg.Width + m.height = msg.Height + m.resize() + return m, nil + + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c": + return m, tea.Quit + case "ctrl+w": + if len(m.panes) <= 1 { + m.globalErr = "Cannot close the last pane" + return m, nil + } + closed := m.panes[m.focus].Session + m.panes = append(m.panes[:m.focus], m.panes[m.focus+1:]...) + if m.focus >= len(m.panes) { + m.focus = len(m.panes) - 1 + } + m.globalErr = "" + m.status = "Closed pane: " + closed + m.resize() + return m, nil + case "ctrl+l": + if len(m.panes) == 0 { + return m, nil + } + m.panes[m.focus].Loading = true + m.panes[m.focus].ErrText = "" + m.globalErr = "" + m.status = "Reloading history: " + m.panes[m.focus].Session + return m, m.loadHistoryCmd(m.panes[m.focus].ID, m.panes[m.focus].Session) + case "esc": + if strings.TrimSpace(m.sessionFilter) != "" { + m.sessionFilter = "" + filtered := m.filteredSessions() + if len(filtered) == 0 { + m.sessionCursor = 0 + } else if m.sessionCursor >= len(filtered) { + m.sessionCursor = len(filtered) - 1 + } + m.globalErr = "" + m.status = "Session filter cleared" + return m, nil + } + case "tab": + if len(m.panes) > 1 { + m.focus = (m.focus + 1) % len(m.panes) + m.panes[m.focus].Unread = false + m.markSessionActive(m.focusedPane().Session) + m.status = "Focused pane: " + m.focusedPane().Session + } + return m, nil + case "alt+left": + if len(m.panes) > 1 { + m.focus = (m.focus - 1 + len(m.panes)) % len(m.panes) + m.panes[m.focus].Unread = false + m.markSessionActive(m.focusedPane().Session) + m.status = "Focused pane: " + m.focusedPane().Session + } + return m, nil + case "alt+right": + if len(m.panes) > 1 { + m.focus = (m.focus + 1) % len(m.panes) + m.panes[m.focus].Unread = false + m.markSessionActive(m.focusedPane().Session) + m.status = "Focused pane: " + m.focusedPane().Session + } + return m, nil + case "up": + filtered := m.filteredSessions() + if len(filtered) > 0 && m.sessionCursor > 0 { + m.sessionCursor-- + m.status = "Selected session: " + filtered[m.sessionCursor].Key + } + return m, nil + case "down": + filtered := m.filteredSessions() + if len(filtered) > 0 && m.sessionCursor < len(filtered)-1 { + m.sessionCursor++ + m.status = "Selected session: " + filtered[m.sessionCursor].Key + } + return m, nil + case "shift+tab": + if len(m.panes) > 1 { + m.focus = (m.focus - 1 + len(m.panes)) % len(m.panes) + m.status = "Focused pane: " + m.focusedPane().Session + } + return m, nil + case "alt+1", "alt+2", "alt+3", "alt+4": + target := 0 + switch msg.String() { + case "alt+2": + target = 1 + case "alt+3": + target = 2 + case "alt+4": + target = 3 + } + if target < len(m.panes) { + m.focus = target + m.panes[m.focus].Unread = false + m.status = "Focused pane: " + m.focusedPane().Session + } + return m, nil + case "ctrl+o": + return m.openSelectedSessionInPane(false) + case "ctrl+n": + return m.openSelectedSessionInPane(false) + case "ctrl+r": + return m.openSelectedSessionInPane(true) + case "pgdown", "pgup", "home", "end": + if len(m.panes) > 0 { + var cmd tea.Cmd + pane := &m.panes[m.focus] + pane.Viewport, cmd = pane.Viewport.Update(msg) + return m, cmd + } + case "enter": + value := strings.TrimSpace(m.input.Value()) + if value == "" { + return m.openSelectedSessionInPane(false) + } + if len(m.panes) == 0 || m.focusedPane().Sending || m.focusedPane().Loading { + return m, nil + } + m.input.SetValue("") + if strings.HasPrefix(value, "/") { + return m.handleCommand(value) + } + return m.startSend(value) + } + case tuiHistoryLoadedMsg: + idx := m.findPane(msg.PaneID) + if idx < 0 { + return m, nil + } + pane := &m.panes[idx] + pane.Loading = false + if msg.Err != nil { + pane.ErrText = msg.Err.Error() + m.status = "History load failed for " + pane.Session + return m, nil + } + pane.Session = msg.Session + m.markSessionActive(pane.Session) + pane.Entries = historyEntries(msg.Messages) + if len(pane.Entries) == 0 { + pane.Entries = []tuiChatEntry{{Role: "system", Content: "No history in this session yet."}} + } + pane.ErrText = "" + m.status = "Session loaded: " + pane.Session + m.syncPaneViewport(idx, true) + return m, nil + case tuiSessionsLoadedMsg: + if msg.Err != nil { + m.globalErr = msg.Err.Error() + return m, nil + } + m.sessions = filterTUISessions(msg.Sessions) + if len(m.sessions) == 0 { + m.sessions = []tuiSession{{Key: "main"}} + } + filtered := m.filteredSessions() + if len(filtered) == 0 { + m.sessionCursor = 0 + } else if m.sessionCursor >= len(filtered) { + m.sessionCursor = len(filtered) - 1 + } + keys := make([]string, 0, len(msg.Sessions)) + for _, session := range m.sessions { + if strings.TrimSpace(session.Key) != "" { + keys = append(keys, session.Key) + } + } + m.appendSystemToFocused("Sessions: " + strings.Join(keys, ", ")) + m.globalErr = "" + m.status = fmt.Sprintf("%d sessions", len(keys)) + return m, nil + case tuiChatChunkMsg: + idx := m.findPane(msg.PaneID) + if idx < 0 { + return m, nil + } + pane := &m.panes[idx] + if len(pane.Entries) == 0 || !pane.Entries[len(pane.Entries)-1].Streaming { + pane.Entries = append(pane.Entries, tuiChatEntry{Role: "assistant", Streaming: true}) + } + pane.Entries[len(pane.Entries)-1].Content += msg.Delta + if idx != m.focus { + pane.Unread = true + } + m.markSessionActive(pane.Session) + m.syncPaneViewport(idx, true) + return m, nil + case tuiChatDoneMsg: + idx := m.findPane(msg.PaneID) + if idx < 0 { + return m, nil + } + pane := &m.panes[idx] + pane.Sending = false + if len(pane.Entries) > 0 && pane.Entries[len(pane.Entries)-1].Streaming { + pane.Entries[len(pane.Entries)-1].Streaming = false + } + pane.ErrText = "" + if idx == m.focus { + pane.Unread = false + } + m.markSessionActive(pane.Session) + m.status = "Reply complete: " + pane.Session + m.syncPaneViewport(idx, true) + return m, nil + case tuiChatErrorMsg: + idx := m.findPane(msg.PaneID) + if idx < 0 { + return m, nil + } + pane := &m.panes[idx] + pane.Sending = false + if len(pane.Entries) > 0 && pane.Entries[len(pane.Entries)-1].Streaming { + pane.Entries[len(pane.Entries)-1].Streaming = false + } + pane.ErrText = msg.Err.Error() + m.status = "Reply failed: " + pane.Session + m.syncPaneViewport(idx, true) + return m, nil + } + + var cmd tea.Cmd + m.input, cmd = m.input.Update(msg) + return m, cmd +} + +func (m tuiModel) View() string { + if m.width == 0 || m.height == 0 { + return "Loading..." + } + banner := renderTUIBanner(m.width) + focusSession := "" + if len(m.panes) > 0 { + focusSession = m.focusedPane().Session + } + header := tuiHeaderStyle.Width(m.width).Render( + lipgloss.JoinVertical(lipgloss.Left, + banner, + tuiMutedStyle.Render(fmt.Sprintf("Focus %s • Panes %d • %s", focusSession, len(m.panes), compactGatewayLabel(m.client.baseURL))), + ), + ) + body := m.renderPanes() + input := tuiInputBoxStyle.Width(m.width).Render(m.input.View()) + footerText := m.status + if m.globalErr != "" { + footerText = "Error: " + m.globalErr + } + footerStyle := tuiFooterStyle + if m.globalErr != "" { + footerStyle = tuiErrorStyle + } + footerLeft := footerStyle.Render(footerText) + footerRight := tuiFooterHintStyle.Render("Enter send • Ctrl+N open • Ctrl+W close • Alt+1..4 focus") + footer := footerStyle.Width(m.width).Render( + lipgloss.JoinHorizontal( + lipgloss.Top, + footerLeft, + strings.Repeat(" ", max(1, m.width-lipgloss.Width(footerLeft)-lipgloss.Width(footerRight)-2)), + footerRight, + ), + ) + return lipgloss.JoinVertical(lipgloss.Left, header, body, input, footer) +} + +func (m *tuiModel) resize() { + headerHeight := lipgloss.Height( + tuiHeaderStyle.Width(max(m.width, 20)).Render( + lipgloss.JoinVertical(lipgloss.Left, + renderTUIBanner(max(m.width, 20)), + tuiMutedStyle.Render(fmt.Sprintf("Gateway %s", m.client.baseURL)), + ), + ), + ) + inputHeight := 3 + footerHeight := 2 + bodyHeight := m.height - headerHeight - inputHeight - footerHeight + if bodyHeight < 6 { + bodyHeight = 6 + } + m.bodyHeight = bodyHeight + m.sidebarWidth = min(28, max(20, m.width/5)) + availablePaneWidth := m.width - m.sidebarWidth - 1 + if availablePaneWidth < 24 { + availablePaneWidth = 24 + } + switch len(m.panes) { + case 0: + case 1: + m.setPaneSize(0, availablePaneWidth, bodyHeight) + case 2: + leftWidth := (availablePaneWidth - 1) / 2 + rightWidth := availablePaneWidth - 1 - leftWidth + m.setPaneSize(0, leftWidth, bodyHeight) + m.setPaneSize(1, rightWidth, bodyHeight) + case 3: + leftWidth := (availablePaneWidth - 1) / 2 + rightWidth := availablePaneWidth - 1 - leftWidth + topHeight := (bodyHeight - 1) / 2 + bottomHeight := bodyHeight - 1 - topHeight + m.setPaneSize(0, leftWidth, bodyHeight) + m.setPaneSize(1, rightWidth, topHeight) + m.setPaneSize(2, rightWidth, bottomHeight) + default: + leftWidth := (availablePaneWidth - 1) / 2 + rightWidth := availablePaneWidth - 1 - leftWidth + topHeight := (bodyHeight - 1) / 2 + bottomHeight := bodyHeight - 1 - topHeight + m.setPaneSize(0, leftWidth, topHeight) + m.setPaneSize(1, rightWidth, topHeight) + m.setPaneSize(2, leftWidth, bottomHeight) + m.setPaneSize(3, rightWidth, bottomHeight) + } + m.input.Width = max(10, m.width-6) +} + +func (m *tuiModel) renderPanes() string { + sidebar := m.renderSessionSidebar() + panes := m.renderPaneGrid() + return lipgloss.JoinHorizontal(lipgloss.Top, sidebar, tuiPaneGapStyle.Render(" "), panes) +} + +func (m *tuiModel) renderPane(index int, pane tuiPane) string { + titleStyle := tuiPaneTitleStyle + borderStyle := tuiPaneStyle + if index == m.focus { + titleStyle = tuiPaneTitleActiveStyle + borderStyle = tuiPaneActiveStyle + } + statusBadge := "" + switch { + case pane.Sending: + statusBadge = tuiPaneReplyingStyle.Render(" replying ") + case pane.Loading: + statusBadge = tuiPaneLoadingStyle.Render(" loading ") + case pane.ErrText != "": + statusBadge = tuiPaneErrorBadgeStyle.Render(" error ") + } + titleText := fmt.Sprintf("[%d] %s", index+1, pane.Session) + if pane.Unread { + titleText += " •" + } + title := titleStyle.Render(titleText) + if index == m.focus { + title = tuiPaneTitleActiveBadgeStyle.Render(" " + titleText + " ") + } + meta := []string{} + if pane.Loading { + meta = append(meta, "loading") + } + if pane.Sending { + meta = append(meta, "replying") + } + if pane.ErrText != "" { + meta = append(meta, "error") + } + header := title + if statusBadge != "" { + header = lipgloss.JoinHorizontal(lipgloss.Left, header, " ", statusBadge) + } + if len(meta) > 0 { + header = lipgloss.JoinHorizontal(lipgloss.Left, title, tuiMutedStyle.Render(" "+strings.Join(meta, " • "))) + if statusBadge != "" { + header = lipgloss.JoinHorizontal(lipgloss.Left, title, " ", statusBadge, tuiMutedStyle.Render(" "+strings.Join(meta, " • "))) + } + } + content := pane.Viewport.View() + if pane.ErrText != "" { + content += "\n\n" + tuiErrorStyle.Render("Error: "+pane.ErrText) + } + view := lipgloss.JoinVertical(lipgloss.Left, header, content) + return borderStyle.Width(pane.Viewport.Width + 4).Height(pane.Viewport.Height + 4).Render(view) +} + +func (m *tuiModel) syncPaneViewport(index int, toBottom bool) { + if index < 0 || index >= len(m.panes) { + return + } + pane := &m.panes[index] + pane.Viewport.SetContent(renderEntries(pane.Entries, pane.Viewport.Width)) + if toBottom { + pane.Viewport.GotoBottom() + } +} + +func (m *tuiModel) renderSessionSidebar() string { + lines := []string{tuiPaneTitleStyle.Render("Sessions")} + filtered := m.filteredSessions() + if len(filtered) == 0 { + lines = append(lines, tuiMutedStyle.Render("No sessions")) + } else { + for i, session := range filtered { + style := tuiSidebarItemStyle + prefix := " " + suffix := m.sidebarSessionMarker(session.Key) + if i == m.sessionCursor { + style = tuiSidebarItemActiveStyle + prefix = "› " + } + lines = append(lines, style.Width(max(8, m.sidebarWidth-4)).Render(prefix+session.Key+suffix)) + } + } + lines = append(lines, "") + if strings.TrimSpace(m.sessionFilter) != "" { + lines = append(lines, tuiMutedStyle.Render("Filter: "+m.sessionFilter)) + } + lines = append(lines, tuiMutedStyle.Render("Up/Down: select")) + lines = append(lines, tuiMutedStyle.Render("Enter: open selected")) + lines = append(lines, tuiMutedStyle.Render("Ctrl+O: open pane")) + lines = append(lines, tuiMutedStyle.Render("Ctrl+N: new pane")) + lines = append(lines, tuiMutedStyle.Render("Ctrl+R: replace pane")) + lines = append(lines, tuiMutedStyle.Render("Ctrl+L: reload pane")) + lines = append(lines, tuiMutedStyle.Render("Ctrl+W: close pane")) + lines = append(lines, tuiMutedStyle.Render("Alt+←/→: cycle pane")) + lines = append(lines, tuiMutedStyle.Render("Alt+1..4: focus pane")) + lines = append(lines, tuiMutedStyle.Render("Esc: clear filter")) + lines = append(lines, tuiMutedStyle.Render("/filter : filter")) + return tuiSidebarStyle.Width(m.sidebarWidth).Height(m.bodyHeight).Render(strings.Join(lines, "\n")) +} + +func (m *tuiModel) renderPaneGrid() string { + if len(m.panes) == 0 { + return tuiViewportStyle.Width(max(20, m.width-m.sidebarWidth-1)).Height(m.bodyHeight).Render("No panes.") + } + switch len(m.panes) { + case 1: + return m.renderPane(0, m.panes[0]) + case 2: + return lipgloss.JoinHorizontal(lipgloss.Top, + m.renderPane(0, m.panes[0]), + tuiPaneGapStyle.Render(" "), + m.renderPane(1, m.panes[1]), + ) + case 3: + right := lipgloss.JoinVertical(lipgloss.Left, + m.renderPane(1, m.panes[1]), + tuiPaneGapStyle.Render(" "), + m.renderPane(2, m.panes[2]), + ) + return lipgloss.JoinHorizontal(lipgloss.Top, + m.renderPane(0, m.panes[0]), + tuiPaneGapStyle.Render(" "), + right, + ) + default: + top := lipgloss.JoinHorizontal(lipgloss.Top, + m.renderPane(0, m.panes[0]), + tuiPaneGapStyle.Render(" "), + m.renderPane(1, m.panes[1]), + ) + bottom := lipgloss.JoinHorizontal(lipgloss.Top, + m.renderPane(2, m.panes[2]), + tuiPaneGapStyle.Render(" "), + m.renderPane(3, m.panes[3]), + ) + return lipgloss.JoinVertical(lipgloss.Left, + top, + tuiPaneGapStyle.Render(" "), + bottom, + ) + } +} + +func (m tuiModel) loadHistoryCmd(paneID int, session string) tea.Cmd { + return func() tea.Msg { + messages, err := m.client.history(context.Background(), session) + return tuiHistoryLoadedMsg{PaneID: paneID, Session: session, Messages: messages, Err: err} + } +} + +func (m tuiModel) loadSessionsCmd() tea.Cmd { + return func() tea.Msg { + sessions, err := m.client.sessions(context.Background()) + return tuiSessionsLoadedMsg{Sessions: sessions, Err: err} + } +} + +func (m tuiModel) startSend(content string) (tea.Model, tea.Cmd) { + if len(m.panes) == 0 { + return m, nil + } + pane := &m.panes[m.focus] + pane.Entries = append(pane.Entries, + tuiChatEntry{Role: "user", Content: content}, + tuiChatEntry{Role: "assistant", Streaming: true}, + ) + pane.Sending = true + pane.ErrText = "" + m.markSessionActive(pane.Session) + m.status = "Waiting for reply: " + pane.Session + m.syncPaneViewport(m.focus, true) + return m, m.streamChatCmd(pane.ID, pane.Session, content) +} + +func (m tuiModel) handleCommand(input string) (tea.Model, tea.Cmd) { + fields := strings.Fields(input) + cmd := strings.ToLower(strings.TrimPrefix(fields[0], "/")) + switch cmd { + case "quit", "exit": + return m, tea.Quit + case "help": + m.appendSystemToFocused("Commands: /help, /sessions, /split , /close, /session , /focus , /history, /clear, /filter , /quit") + m.status = "Help shown" + return m, nil + case "sessions": + m.status = "Loading sessions..." + return m, m.loadSessionsCmd() + case "history": + if len(m.panes) == 0 { + return m, nil + } + m.panes[m.focus].Loading = true + m.status = "Loading history..." + return m, m.loadHistoryCmd(m.panes[m.focus].ID, m.panes[m.focus].Session) + case "clear": + if len(m.panes) == 0 { + return m, nil + } + m.panes[m.focus].Entries = nil + m.panes[m.focus].ErrText = "" + m.status = "Transcript cleared: " + m.panes[m.focus].Session + m.syncPaneViewport(m.focus, true) + return m, nil + case "session": + if len(fields) < 2 || len(m.panes) == 0 { + m.globalErr = "/session requires a session name" + return m, nil + } + m.panes[m.focus].Session = strings.TrimSpace(fields[1]) + m.markSessionActive(m.panes[m.focus].Session) + m.panes[m.focus].Loading = true + m.status = "Switched pane to " + m.panes[m.focus].Session + return m, m.loadHistoryCmd(m.panes[m.focus].ID, m.panes[m.focus].Session) + case "split", "new": + if len(fields) < 2 { + m.globalErr = fmt.Sprintf("/%s requires a session name", cmd) + return m, nil + } + if len(m.panes) >= 4 { + m.globalErr = "Maximum 4 panes" + return m, nil + } + session := strings.TrimSpace(fields[1]) + pane := newTUIPane(m.nextPaneID, session) + m.nextPaneID++ + pane.Loading = true + m.panes = append(m.panes, pane) + m.focus = len(m.panes) - 1 + m.globalErr = "" + m.status = "Opened pane: " + session + m.resize() + return m, tea.Batch(m.loadHistoryCmd(pane.ID, pane.Session), m.loadSessionsCmd()) + case "close": + if len(m.panes) <= 1 { + m.globalErr = "Cannot close the last pane" + return m, nil + } + closed := m.panes[m.focus].Session + m.panes = append(m.panes[:m.focus], m.panes[m.focus+1:]...) + if m.focus >= len(m.panes) { + m.focus = len(m.panes) - 1 + } + m.globalErr = "" + m.status = "Closed pane: " + closed + m.resize() + return m, nil + case "focus": + if len(fields) < 2 { + m.globalErr = "/focus requires a pane number" + return m, nil + } + var n int + if _, err := fmt.Sscanf(fields[1], "%d", &n); err != nil || n < 1 || n > len(m.panes) { + m.globalErr = "Invalid pane number" + return m, nil + } + m.focus = n - 1 + m.panes[m.focus].Unread = false + m.markSessionActive(m.panes[m.focus].Session) + m.globalErr = "" + m.status = "Focused pane: " + m.panes[m.focus].Session + return m, nil + case "filter": + value := "" + if len(fields) > 1 { + value = strings.TrimSpace(strings.Join(fields[1:], " ")) + } + m.sessionFilter = value + filtered := m.filteredSessions() + if len(filtered) == 0 { + m.sessionCursor = 0 + m.globalErr = "No matching sessions" + } else { + m.sessionCursor = 0 + m.globalErr = "" + m.status = fmt.Sprintf("Filtered sessions: %d", len(filtered)) + } + return m, nil + default: + m.globalErr = "Unknown command: " + input + return m, nil + } +} + +func (m tuiModel) streamChatCmd(paneID int, session, content string) tea.Cmd { + client := m.client + runtime := m.runtime + return func() tea.Msg { + go func() { + err := client.streamChat(context.Background(), session, content, func(msg tea.Msg) { + switch typed := msg.(type) { + case tuiChatChunkMsg: + typed.PaneID = paneID + runtime.send(typed) + default: + runtime.send(msg) + } + }) + if err != nil { + runtime.send(tuiChatErrorMsg{PaneID: paneID, Err: err}) + } + }() + return nil + } +} + +func (m *tuiModel) appendSystemToFocused(content string) { + if len(m.panes) == 0 { + return + } + m.panes[m.focus].Entries = append(m.panes[m.focus].Entries, tuiChatEntry{Role: "system", Content: content}) + m.syncPaneViewport(m.focus, true) +} + +func (m *tuiModel) setPaneSize(index, width, height int) { + if index < 0 || index >= len(m.panes) { + return + } + if width < 24 { + width = 24 + } + if height < 6 { + height = 6 + } + m.panes[index].Viewport.Width = width - 4 + m.panes[index].Viewport.Height = height - 4 + if m.panes[index].Viewport.Height < 3 { + m.panes[index].Viewport.Height = 3 + } + m.syncPaneViewport(index, false) +} + +func (m *tuiModel) hasPaneSession(session string) bool { + needle := strings.TrimSpace(session) + if needle == "" { + return false + } + for i := range m.panes { + if strings.TrimSpace(m.panes[i].Session) == needle { + return true + } + } + return false +} + +func (m *tuiModel) hasUnreadSession(session string) bool { + needle := strings.TrimSpace(session) + if needle == "" { + return false + } + for i := range m.panes { + if strings.TrimSpace(m.panes[i].Session) == needle && m.panes[i].Unread { + return true + } + } + return false +} + +func (m *tuiModel) sidebarSessionMarker(session string) string { + focused := m.focusedPane() != nil && strings.TrimSpace(m.focusedPane().Session) == strings.TrimSpace(session) + open := m.hasPaneSession(session) + unread := m.hasUnreadSession(session) + switch { + case focused && unread: + return " ◉•" + case focused: + return " ◉" + case unread: + return " •" + case open: + return " ●" + default: + return "" + } +} + +func (m tuiModel) openSelectedSessionInPane(replace bool) (tea.Model, tea.Cmd) { + filtered := m.filteredSessions() + if len(filtered) == 0 { + m.globalErr = "No sessions available" + return m, nil + } + if m.sessionCursor >= len(filtered) { + m.sessionCursor = len(filtered) - 1 + } + session := strings.TrimSpace(filtered[m.sessionCursor].Key) + if session == "" { + m.globalErr = "Invalid session" + return m, nil + } + if idx := m.findPaneBySession(session); idx >= 0 && !replace { + m.focus = idx + m.panes[m.focus].Unread = false + m.markSessionActive(session) + m.globalErr = "" + m.status = "Focused pane: " + session + return m, nil + } + if replace { + if len(m.panes) == 0 { + return m, nil + } + m.panes[m.focus].Session = session + m.markSessionActive(session) + m.panes[m.focus].Loading = true + m.panes[m.focus].ErrText = "" + m.status = "Replaced pane with " + session + return m, m.loadHistoryCmd(m.panes[m.focus].ID, session) + } + if len(m.panes) >= 4 { + m.globalErr = "Maximum 4 panes" + return m, nil + } + pane := newTUIPane(m.nextPaneID, session) + m.nextPaneID++ + pane.Loading = true + m.panes = append(m.panes, pane) + m.focus = len(m.panes) - 1 + m.markSessionActive(session) + m.globalErr = "" + m.status = "Opened pane: " + session + m.resize() + return m, m.loadHistoryCmd(pane.ID, session) +} + +func (m *tuiModel) findPane(id int) int { + for i := range m.panes { + if m.panes[i].ID == id { + return i + } + } + return -1 +} + +func (m *tuiModel) findPaneBySession(session string) int { + needle := strings.TrimSpace(session) + for i := range m.panes { + if strings.TrimSpace(m.panes[i].Session) == needle { + return i + } + } + return -1 +} + +func (m *tuiModel) filteredSessions() []tuiSession { + out := make([]tuiSession, 0, len(m.sessions)) + if strings.TrimSpace(m.sessionFilter) == "" { + out = append(out, m.sessions...) + } else { + query := strings.ToLower(strings.TrimSpace(m.sessionFilter)) + for _, session := range m.sessions { + if strings.Contains(strings.ToLower(session.Key), query) { + out = append(out, session) + } + } + } + sort.SliceStable(out, func(i, j int) bool { + iOpen := m.hasPaneSession(out[i].Key) + jOpen := m.hasPaneSession(out[j].Key) + if iOpen != jOpen { + return iOpen + } + iActive := m.sessionActiveAt(out[i].Key) + jActive := m.sessionActiveAt(out[j].Key) + if !iActive.Equal(jActive) { + return iActive.After(jActive) + } + return strings.ToLower(out[i].Key) < strings.ToLower(out[j].Key) + }) + return out +} + +func (m *tuiModel) markSessionActive(session string) { + key := strings.TrimSpace(session) + if key == "" { + return + } + if m.sessionActive == nil { + m.sessionActive = map[string]time.Time{} + } + m.sessionActive[key] = time.Now() +} + +func (m *tuiModel) sessionActiveAt(session string) time.Time { + if m.sessionActive == nil { + return time.Time{} + } + return m.sessionActive[strings.TrimSpace(session)] +} + +func (m *tuiModel) focusedPane() *tuiPane { + if len(m.panes) == 0 { + return nil + } + return &m.panes[m.focus] +} + +func parseTUIOptions(args []string) (tuiOptions, error) { + opts := tuiOptions{session: "main"} + cfg, err := loadConfig() + if err == nil { + opts.baseURL = gatewayBaseURLFromConfig(cfg) + opts.token = strings.TrimSpace(cfg.Gateway.Token) + } + for i := 0; i < len(args); i++ { + switch args[i] { + case "--help", "-h": + printTUIHelp() + os.Exit(0) + case "--gateway": + if i+1 >= len(args) { + return opts, fmt.Errorf("--gateway requires a value") + } + opts.baseURL = strings.TrimSpace(args[i+1]) + i++ + case "--token": + if i+1 >= len(args) { + return opts, fmt.Errorf("--token requires a value") + } + opts.token = strings.TrimSpace(args[i+1]) + i++ + case "--session", "-s": + if i+1 >= len(args) { + return opts, fmt.Errorf("--session requires a value") + } + opts.session = strings.TrimSpace(args[i+1]) + i++ + case "--no-history": + opts.noHistory = true + default: + return opts, fmt.Errorf("unknown option: %s", args[i]) + } + } + if strings.TrimSpace(opts.baseURL) == "" { + opts.baseURL = "http://127.0.0.1:8080" + } + if !strings.HasPrefix(opts.baseURL, "http://") && !strings.HasPrefix(opts.baseURL, "https://") { + opts.baseURL = "http://" + opts.baseURL + } + if strings.TrimSpace(opts.session) == "" { + opts.session = "main" + } + return opts, nil +} + +func printTUIHelp() { + fmt.Println("Usage: clawgo tui [options]") + fmt.Println() + fmt.Println("Options:") + fmt.Println(" --gateway Gateway base URL, for example http://127.0.0.1:8080") + fmt.Println(" --token Gateway token") + fmt.Println(" --session, -s Initial session key (default: main)") + fmt.Println(" --no-history Skip loading session history on startup") + fmt.Println() + fmt.Println("In TUI:") + fmt.Println(" /split Open a new pane for another session") + fmt.Println(" /close Close focused pane") + fmt.Println(" /focus Focus pane number") +} + +func gatewayBaseURLFromConfig(cfg *config.Config) string { + host := strings.TrimSpace(cfg.Gateway.Host) + switch host { + case "", "0.0.0.0", "::", "[::]": + host = "127.0.0.1" + } + return fmt.Sprintf("http://%s:%d", host, cfg.Gateway.Port) +} + +func historyEntries(messages []tuiMessage) []tuiChatEntry { + out := make([]tuiChatEntry, 0, len(messages)) + for _, message := range messages { + content := strings.TrimSpace(message.Content) + if content == "" && len(message.ToolCalls) > 0 { + content = fmt.Sprintf("[tool calls: %d]", len(message.ToolCalls)) + } + if content == "" { + continue + } + out = append(out, tuiChatEntry{Role: strings.ToLower(strings.TrimSpace(message.Role)), Content: content}) + } + return out +} + +func renderEntries(entries []tuiChatEntry, width int) string { + if len(entries) == 0 { + return tuiMutedStyle.Render("No messages yet.") + } + parts := make([]string, 0, len(entries)) + for _, entry := range entries { + label := tuiRoleLabel(entry.Role) + content := entry.Content + if strings.TrimSpace(content) == "" && entry.Streaming { + content = "..." + } + style := tuiContentStyle + switch entry.Role { + case "user": + style = tuiUserStyle + case "assistant": + style = tuiAssistantStyle + case "system": + style = tuiSystemStyle + case "tool": + style = tuiToolStyle + } + if width > 8 { + style = style.Width(width - 2) + } + if entry.Streaming { + content += " ▌" + } + parts = append(parts, lipgloss.JoinVertical(lipgloss.Left, tuiLabelStyle.Render(label), style.Render(content))) + } + return strings.Join(parts, "\n\n") +} + +func tuiRoleLabel(role string) string { + switch role { + case "user": + return "You" + case "system": + return "System" + case "tool": + return "Tool" + default: + return "Assistant" + } +} + +type tuiClient struct { + baseURL string + token string + http *http.Client +} + +func (c *tuiClient) ping() error { + ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second) + defer cancel() + req, err := c.newRequest(ctx, http.MethodGet, "/webui/api/version", nil) + if err != nil { + return err + } + resp, err := c.http.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode >= 300 { + body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048)) + return fmt.Errorf("%s", strings.TrimSpace(string(body))) + } + return nil +} + +func (c *tuiClient) history(ctx context.Context, session string) ([]tuiMessage, error) { + req, err := c.newRequest(ctx, http.MethodGet, "/webui/api/chat/history?session="+url.QueryEscape(session), nil) + if err != nil { + return nil, err + } + resp, err := c.http.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode >= 300 { + body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) + return nil, fmt.Errorf("%s", strings.TrimSpace(string(body))) + } + var payload struct { + Messages []tuiMessage `json:"messages"` + } + if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil { + return nil, err + } + return payload.Messages, nil +} + +func (c *tuiClient) sessions(ctx context.Context) ([]tuiSession, error) { + req, err := c.newRequest(ctx, http.MethodGet, "/webui/api/sessions", nil) + if err != nil { + return nil, err + } + resp, err := c.http.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode >= 300 { + body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) + return nil, fmt.Errorf("%s", strings.TrimSpace(string(body))) + } + var payload struct { + Sessions []tuiSession `json:"sessions"` + } + if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil { + return nil, err + } + return payload.Sessions, nil +} + +func (c *tuiClient) streamChat(ctx context.Context, session, content string, send func(tea.Msg)) error { + conn, err := c.openChatSocket(ctx) + if err != nil { + return err + } + defer conn.Close() + if err := conn.WriteJSON(map[string]string{"session": session, "message": content}); err != nil { + return err + } + for { + var frame struct { + Type string `json:"type"` + Delta string `json:"delta"` + Error string `json:"error"` + } + if err := conn.ReadJSON(&frame); err != nil { + return err + } + switch frame.Type { + case "chat_chunk": + send(tuiChatChunkMsg{Delta: frame.Delta}) + case "chat_done": + send(tuiChatDoneMsg{}) + return nil + case "chat_error": + return fmt.Errorf("%s", strings.TrimSpace(frame.Error)) + } + } +} + +func (c *tuiClient) openChatSocket(ctx context.Context) (*websocket.Conn, error) { + wsURL, err := c.websocketURL("/webui/api/chat/live") + if err != nil { + return nil, err + } + header := http.Header{} + if c.token != "" { + header.Set("Authorization", "Bearer "+c.token) + } + conn, resp, err := websocket.DefaultDialer.DialContext(ctx, wsURL, header) + if err != nil { + if resp != nil && resp.Body != nil { + defer resp.Body.Close() + body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048)) + if strings.TrimSpace(string(body)) != "" { + return nil, fmt.Errorf("%s", strings.TrimSpace(string(body))) + } + } + return nil, err + } + return conn, nil +} + +func (c *tuiClient) websocketURL(path string) (string, error) { + u, err := url.Parse(c.baseURL) + if err != nil { + return "", err + } + switch u.Scheme { + case "http": + u.Scheme = "ws" + case "https": + u.Scheme = "wss" + default: + return "", fmt.Errorf("unsupported gateway scheme: %s", u.Scheme) + } + u.Path = path + q := u.Query() + if c.token != "" { + q.Set("token", c.token) + } + u.RawQuery = q.Encode() + return u.String(), nil +} + +func (c *tuiClient) newRequest(ctx context.Context, method, path string, body io.Reader) (*http.Request, error) { + req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, body) + if err != nil { + return nil, err + } + if c.token != "" { + req.Header.Set("Authorization", "Bearer "+c.token) + } + return req, nil +} + +var ( + tuiHeaderStyle = lipgloss.NewStyle(). + Padding(0, 1). + BorderBottom(true). + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("240")) + + tuiTitleStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("212")) + + tuiBannerClawStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("69")) + + tuiBannerGoStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("204")) + + tuiMutedStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("245")) + + tuiInputBoxStyle = lipgloss.NewStyle(). + Padding(0, 1). + BorderTop(true). + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("240")) + + tuiViewportStyle = lipgloss.NewStyle().Padding(0, 1) + + tuiFooterStyle = lipgloss.NewStyle(). + Padding(0, 1). + Foreground(lipgloss.Color("245")) + + tuiErrorStyle = lipgloss.NewStyle(). + Padding(0, 1). + Foreground(lipgloss.Color("203")) + + tuiPaneStyle = lipgloss.NewStyle(). + Padding(0, 1). + BorderStyle(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("240")) + + tuiPaneActiveStyle = lipgloss.NewStyle(). + Padding(0, 1). + BorderStyle(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("69")) + + tuiPaneTitleStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("252")) + + tuiPaneTitleActiveStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("69")) + + tuiPaneTitleActiveBadgeStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("16")). + Background(lipgloss.Color("69")) + + tuiPaneGapStyle = lipgloss.NewStyle() + + tuiSidebarStyle = lipgloss.NewStyle(). + Padding(0, 1). + BorderStyle(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("240")) + + tuiSidebarItemStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("252")) + + tuiSidebarItemActiveStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("69")) + tuiFooterHintStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("241")) + + tuiPaneReplyingStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("230")). + Background(lipgloss.Color("62")) + + tuiPaneLoadingStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("16")). + Background(lipgloss.Color("221")) + + tuiPaneErrorBadgeStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("255")). + Background(lipgloss.Color("160")) + + tuiLabelStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("249")) + + tuiContentStyle = lipgloss.NewStyle().PaddingLeft(1) + tuiUserStyle = lipgloss.NewStyle().PaddingLeft(1).Foreground(lipgloss.Color("87")) + tuiAssistantStyle = lipgloss.NewStyle(). + PaddingLeft(1). + Foreground(lipgloss.Color("230")) + tuiSystemStyle = lipgloss.NewStyle().PaddingLeft(1).Foreground(lipgloss.Color("221")) + tuiToolStyle = lipgloss.NewStyle().PaddingLeft(1).Foreground(lipgloss.Color("141")) +) + +func renderTUIBanner(width int) string { + claw := strings.Join([]string{ + " ██████╗██╗ █████╗ ██╗ ██╗", + "██╔════╝██║ ██╔══██╗██║ ██║", + "██║ ██║ ███████║██║ █╗ ██║", + "██║ ██║ ██╔══██║██║███╗██║", + "╚██████╗███████╗██║ ██║╚███╔███╔╝", + " ╚═════╝╚══════╝╚═╝ ╚═╝ ╚══╝╚══╝ ", + }, "\n") + goText := strings.Join([]string{ + " ██████╗ ██████╗ ", + "██╔════╝ ██╔═══██╗", + "██║ ███╗██║ ██║", + "██║ ██║██║ ██║", + "╚██████╔╝╚██████╔╝", + " ╚═════╝ ╚═════╝ ", + }, "\n") + left := strings.Split(claw, "\n") + right := strings.Split(goText, "\n") + lines := make([]string, 0, len(left)) + for i := range left { + lines = append(lines, tuiBannerClawStyle.Render(left[i])+" "+tuiBannerGoStyle.Render(right[i])) + } + full := strings.Join(lines, "\n") + if width <= 0 || lipgloss.Width(full) <= width-2 { + return full + } + compactLines := []string{ + tuiBannerClawStyle.Render("██████╗██╗ █████╗ ██╗ ██╗") + " " + tuiBannerGoStyle.Render(" ██████╗ ██████╗ "), + tuiBannerClawStyle.Render("██╔═══╝██║ ██╔══██╗██║ █╗ ██║") + " " + tuiBannerGoStyle.Render("██╔════╝ ██╔═══██╗"), + tuiBannerClawStyle.Render("██║ ██║ ███████║██║███╗██║") + " " + tuiBannerGoStyle.Render("██║ ███╗██║ ██║"), + tuiBannerClawStyle.Render("╚██████╗███████╗██║ ██║╚███╔███╔╝") + " " + tuiBannerGoStyle.Render("╚██████╔╝╚██████╔╝"), + } + compact := strings.Join(compactLines, "\n") + if lipgloss.Width(compact) <= width-2 { + return compact + } + short := tuiBannerClawStyle.Render("Claw") + tuiBannerGoStyle.Render("Go") + if lipgloss.Width(short) <= width-2 { + return short + } + return tuiTitleStyle.Render("ClawGo") +} + +func compactGatewayLabel(raw string) string { + text := strings.TrimSpace(raw) + text = strings.TrimPrefix(text, "http://") + text = strings.TrimPrefix(text, "https://") + return text +} + +func intersperse(items []string, sep string) []string { + if len(items) <= 1 { + return items + } + out := make([]string, 0, len(items)*2-1) + for i, item := range items { + if i > 0 { + out = append(out, sep) + } + out = append(out, item) + } + return out +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func filterTUISessions(items []tuiSession) []tuiSession { + out := make([]tuiSession, 0, len(items)) + seen := map[string]struct{}{} + for _, item := range items { + key := strings.TrimSpace(item.Key) + if key == "" { + continue + } + if _, ok := seen[key]; ok { + continue + } + seen[key] = struct{}{} + out = append(out, tuiSession{Key: key}) + } + return out +} diff --git a/cmd/cmd_tui_stub.go b/cmd/cmd_tui_stub.go new file mode 100644 index 0000000..d65b1cd --- /dev/null +++ b/cmd/cmd_tui_stub.go @@ -0,0 +1,12 @@ +//go:build !with_tui + +package main + +import "fmt" + +var tuiEnabled = false + +func tuiCmd() { + fmt.Println("TUI is not included in this build.") + fmt.Println("Install the no-channel variant to use `clawgo tui`.") +} diff --git a/cmd/main.go b/cmd/main.go index 0babb61..a949d65 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -71,6 +71,8 @@ func main() { nodeCmd() case "skills": skillsCmd() + case "tui": + tuiCmd() case "version", "--version", "-v": fmt.Printf("%s clawgo v%s\n", logo, version) case "uninstall": diff --git a/go.mod b/go.mod index 1e634bd..b61dc0e 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,9 @@ go 1.25.7 require ( github.com/bwmarrin/discordgo v0.29.0 github.com/caarlos0/env/v11 v11.4.0 + github.com/charmbracelet/bubbles v1.0.0 + github.com/charmbracelet/bubbletea v1.3.10 + github.com/charmbracelet/lipgloss v1.1.0 github.com/chzyer/readline v1.5.1 github.com/gorilla/websocket v1.5.3 github.com/larksuite/oapi-sdk-go/v3 v3.5.3 @@ -26,22 +29,38 @@ require ( require ( filippo.io/edwards25519 v1.2.0 // indirect github.com/andybalholm/brotli v1.2.0 // indirect + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beeper/argo-go v1.1.2 // indirect github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/sonic v1.15.0 // indirect github.com/bytedance/sonic/loader v0.5.0 // indirect + github.com/charmbracelet/colorprofile v0.4.1 // indirect + github.com/charmbracelet/x/ansi v0.11.6 // indirect + github.com/charmbracelet/x/cellbuf v0.0.15 // indirect + github.com/charmbracelet/x/term v0.2.2 // indirect + github.com/clipperhouse/displaywidth v0.9.0 // indirect + github.com/clipperhouse/stringish v0.1.1 // indirect + github.com/clipperhouse/uax29/v2 v2.5.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/coder/websocket v1.8.14 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/go-resty/resty/v2 v2.17.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grbit/go-json v0.11.0 // indirect github.com/klauspost/compress v1.18.4 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mattn/go-colorable v0.1.14 // 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/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect github.com/petermattis/goid v0.0.0-20260226131333-17d1149c6ac6 // indirect github.com/pion/datachannel v1.6.0 // indirect @@ -57,10 +76,10 @@ require ( github.com/pion/sdp/v3 v3.0.18 // indirect github.com/pion/srtp/v3 v3.0.10 // indirect github.com/pion/stun/v3 v3.1.1 // indirect - github.com/pion/transport/v3 v3.1.1 // indirect github.com/pion/transport/v4 v4.0.1 // indirect github.com/pion/turn/v4 v4.1.4 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/rs/zerolog v1.34.0 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.2.0 // indirect @@ -71,6 +90,7 @@ require ( github.com/valyala/fastjson v1.6.10 // indirect github.com/vektah/gqlparser/v2 v2.5.32 // indirect github.com/wlynxg/anet v0.0.5 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.mau.fi/libsignal v0.2.1 // indirect go.mau.fi/util v0.9.6 // indirect golang.org/x/arch v0.25.0 // indirect diff --git a/go.sum b/go.sum index 93b4132..3fd50d8 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,4 @@ cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= -filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= -filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= @@ -11,6 +9,10 @@ github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNg github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +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/beeper/argo-go v1.1.2 h1:UQI2G8F+NLfGTOmTUI0254pGKx/HUU/etbUGTJv91Fs= github.com/beeper/argo-go v1.1.2/go.mod h1:M+LJAnyowKVQ6Rdj6XYGEn+qcVFkb3R/MUpqkGR0hM4= github.com/bwmarrin/discordgo v0.29.0 h1:FmWeXFaKUwrcL3Cx65c20bTRW+vOb6k8AnaP+EgjDno= @@ -25,12 +27,32 @@ github.com/caarlos0/env/v11 v11.4.0 h1:Kcb6t5kIIr4XkoQC9AF2j+8E1Jsrl3Wz/hhm1LtoG github.com/caarlos0/env/v11 v11.4.0/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +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.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk= +github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +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/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= +github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA= +github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA= +github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= +github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= +github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U= +github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= @@ -45,12 +67,12 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg= github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo= +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/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w= github.com/go-resty/resty/v2 v2.6.0/go.mod h1:PwvJS6hvaPkjtjNg9ph+VrSD92bi5Zq73w/BIH7cC3Q= -github.com/go-resty/resty/v2 v2.17.1 h1:x3aMpHK1YM9e4va/TMDRlusDDoZiQ+ViDu/WpA6xTM4= -github.com/go-resty/resty/v2 v2.17.1/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA= github.com/go-resty/resty/v2 v2.17.2 h1:FQW5oHYcIlkCNrMD2lloGScxcHJ0gkjshV3qcQAyHQk= github.com/go-resty/resty/v2 v2.17.2/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= @@ -90,12 +112,8 @@ github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyf github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= -github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= -github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= -github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -106,6 +124,8 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/larksuite/oapi-sdk-go/v3 v3.5.3 h1:xvf8Dv29kBXC5/DNDCLhHkAFW8l/0LlQJimO5Zn+JUk= github.com/larksuite/oapi-sdk-go/v3 v3.5.3/go.mod h1:ZEplY+kwuIrj/nqw5uSCINNATcH3KdxSN7y+UxYY5fI= +github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= +github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= @@ -113,12 +133,20 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk= github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mdp/qrterminal/v3 v3.2.1 h1:6+yQjiiOsSuXT5n9/m60E54vdgFsw0zhADHhHLrFet4= github.com/mdp/qrterminal/v3 v3.2.1/go.mod h1:jOTmXvnBsMy5xqLniO0R++Jmjs2sTm9dFSuQ5kpz/SU= -github.com/mymmrac/telego v1.6.0 h1:Zc8rgyHozvd/7ZgyrigyHdAF9koHYMfilYfyB6wlFC0= -github.com/mymmrac/telego v1.6.0/go.mod h1:xt6ZWA8zi8KmuzryE1ImEdl9JSwjHNpM4yhC7D8hU4Y= +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/mymmrac/telego v1.7.0 h1:yRO/l00tFGG4nY66ufUKb4ARqv7qx9+LsjQv/b0NEyo= github.com/mymmrac/telego v1.7.0/go.mod h1:pdLV346EgVuq7Xrh3kMggeBiazeHhsdEoK0RTEOPXRM= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= @@ -133,70 +161,40 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.1 h1:Lb/Uzkiw2Ugt2Xf03J5wmv81PdkYOiWbI8CNBi1boC8= github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.1/go.mod h1:ln3IqPYYocZbYvl9TAOrG/cxGR9xcn4pnZRLdCTEGEU= -github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 h1:KPpdlQLZcHfTMQRi6bFQ7ogNO0ltFT4PmtwTLW4W+14= -github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/petermattis/goid v0.0.0-20260226131333-17d1149c6ac6 h1:rh2lKw/P/EqHa724vYH2+VVQ1YnW4u6EOXl0PMAovZE= github.com/petermattis/goid v0.0.0-20260226131333-17d1149c6ac6/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= -github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= -github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M= github.com/pion/datachannel v1.6.0 h1:XecBlj+cvsxhAMZWFfFcPyUaDZtd7IJvrXqlXD/53i0= github.com/pion/datachannel v1.6.0/go.mod h1:ur+wzYF8mWdC+Mkis5Thosk+u/VOL287apDNEbFpsIk= -github.com/pion/dtls/v3 v3.0.7 h1:bItXtTYYhZwkPFk4t1n3Kkf5TDrfj6+4wG+CZR8uI9Q= -github.com/pion/dtls/v3 v3.0.7/go.mod h1:uDlH5VPrgOQIw59irKYkMudSFprY9IEFCqz/eTz16f8= github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc= github.com/pion/dtls/v3 v3.1.2/go.mod h1:Hw/igcX4pdY69z1Hgv5x7wJFrUkdgHwAn/Q/uo7YHRo= -github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4= -github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= github.com/pion/ice/v4 v4.2.1 h1:XPRYXaLiFq3LFDG7a7bMrmr3mFr27G/gtXN3v/TVfxY= github.com/pion/ice/v4 v4.2.1/go.mod h1:2quLV1S5v1tAx3VvAJaH//KGitRXvo4RKlX6D3tnN+c= -github.com/pion/interceptor v0.1.41 h1:NpvX3HgWIukTf2yTBVjVGFXtpSpWgXjqz7IIpu7NsOw= -github.com/pion/interceptor v0.1.41/go.mod h1:nEt4187unvRXJFyjiw00GKo+kIuXMWQI9K89fsosDLY= github.com/pion/interceptor v0.1.44 h1:sNlZwM8dWXU9JQAkJh8xrarC0Etn8Oolcniukmuy0/I= github.com/pion/interceptor v0.1.44/go.mod h1:4atVlBkcgXuUP+ykQF0qOCGU2j7pQzX2ofvPRFsY5RY= github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= -github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM= -github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA= github.com/pion/mdns/v2 v2.1.0 h1:3IJ9+Xio6tWYjhN6WwuY142P/1jA0D5ERaIqawg/fOY= github.com/pion/mdns/v2 v2.1.0/go.mod h1:pcez23GdynwcfRU1977qKU0mDxSeucttSHbCSfFOd9A= github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= -github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo= -github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0= github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo= github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo= -github.com/pion/rtp v1.8.23 h1:kxX3bN4nM97DPrVBGq5I/Xcl332HnTHeP1Swx3/MCnU= -github.com/pion/rtp v1.8.23/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM= github.com/pion/rtp v1.10.1 h1:xP1prZcCTUuhO2c83XtxyOHJteISg6o8iPsE2acaMtA= github.com/pion/rtp v1.10.1/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM= -github.com/pion/sctp v1.8.40 h1:bqbgWYOrUhsYItEnRObUYZuzvOMsVplS3oNgzedBlG8= -github.com/pion/sctp v1.8.40/go.mod h1:SPBBUENXE6ThkEksN5ZavfAhFYll+h+66ZiG6IZQuzo= github.com/pion/sctp v1.9.2 h1:HxsOzEV9pWoeggv7T5kewVkstFNcGvhMPx0GvUOUQXo= github.com/pion/sctp v1.9.2/go.mod h1:OTOlsQ5EDQ6mQ0z4MUGXt2CgQmKyafBEXhUVqLRB6G8= -github.com/pion/sdp/v3 v3.0.16 h1:0dKzYO6gTAvuLaAKQkC02eCPjMIi4NuAr/ibAwrGDCo= -github.com/pion/sdp/v3 v3.0.16/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo= github.com/pion/sdp/v3 v3.0.18 h1:l0bAXazKHpepazVdp+tPYnrsy9dfh7ZbT8DxesH5ZnI= github.com/pion/sdp/v3 v3.0.18/go.mod h1:ZREGo6A9ZygQ9XkqAj5xYCQtQpif0i6Pa81HOiAdqQ8= -github.com/pion/srtp/v3 v3.0.8 h1:RjRrjcIeQsilPzxvdaElN0CpuQZdMvcl9VZ5UY9suUM= -github.com/pion/srtp/v3 v3.0.8/go.mod h1:2Sq6YnDH7/UDCvkSoHSDNDeyBcFgWL0sAVycVbAsXFg= github.com/pion/srtp/v3 v3.0.10 h1:tFirkpBb3XccP5VEXLi50GqXhv5SKPxqrdlhDCJlZrQ= github.com/pion/srtp/v3 v3.0.10/go.mod h1:3mOTIB0cq9qlbn59V4ozvv9ClW/BSEbRp4cY0VtaR7M= -github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw= -github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU= github.com/pion/stun/v3 v3.1.1 h1:CkQxveJ4xGQjulGSROXbXq94TAWu8gIX2dT+ePhUkqw= github.com/pion/stun/v3 v3.1.1/go.mod h1:qC1DfmcCTQjl9PBaMa5wSn3x9IPmKxSdcCsxBcDBndM= -github.com/pion/transport/v3 v3.0.8 h1:oI3myyYnTKUSTthu/NZZ8eu2I5sHbxbUNNFW62olaYc= -github.com/pion/transport/v3 v3.0.8/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ= github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM= github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ= github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o= github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM= -github.com/pion/turn/v4 v4.1.1 h1:9UnY2HB99tpDyz3cVVZguSxcqkJ1DsTSZ+8TGruh4fc= -github.com/pion/turn/v4 v4.1.1/go.mod h1:2123tHk1O++vmjI5VSD0awT50NywDAq5A2NNNU4Jjs8= github.com/pion/turn/v4 v4.1.4 h1:EU11yMXKIsK43FhcUnjLlrhE4nboHZq+TXBIi3QpcxQ= github.com/pion/turn/v4 v4.1.4/go.mod h1:ES1DXVFKnOhuDkqn9hn5VJlSWmZPaRJLyBXoOeO/BmQ= -github.com/pion/webrtc/v4 v4.1.6 h1:srHH2HwvCGwPba25EYJgUzgLqCQoXl1VCUnrGQMSzUw= -github.com/pion/webrtc/v4 v4.1.6/go.mod h1:wKecGRlkl3ox/As/MYghJL+b/cVXMEhoPMJWPuGQFhU= github.com/pion/webrtc/v4 v4.2.9 h1:DZIh1HAhPIL3RvwEDFsmL5hfPSLEpxsQk9/Jir2vkJE= github.com/pion/webrtc/v4 v4.2.9/go.mod h1:9EmLZve0H76eTzf8v2FmchZ6tcBXtDgpfTEu+drW6SY= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= @@ -205,6 +203,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= @@ -243,16 +243,14 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI= github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw= -github.com/valyala/fastjson v1.6.7 h1:ZE4tRy0CIkh+qDc5McjatheGX2czdn8slQjomexVpBM= -github.com/valyala/fastjson v1.6.7/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4= github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE= -github.com/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTdwFp0s= -github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= github.com/vektah/gqlparser/v2 v2.5.32 h1:k9QPJd4sEDTL+qB4ncPLflqTJ3MmjB9SrVzJrawpFSc= github.com/vektah/gqlparser/v2 v2.5.32/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts= github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -266,8 +264,6 @@ go.mau.fi/whatsmeow v0.0.0-20260305215846-fc65416c22c4 h1:FGA3NtCVNeCJ+C+KBg1pOD go.mau.fi/whatsmeow v0.0.0-20260305215846-fc65416c22c4/go.mod h1:mXCRFyPEPn4jqWz6Afirn8vY7DpHCPnlKq6I2cWwFHM= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.25.0 h1:qnk6Ksugpi5Bz32947rkUgDt9/s5qvqDPl/gBKdMJLE= golang.org/x/arch v0.25.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -278,8 +274,6 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= -golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o= -golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -301,13 +295,9 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= -golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= -golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= -golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= -golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -316,8 +306,6 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -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/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -333,6 +321,7 @@ golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -342,8 +331,6 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -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/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.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -362,8 +349,6 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= -golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= -golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -404,21 +389,16 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= -modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc= -modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM= modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw= -modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= -modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0= modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= +modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= -modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE= -modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo= +modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= -modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI= -modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE= modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw= modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=