Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,9 @@ pkg/apm/webrtc/**/*.h linguist-vendored
pkg/apm/webrtc/**/*.m linguist-vendored
pkg/apm/webrtc/**/*.inc linguist-vendored
pkg/portaudio/pa_src/** linguist-vendored

# TUI visual-regression frames are compared byte for byte against what the
# renderers emit, which is always LF. Without this a checkout under
# core.autocrlf — the default on Windows, including the CI runners — rewrites
# them to CRLF and every frame mismatches.
cmd/lk/testdata/vrt/*.txt text eol=lf
21 changes: 21 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,27 @@ jobs:
exit 1
fi

# The Test step above already fails on a VRT mismatch, but testify's diff of
# a full terminal frame is unreadable and says nothing about how to fix it.
# Re-recording here turns the failure into the diff of what actually moved
# on screen, and catches what the test alone cannot: a frame left behind by
# a fixture that no longer exists (make update-vrt clears the directory, so
# an orphan shows up as a deletion). Runs even when the tests failed, so the
# actionable message is never buried behind them.
- name: Verify TUI reference frames are up to date
if: always() && matrix.os == 'ubuntu-latest'
shell: bash
run: |
set -euo pipefail
make update-vrt
if [ -n "$(git status --porcelain -- cmd/lk/testdata/vrt)" ]; then
git --no-pager diff -- cmd/lk/testdata/vrt
git status --porcelain -- cmd/lk/testdata/vrt
echo "::error::The TUI renders differently than the recorded reference frames."
echo "::error::If the change is intended, run 'make update-vrt' and commit cmd/lk/testdata/vrt."
exit 1
fi

- name: Upload test log
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: always()
Expand Down
33 changes: 29 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
# extract the vendored C/C++ (PortAudio + WebRTC APM).
#
# `make install` puts it on $GOBIN with a `livekit-cli` alias for the legacy
# binary name. Releases use .goreleaser.yaml, not this file.
# binary name. `make update-vrt` re-records the TUI reference frames. Releases
# use .goreleaser.yaml, not this file.

.PHONY: all build install update-vrt clean

ifeq (,$(shell go env GOBIN))
GOBIN := $(shell go env GOPATH)/bin
Expand All @@ -16,17 +19,39 @@ ifeq ($(OS),Windows_NT)
EXE := .exe
endif

all: build

# pa_src holds the PortAudio C source the cgo build links against; the submodule
# init makes this work from a fresh clone (and under CodeQL, whose checkout may
# skip submodules). ALSA headers (libasound2-dev) come from CodeQL's automatic
# dependency installation on Linux.
./bin/lk$(EXE):
# dependency installation on Linux. Keyed on a file the submodule brings with it
# so an already-populated checkout doesn't pay for a git round trip every build.
pkg/portaudio/pa_src/CMakeLists.txt:
git submodule update --init --recursive

# Phony, rather than a rule for ./bin/lk: as a file target the binary counted as
# up to date the moment it existed, so every source change after the first was
# silently skipped and `make install` shipped a stale binary. go build tracks
# staleness itself — over the Go sources, the vendored C/C++ and the build flags
# alike — and exits immediately when there is nothing to do, which no
# prerequisite list written here could match.
build: pkg/portaudio/pa_src/CMakeLists.txt
CGO_ENABLED=1 go build -o ./bin/lk$(EXE) ./cmd/lk

install: ./bin/lk$(EXE)
install: build
cp ./bin/lk$(EXE) "$(GOBIN)/lk$(EXE)"
ln -sf "$(GOBIN)/lk$(EXE)" "$(GOBIN)/livekit-cli$(EXE)"

# Re-records the TUI visual regression frames in cmd/lk/testdata/vrt from the
# current renderers. Run it after an intentional UI change and commit the diff,
# which is the review artifact: it shows exactly what moved on screen.
#
# The existing frames are cleared first so a fixture that was renamed or removed
# takes its frame with it, instead of leaving one behind that nothing renders any
# more. Nothing is lost if the run then fails: git restore cmd/lk/testdata/vrt.
update-vrt:
rm -f cmd/lk/testdata/vrt/*.txt
UPDATE_TUI_VRT=1 go test ./cmd/lk -run TestVRT -count=1

clean:
rm -rf ./bin
22 changes: 11 additions & 11 deletions cmd/lk/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import (
"strings"
"time"

"github.com/charmbracelet/huh"
"charm.land/huh/v2"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/crane"
v1 "github.com/google/go-containerregistry/pkg/v1"
Expand Down Expand Up @@ -490,7 +490,7 @@ func initAgent(ctx context.Context, cmd *cli.Command) error {
huh.NewOption("Node.js", "node"),
).
Value(&lang).
WithTheme(util.Theme).
WithTheme(util.FormTheme()).
Run(); err != nil {
return err
}
Expand Down Expand Up @@ -580,7 +580,7 @@ func createAgent(ctx context.Context, cmd *cli.Command) error {
Value(&useProject).
Affirmative("Yes").
Negative("No, select another...").
WithTheme(util.Theme))).
WithTheme(util.FormTheme()))).
Run(); err != nil {
return err
}
Expand Down Expand Up @@ -722,7 +722,7 @@ func createAgent(ctx context.Context, cmd *cli.Command) error {
Title("Agent deploying. Would you like to view logs?").
Description("You can view logs later with `lk agent logs`").
Value(&viewLogs).
WithTheme(util.Theme),
WithTheme(util.FormTheme()),
),
).Run(); err != nil {
return err
Expand All @@ -748,7 +748,7 @@ func createAgentConfig(ctx context.Context, cmd *cli.Command) error {
fmt.Sprintf("Config file [%s] file already exists. Overwrite?", tomlFilename),
).
Value(&overwriteVal).
WithTheme(util.Theme),
WithTheme(util.FormTheme()),
),
).
Run(); err != nil {
Expand Down Expand Up @@ -1256,7 +1256,7 @@ func deleteAgent(ctx context.Context, cmd *cli.Command) error {
util.Confirm().
Title(fmt.Sprintf("Are you sure you want to delete agent %s?", agentMsg)).
Value(&confirmDelete).
WithTheme(util.Theme),
WithTheme(util.FormTheme()),
),
).Run(); err != nil {
return err
Expand Down Expand Up @@ -1376,7 +1376,7 @@ func resolveDeployAttributes(ctx context.Context, cmd *cli.Command) (map[string]
util.Confirm().
Title("Continue deploying anyway?").
Value(&proceed).
WithTheme(util.Theme),
WithTheme(util.FormTheme()),
),
).Run(); err != nil {
return nil, err
Expand Down Expand Up @@ -1658,7 +1658,7 @@ func updateAgentSecrets(ctx context.Context, cmd *cli.Command) error {
util.Confirm().
Title(fmt.Sprintf("This will remove all existing secrets. Are you sure you want to proceed [%s]?", agentID)).
Value(&confirmOverwrite).
WithTheme(util.Theme),
WithTheme(util.FormTheme()),
),
).Run(); err != nil {
return err
Expand Down Expand Up @@ -1768,7 +1768,7 @@ func selectAgent(ctx context.Context, cmd *cli.Command, excludeEmptyVersion bool
Title("Select an agent").
Options(agentNames...).
Value(&selectedAgent).
WithTheme(util.Theme).
WithTheme(util.FormTheme()).
Run(); err != nil {
return "", err
}
Expand Down Expand Up @@ -1991,7 +1991,7 @@ func resolveRegion(cmd *cli.Command, settingsMap map[string]string, title string
Title(title).
Options(options...).
Value(&region).
WithTheme(util.Theme).
WithTheme(util.FormTheme()).
Run(); err != nil {
return "", err
}
Expand Down Expand Up @@ -2033,7 +2033,7 @@ func confirmRegionResidency(cmd *cli.Command, region, dataRegion string, warnReg
Affirmative("Deploy").
Negative("Cancel").
Value(&confirmed))).
WithTheme(util.Theme).
WithTheme(util.FormTheme()).
Run(); err != nil {
return err
}
Expand Down
22 changes: 11 additions & 11 deletions cmd/lk/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ import (
"regexp"
"strings"

"github.com/charmbracelet/huh"
"github.com/charmbracelet/lipgloss"
"charm.land/huh/v2"
"charm.land/lipgloss/v2"
"github.com/urfave/cli/v3"

"github.com/livekit/livekit-cli/v2/pkg/bootstrap"
Expand Down Expand Up @@ -171,7 +171,7 @@ func requireProjectWithOpts(ctx context.Context, cmd *cli.Command, opts ...loadO
Value(&useDefault).
Affirmative("Yes").
Negative("No, select another...").
WithTheme(util.Theme))).
WithTheme(util.FormTheme()))).
Run(); err != nil {
return ctx, fmt.Errorf("failed to confirm project: %w", err)
}
Expand Down Expand Up @@ -213,7 +213,7 @@ func selectProject(ctx context.Context, cmd *cli.Command) (context.Context, erro
Description("To use a different project, run `lk cloud auth` to add credentials").
Options(options...).
Value(&project).
WithTheme(util.Theme))).
WithTheme(util.FormTheme()))).
Run(); err != nil {
return nil, fmt.Errorf("no project selected: %w", err)
}
Expand All @@ -227,7 +227,7 @@ func selectProject(ctx context.Context, cmd *cli.Command) (context.Context, erro
if err = huh.NewForm(huh.NewGroup(util.Confirm().
Title("No local projects found. Authenticate one?").
Value(&shouldAuth).
WithTheme(util.Theme))).
WithTheme(util.FormTheme()))).
Run(); err != nil {
return nil, fmt.Errorf("no project selected: %w", err)
}
Expand Down Expand Up @@ -320,7 +320,7 @@ func setupTemplate(ctx context.Context, cmd *cli.Command) error {
templateSelect := huh.NewSelect[string]().
Title("Select Template").
Value(&templateURL).
WithTheme(util.Theme)
WithTheme(util.FormTheme())
var options []huh.Option[string]
for _, t := range templateOptions {
if t.IsHidden {
Expand Down Expand Up @@ -376,12 +376,12 @@ func setupTemplate(ctx context.Context, cmd *cli.Command) error {
}
return nil
}).
WithTheme(util.Theme))
WithTheme(util.FormTheme()))
}
if len(preinstallPrompts) > 0 {
group := huh.NewGroup(preinstallPrompts...)
if err := huh.NewForm(group).
WithTheme(util.Theme).
WithTheme(util.FormTheme()).
RunWithContext(ctx); err != nil {
return err
}
Expand Down Expand Up @@ -433,7 +433,7 @@ func setupTemplate(ctx context.Context, cmd *cli.Command) error {
if err := huh.NewForm(huh.NewGroup(util.Confirm().
Title("Install dependencies?").
Value(&install).
WithTheme(util.Theme))).
WithTheme(util.FormTheme()))).
Run(); err != nil {
return err
}
Expand Down Expand Up @@ -579,7 +579,7 @@ func instantiateEnv(ctx context.Context, cmd *cli.Command, rootPath string, addl
Placeholder(oldValue).
Prompt("").
Value(&newValue).
WithTheme(util.Theme).
WithTheme(util.FormTheme()).
Run(); err != nil || newValue == "" {
return oldValue, err
}
Expand Down Expand Up @@ -702,7 +702,7 @@ func runTask(ctx context.Context, cmd *cli.Command) error {
Title("Select Task").
Options(options...).
Value(&taskName).
WithTheme(util.Theme))).
WithTheme(util.FormTheme()))).
Run(); err != nil {
return err
}
Expand Down
8 changes: 4 additions & 4 deletions cmd/lk/cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import (
"net/url"
"time"

"github.com/charmbracelet/huh"
"charm.land/huh/v2"
"github.com/pkg/browser"
"github.com/urfave/cli/v3"

Expand Down Expand Up @@ -261,7 +261,7 @@ func tryAuthIfNeeded(ctx context.Context, cmd *cli.Command) error {
Title("What is the name of this device?").
Prompt("").
Value(&cliConfig.DeviceName).
WithTheme(util.Theme))).
WithTheme(util.FormTheme()))).
Run(); err != nil {
return err
}
Expand Down Expand Up @@ -314,7 +314,7 @@ func tryAuthIfNeeded(ctx context.Context, cmd *cli.Command) error {
if err := huh.NewForm(huh.NewGroup(util.Confirm().
Title("Make this project default?").
Value(&isDefault).
WithTheme(util.Theme))).
WithTheme(util.FormTheme()))).
Run(); err != nil {
return err
}
Expand All @@ -337,7 +337,7 @@ func tryAuthIfNeeded(ctx context.Context, cmd *cli.Command) error {
}
return nil
}).
WithTheme(util.Theme).
WithTheme(util.FormTheme()).
Run(); err != nil {
return err
}
Expand Down
4 changes: 2 additions & 2 deletions cmd/lk/console.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ import (
"syscall"
"time"

tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
"github.com/urfave/cli/v3"

"github.com/livekit/livekit-cli/v2/pkg/console"
Expand Down
24 changes: 18 additions & 6 deletions cmd/lk/console_tui.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ import (
"strings"
"time"

"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"charm.land/bubbles/v2/textinput"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"

agent "github.com/livekit/protocol/livekit/agent"

Expand Down Expand Up @@ -125,9 +125,17 @@ func newConsoleModel(pipeline *console.AudioPipeline, pipelineCancel context.Can
ti := textinput.New()
ti.Placeholder = "Type to talk to your agent"
ti.CharLimit = 1000
ti.Width = 60
ti.SetWidth(60)
ti.Prompt = "❯ "
ti.PromptStyle = boldStyle
// v2 moved per-state styling behind Styles/SetStyles, and renders a real
// terminal cursor unless the virtual one is enabled. The status area is
// composed into a larger view, so keep the inline virtual cursor rather than
// positioning the terminal cursor from here.
tiStyles := ti.Styles()
tiStyles.Focused.Prompt = boldStyle
tiStyles.Blurred.Prompt = boldStyle
ti.SetStyles(tiStyles)
ti.SetVirtualCursor(true)

if textMode {
ti.Focus()
Expand Down Expand Up @@ -552,7 +560,11 @@ func formatChatItem(item *agent.ChatContext_ChatItem) string {
// Layout matches the old Python console (FrequencyVisualizer + prompt).
// ──────────────────────────────────────────────────────────────────

func (m consoleModel) View() string {
func (m consoleModel) View() tea.View {
return tea.NewView(m.render())
}

func (m consoleModel) render() string {
var b strings.Builder

if m.shuttingDown {
Expand Down
Loading
Loading