@@ -9,24 +9,26 @@ import { ExternalImage } from "components/ExternalImage/ExternalImage";
99import { InfoTooltip } from "components/InfoTooltip/InfoTooltip" ;
1010import { Link } from "components/Link/Link" ;
1111import { ScrollArea , ScrollBar } from "components/ScrollArea/ScrollArea" ;
12- import { ChevronDownIcon , LayoutGridIcon } from "lucide-react" ;
12+ import { ChevronDownIcon , LayoutGridIcon , TerminalIcon } from "lucide-react" ;
13+ import { getTerminalHref } from "modules/apps/apps" ;
1314import { useAppLink } from "modules/apps/useAppLink" ;
1415import {
1516 getTaskApps ,
1617 type Task ,
1718 type WorkspaceAppWithAgent ,
1819} from "modules/tasks/tasks" ;
19- import type React from "react" ;
2020import { type FC , useState } from "react" ;
21- import { Link as RouterLink } from "react-router" ;
21+ import { type LinkProps , Link as RouterLink } from "react-router" ;
2222import { cn } from "utils/cn" ;
2323import { docs } from "utils/docs" ;
24- import { TaskAppIFrame } from "./TaskAppIframe" ;
24+ import { TaskAppIFrame , TaskIframe } from "./TaskAppIframe" ;
2525
2626type TaskAppsProps = {
2727 task : Task ;
2828} ;
2929
30+ const TERMINAL_TAB_ID = "terminal" ;
31+
3032export const TaskApps : FC < TaskAppsProps > = ( { task } ) => {
3133 const apps = getTaskApps ( task ) . filter (
3234 // The Chat UI app will be displayed in the sidebar, so we don't want to
@@ -39,6 +41,13 @@ export const TaskApps: FC<TaskAppsProps> = ({ task }) => {
3941 const [ activeAppId , setActiveAppId ] = useState ( embeddedApps . at ( 0 ) ?. id ) ;
4042 const hasAvailableAppsToDisplay =
4143 embeddedApps . length > 0 || externalApps . length > 0 ;
44+ const taskAgent = apps . at ( 0 ) ?. agent ;
45+ const terminalHref = getTerminalHref ( {
46+ username : task . workspace . owner_name ,
47+ workspace : task . workspace . name ,
48+ agent : taskAgent ?. name ,
49+ } ) ;
50+ const isTerminalActive = activeAppId === TERMINAL_TAB_ID ;
4251
4352 return (
4453 < main className = "flex flex-col h-full" >
@@ -58,6 +67,17 @@ export const TaskApps: FC<TaskAppsProps> = ({ task }) => {
5867 } }
5968 />
6069 ) ) }
70+ < TaskTab
71+ to = { terminalHref }
72+ active = { isTerminalActive }
73+ onClick = { ( e ) => {
74+ e . preventDefault ( ) ;
75+ setActiveAppId ( TERMINAL_TAB_ID ) ;
76+ } }
77+ >
78+ < TerminalIcon />
79+ Terminal
80+ </ TaskTab >
6181 </ div >
6282 < ScrollBar orientation = "horizontal" className = "h-2" />
6383 </ ScrollArea >
@@ -78,6 +98,14 @@ export const TaskApps: FC<TaskAppsProps> = ({ task }) => {
7898 task = { task }
7999 />
80100 ) ) }
101+
102+ < TaskIframe
103+ src = { terminalHref }
104+ title = "Terminal"
105+ className = { cn ( {
106+ hidden : ! isTerminalActive ,
107+ } ) }
108+ />
81109 </ div >
82110 ) : (
83111 < div className = "mx-auto my-auto flex flex-col items-center" >
@@ -161,11 +189,30 @@ const TaskAppTab: FC<TaskAppTabProps> = ({ task, app, active, onClick }) => {
161189 workspace : task . workspace ,
162190 } ) ;
163191
192+ return (
193+ < TaskTab active = { active } to = { link . href } onClick = { onClick } >
194+ { app . icon ? < ExternalImage src = { app . icon } /> : < LayoutGridIcon /> }
195+ { link . label }
196+ { app . health === "unhealthy" && (
197+ < InfoTooltip
198+ title = "This app is unhealthy."
199+ message = "The health check failed."
200+ type = "warning"
201+ />
202+ ) }
203+ </ TaskTab >
204+ ) ;
205+ } ;
206+
207+ type TaskTabProps = LinkProps & {
208+ active : boolean ;
209+ } ;
210+
211+ const TaskTab : FC < TaskTabProps > = ( { active, ...routerLinkProps } ) => {
164212 return (
165213 < Button
166214 size = "sm"
167215 variant = "subtle"
168- key = { app . id }
169216 asChild
170217 className = { cn ( [
171218 "px-3" ,
@@ -176,17 +223,7 @@ const TaskAppTab: FC<TaskAppTabProps> = ({ task, app, active, onClick }) => {
176223 { "opacity-75 hover:opacity-100" : ! active } ,
177224 ] ) }
178225 >
179- < RouterLink to = { link . href } onClick = { onClick } >
180- { app . icon ? < ExternalImage src = { app . icon } /> : < LayoutGridIcon /> }
181- { link . label }
182- { app . health === "unhealthy" && (
183- < InfoTooltip
184- title = "This app is unhealthy."
185- message = "The health check failed."
186- type = "warning"
187- />
188- ) }
189- </ RouterLink >
226+ < RouterLink { ...routerLinkProps } />
190227 </ Button >
191228 ) ;
192229} ;
0 commit comments