fix: docker version check, uv PATH, install progress bar
All checks were successful
CI / build (push) Successful in 1m45s

- Fix Docker latest version: use moby/moby instead of docker/compose
- Fix uv install: add ~/.local/bin to PATH in shell rc after install
- Add progress bar during tool installation in dashboard
- Show spinner + per-tool progress with X/Y counter
- Disable install key while installation is running

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
This commit is contained in:
Augustin
2026-04-20 00:07:37 +02:00
parent 1be4fc061f
commit e6fdec4ff5
3 changed files with 113 additions and 20 deletions

View File

@@ -344,7 +344,14 @@ func (i *Installer) installUv() InstallResult {
var cmd *exec.Cmd
switch i.system.OS {
case platform.Linux, platform.MacOS:
home, _ := os.UserHomeDir()
cmd = exec.Command("bash", "-c", "curl -LsSf https://astral.sh/uv/install.sh | sh")
if output, err := cmd.CombinedOutput(); err != nil {
return InstallResult{Tool: "uv", Success: false, Message: fmt.Sprintf("install failed: %s: %s", err, string(output))}
}
rcFile := i.getRCFile()
appendLine(rcFile, "export PATH="+home+"/.local/bin:$PATH")
return InstallResult{Tool: "uv", Success: true, Message: "installed (added ~/.local/bin to PATH)"}
case platform.Windows:
cmd = exec.Command("powershell", "-Command", "irm https://astral.sh/uv/install.ps1 | iex")
default:

View File

@@ -163,6 +163,11 @@ type aiResponseMsg struct{ content string }
type aiErrMsg struct{ err error }
type scanCompleteMsg struct{ result *scanner.ScanResult }
type installCompleteMsg struct{ results []installer.InstallResult }
type installProgressMsg struct {
tool string
current int
total int
}
type updateCheckMsg struct{ statuses []updater.UpdateStatus }
type previewReadyMsg struct{ url string }
type workflowPhaseMsg struct{ phase workflow.Phase }
@@ -206,6 +211,11 @@ type Model struct {
ctrlCCount int
lastCtrlC time.Time
installing bool
installCurrent int
installTotal int
installTool string
}
type keyMap struct {
@@ -390,6 +400,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.viewport.SetContent(m.renderContent())
return m, nil
case installCompleteMsg:
m.installing = false
for _, r := range msg.results {
status := itemOKStyle.Render("[OK]")
if !r.Success {
@@ -398,6 +409,35 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.installLog = append(m.installLog, fmt.Sprintf(" %s %s: %s", status, r.Tool, r.Message))
}
m.scanResult = scanner.ScanSystem()
m.progressBar.SetPercent(1)
m.viewport.SetContent(m.renderContent())
return m, nil
case installProgressMsg:
status := itemOKStyle.Render("[OK]")
m.installLog = append(m.installLog, fmt.Sprintf(" %s %s installed", status, msg.tool))
m.installCurrent = msg.current
m.installTool = ""
pct := float64(msg.current) / float64(max(msg.total, 1))
m.progressBar.SetPercent(pct)
m.viewport.SetContent(m.renderContent())
return m, nil
case installBatchMsg:
status := itemOKStyle.Render("[OK]")
if !msg.result.Success {
status = itemMissingStyle.Render("[FAIL]")
}
m.installLog = append(m.installLog, fmt.Sprintf(" %s %s: %s", status, msg.result.Tool, msg.result.Message))
m.installCurrent = msg.index + 1
m.installTotal = len(msg.tools)
pct := float64(m.installCurrent) / float64(max(m.installTotal, 1))
m.progressBar.SetPercent(pct)
if msg.index+1 < len(msg.tools) {
m.installTool = msg.tools[msg.index+1]
m.viewport.SetContent(m.renderContent())
return m, startInstallCmd(msg.config, msg.tools, msg.index+1)
}
m.installing = false
m.scanResult = scanner.ScanSystem()
m.viewport.SetContent(m.renderContent())
return m, nil
case updateCheckMsg:
@@ -577,27 +617,35 @@ func (m Model) handleChatSubmit() (tea.Model, tea.Cmd) {
func (m Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.String() {
case "i":
return m, tea.Cmd(func() tea.Msg {
needsSudo := checkNeedsSudo(m.scanResult)
if needsSudo && !hasSudo() {
return installCompleteMsg{results: []installer.InstallResult{
{Tool: "system", Success: false, Message: "some tools require sudo. Run: muyue install, or relaunch with sudo/pkexec"},
}}
}
inst := installer.New(m.config)
var missing []string
if m.scanResult != nil {
for _, t := range m.scanResult.Tools {
if !t.Installed {
missing = append(missing, t.Name)
}
if m.installing {
return m, nil
}
var missing []string
if m.scanResult != nil {
for _, t := range m.scanResult.Tools {
if !t.Installed {
missing = append(missing, t.Name)
}
}
if len(missing) == 0 {
return installCompleteMsg{results: []installer.InstallResult{}}
}
return installCompleteMsg{results: inst.InstallAll(missing)}
})
}
if len(missing) == 0 {
m.installLog = append(m.installLog, itemOKStyle.Render("All tools already installed!"))
m.viewport.SetContent(m.renderContent())
return m, nil
}
needsSudo := checkNeedsSudo(m.scanResult)
if needsSudo && !hasSudo() {
m.installLog = append(m.installLog, errMsgStyle.Render("Some tools require sudo. Run: sudo muyue install"))
m.viewport.SetContent(m.renderContent())
return m, nil
}
m.installing = true
m.installCurrent = 0
m.installTotal = len(missing)
m.installTool = missing[0]
m.progressBar.SetPercent(0)
m.viewport.SetContent(m.renderContent())
return m, startInstallCmd(m.config, missing, 0)
case "u":
return m, tea.Cmd(func() tea.Msg {
result := scanner.ScanSystem()
@@ -748,6 +796,30 @@ func hasSudo() bool {
return false
}
func startInstallCmd(cfg *config.MuyueConfig, tools []string, index int) tea.Cmd {
return tea.Cmd(func() tea.Msg {
inst := installer.New(cfg)
result := inst.InstallTool(tools[index])
if index+1 < len(tools) {
return installBatchMsg{
result: result,
tools: tools,
index: index,
config: cfg,
}
}
return installCompleteMsg{results: []installer.InstallResult{result}}
})
}
type installBatchMsg struct {
result installer.InstallResult
tools []string
index int
config *config.MuyueConfig
}
func sendAIMessage(orch *orchestrator.Orchestrator, input string) tea.Cmd {
return tea.Cmd(func() tea.Msg {
if orch == nil {
@@ -968,6 +1040,20 @@ func (m Model) renderDashboard() string {
b.WriteString("\n")
}
if m.installing {
b.WriteString(sectionStyle.Render("Installing..."))
b.WriteString("\n")
progBar := m.progressBar.View()
label := ""
if m.installTool != "" {
label = fmt.Sprintf(" %d/%d - %s", m.installCurrent+1, m.installTotal, m.installTool)
} else {
label = fmt.Sprintf(" %d/%d", m.installCurrent, m.installTotal)
}
b.WriteString(fmt.Sprintf(" %s%s\n", progBar, label))
b.WriteString("\n")
}
if len(m.installLog) > 0 {
b.WriteString(sectionStyle.Render("Install Log"))
b.WriteString("\n")

View File

@@ -60,7 +60,7 @@ func getLatestVersion(tool string) (string, error) {
"crush": "charmbracelet/crush",
"gh": "cli/cli",
"starship": "starship/starship",
"docker": "docker/compose",
"docker": "moby/moby",
}
repo, ok := repos[tool]