@@ -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 {
1516getTaskApps ,
1617type Task ,
1718type 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 = {
2727task :Task ;
2828} ;
2929
30+ const TERMINAL_TAB_ID = "terminal" ;
31+
3032export const TaskApps :FC < TaskAppsProps > = ( { task} ) => {
3133const 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 }) => {
3941const [ activeAppId , setActiveAppId ] = useState ( embeddedApps . at ( 0 ) ?. id ) ;
4042const hasAvailableAppsToDisplay =
4143embeddedApps . 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
4352return (
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 }) => {
7898task = { 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 }) => {
161189workspace :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 } ) => {
164212return (
165213< Button
166214size = "sm"
167215variant = "subtle"
168- key = { app . id }
169216asChild
170217className = { 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} ;