@@ -168,6 +168,12 @@ type updatesLoadedMsg struct {
168168 Updates map [string ]UpdateInfo
169169}
170170
171+ type setupProgressMsg struct {
172+ Step int
173+ Total int
174+ Label string
175+ }
176+
171177// ─────────────────────────────────────────────
172178// Prompt states
173179// ─────────────────────────────────────────────
@@ -183,6 +189,7 @@ const (
183189 PromptSettings
184190 PromptThemeDepConfirm
185191 PromptThemeDepMethod
192+ PromptSetupProgress
186193)
187194
188195var Program * tea.Program
@@ -263,6 +270,9 @@ type TuiModel struct {
263270 themeDepResolveChan chan themeDepDecision
264271 activeThemeDep string
265272 startupCheckDone bool
273+ setupStep int
274+ setupTotalSteps int
275+ setupLabel string
266276}
267277
268278func NewModel (root string ) TuiModel {
@@ -285,6 +295,9 @@ func NewModel(root string) TuiModel {
285295 finalizedAdds : make (map [string ]string ),
286296 themeDepResolveChan : make (chan themeDepDecision , 1 ),
287297 startupCheckDone : false ,
298+ setupStep : 0 ,
299+ setupTotalSteps : 0 ,
300+ setupLabel : "" ,
288301 }
289302}
290303
@@ -781,7 +794,11 @@ func (m TuiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
781794 return m , tickCmd ()
782795
783796 case tea.KeyMsg :
784- if m .activePrompt != PromptNone {
797+ promptToShow := m .activePrompt
798+ if m .taskActive && (promptToShow == PromptNone || promptToShow == PromptUninstallConfirm || promptToShow == PromptInstallMethod ) {
799+ promptToShow = PromptSetupProgress
800+ }
801+ if promptToShow != PromptNone {
785802 return m .handlePromptKeys (msg )
786803 }
787804 switch msg .String () {
@@ -990,6 +1007,12 @@ func (m TuiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
9901007 } else {
9911008 m .statusMsg = "All packages are up to date."
9921009 }
1010+
1011+ case setupProgressMsg :
1012+ m .setupStep = msg .Step
1013+ m .setupTotalSteps = msg .Total
1014+ m .setupLabel = msg .Label
1015+ return m , nil
9931016 case promptThemeDepMsg :
9941017 m .activePrompt = PromptThemeDepConfirm
9951018 m .activeThemeDep = msg .DepName
@@ -1065,6 +1088,17 @@ func (m *TuiModel) addLog(line string) {
10651088// ─────────────────────────────────────────────
10661089
10671090func (m TuiModel ) handlePromptKeys (msg tea.KeyMsg ) (tea.Model , tea.Cmd ) {
1091+ promptToShow := m .activePrompt
1092+ if m .taskActive && (promptToShow == PromptNone || promptToShow == PromptUninstallConfirm || promptToShow == PromptInstallMethod ) {
1093+ promptToShow = PromptSetupProgress
1094+ }
1095+ if promptToShow == PromptSetupProgress {
1096+ if msg .String () == "ctrl+c" {
1097+ return m , tea .Quit
1098+ }
1099+ return m , nil
1100+ }
1101+
10681102 switch msg .String () {
10691103 case "esc" , "q" :
10701104 if m .activePrompt == PromptThemeDepConfirm || m .activePrompt == PromptThemeDepMethod {
@@ -1969,8 +2003,12 @@ func (m TuiModel) View() string {
19692003 // Catalog panel — always rendered; modal overlaid on top if active
19702004 catalogContent := m .renderCatalogPanel (catW - 4 , catalogH - 2 , showLogs )
19712005 catalogPanel = drawPanel (catW - 1 , catalogH , "Catalog" , catalogContent , true )
1972- if m .activePrompt != PromptNone {
1973- modal := m .renderModal ()
2006+ promptToShow := m .activePrompt
2007+ if m .taskActive && (promptToShow == PromptNone || promptToShow == PromptUninstallConfirm || promptToShow == PromptInstallMethod ) {
2008+ promptToShow = PromptSetupProgress
2009+ }
2010+ if promptToShow != PromptNone {
2011+ modal := m .renderModal (promptToShow )
19742012 styledModal := lipgloss .NewStyle ().
19752013 Border (lipgloss .RoundedBorder ()).
19762014 BorderForeground (colorCyan ).
@@ -1997,8 +2035,12 @@ func (m TuiModel) View() string {
19972035 // Catalog full width — always rendered; modal overlaid on top if active
19982036 catalogContent := m .renderCatalogPanel (w - 4 , catalogH - 2 , showLogs )
19992037 catalogPanel = drawPanel (w , catalogH , "Catalog" , catalogContent , true )
2000- if m .activePrompt != PromptNone {
2001- modal := m .renderModal ()
2038+ promptToShow := m .activePrompt
2039+ if m .taskActive && (promptToShow == PromptNone || promptToShow == PromptUninstallConfirm || promptToShow == PromptInstallMethod ) {
2040+ promptToShow = PromptSetupProgress
2041+ }
2042+ if promptToShow != PromptNone {
2043+ modal := m .renderModal (promptToShow )
20022044 styledModal := lipgloss .NewStyle ().
20032045 Border (lipgloss .RoundedBorder ()).
20042046 BorderForeground (colorCyan ).
@@ -2710,11 +2752,32 @@ func (m TuiModel) getInstallMethods(pkg *bridge.CatalogEntry) []InstallMethod {
27102752// Modal
27112753// ─────────────────────────────────────────────
27122754
2713- func (m TuiModel ) renderModal () string {
2755+ func renderProgressBar (step , total , width int ) string {
2756+ if total <= 0 {
2757+ return lipgloss .NewStyle ().Foreground (colorDim ).Render (strings .Repeat ("░" , width ))
2758+ }
2759+ filledWidth := (step * width ) / total
2760+ if filledWidth > width {
2761+ filledWidth = width
2762+ }
2763+ if filledWidth < 0 {
2764+ filledWidth = 0
2765+ }
2766+ emptyWidth := width - filledWidth
2767+ filled := lipgloss .NewStyle ().Foreground (colorCyan ).Render (strings .Repeat ("█" , filledWidth ))
2768+ empty := lipgloss .NewStyle ().Foreground (colorDim ).Render (strings .Repeat ("░" , emptyWidth ))
2769+ return filled + empty
2770+ }
2771+
2772+ // ─────────────────────────────────────────────
2773+ // Modal
2774+ // ─────────────────────────────────────────────
2775+
2776+ func (m TuiModel ) renderModal (prompt PromptType ) string {
27142777 pkg := m .promptPkg
27152778 var content strings.Builder
27162779
2717- if m . activePrompt == PromptInstallMethod {
2780+ if prompt == PromptInstallMethod {
27182781 content .WriteString (boldStyle .Render ("Install source for " ) + accentStyle .Render (pkg .ShortName ) + "\n \n " )
27192782 methods := m .getInstallMethods (pkg )
27202783 for i , mth := range methods {
@@ -2736,7 +2799,7 @@ func (m TuiModel) renderModal() string {
27362799 }
27372800 content .WriteString ("\n " + subtleStyle .Render ("↑↓ select Enter confirm Esc cancel" ))
27382801 return modalStyle .Width (72 ).Render (content .String ())
2739- } else if m . activePrompt == PromptThemeDepMethod {
2802+ } else if prompt == PromptThemeDepMethod {
27402803 content .WriteString (boldStyle .Render ("Install source for theme dependency: " ) + accentStyle .Render (pkg .ShortName ) + "\n \n " )
27412804 methods := m .getInstallMethods (pkg )
27422805 for i , mth := range methods {
@@ -2758,7 +2821,7 @@ func (m TuiModel) renderModal() string {
27582821 }
27592822 content .WriteString ("\n " + subtleStyle .Render ("↑↓ select Enter confirm Esc cancel" ))
27602823 return modalStyle .Width (72 ).Render (content .String ())
2761- } else if m . activePrompt == PromptThemeDepConfirm {
2824+ } else if prompt == PromptThemeDepConfirm {
27622825 content .WriteString (boldStyle .Render ("Theme dependency required: " ) + accentStyle .Render (pkg .ShortName ) + "\n " )
27632826 content .WriteString (subtleStyle .Render ("The active or queued theme requires this package to function correctly." ) + "\n \n " )
27642827 content .WriteString (boldStyle .Render ("Install " ) + accentStyle .Render (pkg .ShortName ) + boldStyle .Render ("?" ) + "\n \n " )
@@ -2776,7 +2839,7 @@ func (m TuiModel) renderModal() string {
27762839 content .WriteString (" " )
27772840 }
27782841 content .WriteString ("\n \n " + subtleStyle .Render ("← → select Enter confirm Esc cancel" ))
2779- } else if m . activePrompt == PromptUninstallConfirm {
2842+ } else if prompt == PromptUninstallConfirm {
27802843 content .WriteString (boldStyle .Render ("Uninstall " ) + errStyle .Render (pkg .ShortName ) + boldStyle .Render ("?" ) + "\n \n " )
27812844 opts := []string {"Yes, uninstall" , "No, keep it" }
27822845 for i , opt := range opts {
@@ -2792,7 +2855,7 @@ func (m TuiModel) renderModal() string {
27922855 content .WriteString (" " )
27932856 }
27942857 content .WriteString ("\n \n " + subtleStyle .Render ("← → select Enter confirm Esc cancel" ))
2795- } else if m . activePrompt == PromptForceReinstallConfirm {
2858+ } else if prompt == PromptForceReinstallConfirm {
27962859 content .WriteString (boldStyle .Render ("Force re-download " ) + errStyle .Render (pkg .ShortName ) + boldStyle .Render ("?" ) + "\n " )
27972860 content .WriteString (warnStyle .Render ("⚠ WARNING: This will delete the local directory and lose all modifications!" ) + "\n \n " )
27982861 opts := []string {"Yes, wipe & reinstall" , "No, cancel" }
@@ -2809,7 +2872,19 @@ func (m TuiModel) renderModal() string {
28092872 content .WriteString (" " )
28102873 }
28112874 content .WriteString ("\n \n " + subtleStyle .Render ("← → select Enter confirm Esc cancel" ))
2812- } else if m .activePrompt == PromptManagePackage {
2875+ } else if prompt == PromptSetupProgress {
2876+ spinnerFrames := []string {"⠋" , "⠙" , "⠹" , "⠸" , "⠼" , "⠴" , "⠦" , "⠧" , "⠇" , "⠏" }
2877+ frame := spinnerFrames [m .tickCount % len (spinnerFrames )]
2878+ spin := lipgloss .NewStyle ().Foreground (colorCyan ).Bold (true ).Render (frame )
2879+
2880+ content .WriteString (boldStyle .Render ("Installing packages…" ) + "\n \n " )
2881+ content .WriteString (" " + spin + " " + boldStyle .Render (m .setupLabel ) + "\n \n " )
2882+
2883+ bar := renderProgressBar (m .setupStep , m .setupTotalSteps , 40 )
2884+ content .WriteString (" " + bar + "\n " )
2885+ content .WriteString (fmt .Sprintf (" Step %d of %d\n " , m .setupStep , m .setupTotalSteps ))
2886+ return modalStyle .Width (72 ).Render (content .String ())
2887+ } else if prompt == PromptManagePackage {
28132888 var leftCols []string
28142889 leftCols = append (leftCols , boldStyle .Render ("Package:" )+ " " + accentStyle .Render (pkg .ShortName ))
28152890 leftCols = append (leftCols , mutedStyle .Render ("Type: " )+ " " + boldStyle .Render (strings .ToUpper (pkg .Kind )))
@@ -3089,12 +3164,18 @@ func (m *TuiModel) RunSetupTask(adds map[string]string, p *tea.Program) {
30893164 go func () {
30903165 desktopJs := filepath .Join (m .workspaceRoot , "packages" , "cli" , "bin" , "desktop.js" )
30913166
3167+ totalSteps := len (adds ) + 2
3168+ step := 0
3169+ p .Send (setupProgressMsg {Step : step , Total : totalSteps , Label : "Initializing setup task…" })
3170+
30923171 // 1. Process all additions
30933172 for pkgName , method := range adds {
3173+ step ++
30943174 shortName := pkgName
30953175 if idx := strings .LastIndex (pkgName , "/" ); idx >= 0 {
30963176 shortName = pkgName [idx + 1 :]
30973177 }
3178+ p .Send (setupProgressMsg {Step : step , Total : totalSteps , Label : fmt .Sprintf ("Installing %s via %s…" , shortName , method )})
30983179
30993180 args := []string {desktopJs , "add" , shortName }
31003181 if method == "npm" {
@@ -3199,13 +3280,17 @@ func (m *TuiModel) RunSetupTask(adds map[string]string, p *tea.Program) {
31993280 }
32003281
32013282 // 2. Run pnpm install for cleanup/removal sync
3283+ step ++
3284+ p .Send (setupProgressMsg {Step : step , Total : totalSteps , Label : "Installing dependencies (pnpm install)…" })
32023285 p .Send (logLineMsg (">>> Running pnpm install (syncing workspace)…" ))
32033286 if err := runProcessAndStreamLogsSilent (m .workspaceRoot , "pnpm" , []string {"install" }, p ); err != nil {
32043287 p .Send (taskFinishedMsg {Success : false , Err : err })
32053288 return
32063289 }
32073290
32083291 // 3. Rebuild stubs
3292+ step ++
3293+ p .Send (setupProgressMsg {Step : step , Total : totalSteps , Label : "Rebuilding stubs (prepare:modules)…" })
32093294 p .Send (logLineMsg (">>> Rebuilding stubs…" ))
32103295 if err := runProcessAndStreamLogsSilent (m .workspaceRoot , "pnpm" , []string {"run" , "prepare:modules" }, p ); err != nil {
32113296 p .Send (taskFinishedMsg {Success : false , Err : err })
@@ -3220,6 +3305,7 @@ func (m *TuiModel) RunUpdatePackageTask(pkgName string, shortName string, kind s
32203305 go func () {
32213306 p .Send (clearLogsMsg {})
32223307 p .Send (logLineMsg (fmt .Sprintf (">>> Starting update for %s…" , shortName )))
3308+ p .Send (setupProgressMsg {Step : 1 , Total : 3 , Label : fmt .Sprintf ("Updating %s…" , shortName )})
32233309
32243310 desktopJs := filepath .Join (m .workspaceRoot , "packages" , "cli" , "bin" , "desktop.js" )
32253311
@@ -3254,13 +3340,15 @@ func (m *TuiModel) RunUpdatePackageTask(pkgName string, shortName string, kind s
32543340 }
32553341
32563342 // Run pnpm install
3343+ p .Send (setupProgressMsg {Step : 2 , Total : 3 , Label : "Installing dependencies (pnpm install)…" })
32573344 p .Send (logLineMsg (">>> Running pnpm install (syncing workspace)…" ))
32583345 if err := runProcessAndStreamLogsSilent (m .workspaceRoot , "pnpm" , []string {"install" }, p ); err != nil {
32593346 p .Send (taskFinishedMsg {Success : false , Err : err })
32603347 return
32613348 }
32623349
32633350 // Rebuild stubs
3351+ p .Send (setupProgressMsg {Step : 3 , Total : 3 , Label : "Rebuilding stubs (prepare:modules)…" })
32643352 p .Send (logLineMsg (">>> Rebuilding stubs…" ))
32653353 if err := runProcessAndStreamLogsSilent (m .workspaceRoot , "pnpm" , []string {"run" , "prepare:modules" }, p ); err != nil {
32663354 p .Send (taskFinishedMsg {Success : false , Err : err })
0 commit comments