@@ -19,6 +19,7 @@ import { Avatar } from "components/Avatar/Avatar";
19
19
import { AvatarData } from "components/Avatar/AvatarData" ;
20
20
import { AvatarDataSkeleton } from "components/Avatar/AvatarDataSkeleton" ;
21
21
import { Button } from "components/Button/Button" ;
22
+ import { ExternalImage } from "components/ExternalImage/ExternalImage" ;
22
23
import { VSCodeIcon } from "components/Icons/VSCodeIcon" ;
23
24
import { VSCodeInsidersIcon } from "components/Icons/VSCodeInsidersIcon" ;
24
25
import { InfoTooltip } from "components/InfoTooltip/InfoTooltip" ;
@@ -63,6 +64,7 @@ import {
63
64
getVSCodeHref ,
64
65
openAppInNewWindow ,
65
66
} from "modules/apps/apps" ;
67
+ import { useAppLink } from "modules/apps/useAppLink" ;
66
68
import { useDashboard } from "modules/dashboard/useDashboard" ;
67
69
import { WorkspaceAppStatus } from "modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus" ;
68
70
import { WorkspaceDormantBadge } from "modules/workspaces/WorkspaceDormantBadge/WorkspaceDormantBadge" ;
@@ -622,6 +624,9 @@ const PrimaryAction: FC<PrimaryActionProps> = ({
622
624
) ;
623
625
} ;
624
626
627
+ // The total number of apps that can be displayed in the workspace row
628
+ const WORKSPACE_APPS_SLOTS = 4 ;
629
+
625
630
type WorkspaceAppsProps = {
626
631
workspace : Workspace ;
627
632
} ;
@@ -647,11 +652,18 @@ const WorkspaceApps: FC<WorkspaceAppsProps> = ({ workspace }) => {
647
652
return null ;
648
653
}
649
654
655
+ const builtinApps = new Set ( agent . display_apps ) ;
656
+ builtinApps . delete ( "port_forwarding_helper" ) ;
657
+ builtinApps . delete ( "ssh_helper" ) ;
658
+
659
+ const remainingSlots = WORKSPACE_APPS_SLOTS - builtinApps . size ;
660
+ const userApps = agent . apps . slice ( 0 , remainingSlots ) ;
661
+
650
662
const buttons : ReactNode [ ] = [ ] ;
651
663
652
- if ( agent . display_apps . includes ( "vscode" ) ) {
664
+ if ( builtinApps . has ( "vscode" ) ) {
653
665
buttons . push (
654
- < AppLink
666
+ < BaseIconLink
655
667
key = "vscode"
656
668
isLoading = { ! token }
657
669
label = "Open VSCode"
@@ -664,13 +676,13 @@ const WorkspaceApps: FC<WorkspaceAppsProps> = ({ workspace }) => {
664
676
} ) }
665
677
>
666
678
< VSCodeIcon />
667
- </ AppLink > ,
679
+ </ BaseIconLink > ,
668
680
) ;
669
681
}
670
682
671
- if ( agent . display_apps . includes ( "vscode_insiders" ) ) {
683
+ if ( builtinApps . has ( "vscode_insiders" ) ) {
672
684
buttons . push (
673
- < AppLink
685
+ < BaseIconLink
674
686
key = "vscode-insiders"
675
687
label = "Open VSCode Insiders"
676
688
isLoading = { ! token }
@@ -683,18 +695,29 @@ const WorkspaceApps: FC<WorkspaceAppsProps> = ({ workspace }) => {
683
695
} ) }
684
696
>
685
697
< VSCodeInsidersIcon />
686
- </ AppLink > ,
698
+ </ BaseIconLink > ,
687
699
) ;
688
700
}
689
701
690
- if ( agent . display_apps . includes ( "web_terminal" ) ) {
702
+ for ( const app of userApps ) {
703
+ buttons . push (
704
+ < IconAppLink
705
+ key = { app . id }
706
+ app = { app }
707
+ workspace = { workspace }
708
+ agent = { agent }
709
+ /> ,
710
+ ) ;
711
+ }
712
+
713
+ if ( builtinApps . has ( "web_terminal" ) ) {
691
714
const href = getTerminalHref ( {
692
715
username : workspace . owner_name ,
693
716
workspace : workspace . name ,
694
717
agent : agent . name ,
695
718
} ) ;
696
719
buttons . push (
697
- < AppLink
720
+ < BaseIconLink
698
721
key = "terminal"
699
722
href = { href }
700
723
onClick = { ( e ) => {
@@ -704,21 +727,45 @@ const WorkspaceApps: FC<WorkspaceAppsProps> = ({ workspace }) => {
704
727
label = "Open Terminal"
705
728
>
706
729
< SquareTerminalIcon />
707
- </ AppLink > ,
730
+ </ BaseIconLink > ,
708
731
) ;
709
732
}
710
733
711
734
return buttons ;
712
735
} ;
713
736
714
- type AppLinkProps = PropsWithChildren < {
737
+ type IconAppLinkProps = {
738
+ app : WorkspaceApp ;
739
+ workspace : Workspace ;
740
+ agent : WorkspaceAgent ;
741
+ } ;
742
+
743
+ const IconAppLink : FC < IconAppLinkProps > = ( { app, workspace, agent } ) => {
744
+ const link = useAppLink ( app , {
745
+ workspace,
746
+ agent,
747
+ } ) ;
748
+
749
+ return (
750
+ < BaseIconLink
751
+ key = { app . id }
752
+ label = { `Open ${ link . label } ` }
753
+ href = { link . href }
754
+ onClick = { link . onClick }
755
+ >
756
+ < ExternalImage src = { app . icon ?? "/icon/widgets.svg" } />
757
+ </ BaseIconLink >
758
+ ) ;
759
+ } ;
760
+
761
+ type BaseIconLinkProps = PropsWithChildren < {
715
762
label : string ;
716
763
href : string ;
717
764
isLoading ?: boolean ;
718
765
onClick ?: ( e : React . MouseEvent < HTMLAnchorElement > ) => void ;
719
766
} > ;
720
767
721
- const AppLink : FC < AppLinkProps > = ( {
768
+ const BaseIconLink : FC < BaseIconLinkProps > = ( {
722
769
href,
723
770
isLoading,
724
771
label,
0 commit comments