diff --git a/Dockerfile b/Dockerfile index 3a1199bc..080e67fe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -59,6 +59,12 @@ WORKDIR $GOPATH/src/packages/ai-developer/ RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o /go/executor executor.go +FROM build-base AS terminal-base + +WORKDIR $GOPATH/src/packages/ai-developer/ + +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o /go/terminal terminal.go + FROM build-base AS worker-development @@ -105,6 +111,23 @@ COPY ./app/prompts /go/prompts ENTRYPOINT ["bash", "-c", "/go/executor"] +FROM superagidev/supercoder-python-ide:latest AS terminal + +RUN git config --global user.email "supercoder@superagi.com" +RUN git config --global user.name "SuperCoder" + +ENV TERM xterm +ENV HOME /home/coder + +# to make the terminal look nice +RUN echo "PS1='\[\033[01;32m\]\u:\[\033[01;34m\]\w\[\033[00m\]\$ '" >> /home/coder/.bashrc + + +COPY --from=terminal-base /go/terminal /go/terminal +COPY ./app/prompts /go/prompts + +ENTRYPOINT ["bash", "-c", "/go/terminal"] + FROM public.ecr.aws/docker/library/debian:bookworm-slim as production # install git diff --git a/app/config/config.go b/app/config/config.go index 5a8a2c11..7a1fc426 100644 --- a/app/config/config.go +++ b/app/config/config.go @@ -42,6 +42,7 @@ func LoadConfig() (*koanf.Koanf, error) { "aws": map[string]interface{}{ "region": "us-west-2", }, + "terminal.allowed.host": "localhost", }, "."), nil) if err != nil { return nil, err diff --git a/app/config/terminal_config.go b/app/config/terminal_config.go new file mode 100644 index 00000000..d84cc9bb --- /dev/null +++ b/app/config/terminal_config.go @@ -0,0 +1,5 @@ +package config + +func GetAllowedHost() string { + return config.String("terminal.allowed.host") +} diff --git a/app/controllers/terminal.go b/app/controllers/terminal.go new file mode 100644 index 00000000..4ab4697c --- /dev/null +++ b/app/controllers/terminal.go @@ -0,0 +1,296 @@ +package controllers + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "os" + "os/exec" + "strings" + "sync" + "time" + + "ai-developer/app/types/request" + "ai-developer/app/utils" + + "github.com/creack/pty" + "github.com/gin-gonic/gin" + "github.com/gorilla/websocket" + "go.uber.org/zap" +) + +type TTYSize struct { + Cols uint16 `json:"cols"` + Rows uint16 `json:"rows"` + X uint16 `json:"x"` + Y uint16 `json:"y"` +} + +var WebsocketMessageType = map[int]string{ + websocket.BinaryMessage: "binary", + websocket.TextMessage: "text", + websocket.CloseMessage: "close", + websocket.PingMessage: "ping", + websocket.PongMessage: "pong", +} + +type TerminalController struct { + MaxBufferSizeBytes int + KeepalivePingTimeout time.Duration + ConnectionErrorLimit int + cmd *exec.Cmd + Command string + Arguments []string + AllowedHostnames []string + logger *zap.Logger + tty *os.File + cancelFunc context.CancelFunc + writeMutex sync.Mutex + historyBuffer bytes.Buffer +} + +func NewTerminalController(logger *zap.Logger, command string, arguments []string, allowedHostnames []string) (*TerminalController, error) { + cmd := exec.Command(command, arguments...) + tty, err := pty.Start(cmd) + if err != nil { + logger.Warn("failed to start command", zap.Error(err)) + return nil, err + } + ttyBuffer := bytes.Buffer{} + return &TerminalController{ + MaxBufferSizeBytes: 1024, + KeepalivePingTimeout: 20 * time.Second, + ConnectionErrorLimit: 10, + tty: tty, + cmd: cmd, + Arguments: arguments, + AllowedHostnames: allowedHostnames, + logger: logger, + historyBuffer: ttyBuffer, + }, nil +} + +func (controller *TerminalController) RunCommand(ctx *gin.Context) { + var commandRequest request.RunCommandRequest + if err := ctx.ShouldBindJSON(&commandRequest); err != nil { + ctx.JSON(400, gin.H{"error": err.Error()}) + return + } + command := commandRequest.Command + if command == "" { + ctx.JSON(400, gin.H{"error": "command is required"}) + return + } + if !strings.HasSuffix(command, "\n") { + command += "\n" + } + + _, err := controller.tty.Write([]byte(command)) + if err != nil { + return + } +} + +func (controller *TerminalController) NewTerminal(ctx *gin.Context) { + subCtx, cancelFunc := context.WithCancel(ctx) + controller.cancelFunc = cancelFunc + + controller.logger.Info("setting up new terminal connection...") + + connection, err := controller.setupConnection(ctx, ctx.Writer, ctx.Request) + defer func(connection *websocket.Conn) { + controller.logger.Info("closing websocket connection...") + err := connection.Close() + if err != nil { + controller.logger.Warn("failed to close connection", zap.Error(err)) + } + }(connection) + if err != nil { + controller.logger.Warn("failed to setup connection", zap.Error(err)) + return + } + + // restore history from buffer + controller.writeMutex.Lock() + if err := connection.WriteMessage(websocket.BinaryMessage, controller.historyBuffer.Bytes()); err != nil { + controller.logger.Info("failed to write tty buffer to xterm.js", zap.Error(err)) + } + controller.writeMutex.Unlock() + + var waiter sync.WaitGroup + + waiter.Add(3) + + go controller.keepAlive(subCtx, connection, &waiter) + + go controller.readFromTTY(subCtx, connection, &waiter) + + go controller.writeToTTY(subCtx, connection, &waiter) + + waiter.Wait() + + controller.logger.Info("closing connection...") +} + +func (controller *TerminalController) setupConnection(ctx context.Context, w gin.ResponseWriter, r *http.Request) (*websocket.Conn, error) { + upgrader := utils.GetConnectionUpgrader(controller.AllowedHostnames, controller.MaxBufferSizeBytes) + connection, err := upgrader.Upgrade(w, r, nil) + if err != nil { + return nil, err + } + return connection, nil +} + +func (controller *TerminalController) keepAlive(ctx context.Context, connection *websocket.Conn, waiter *sync.WaitGroup) { + defer func() { + waiter.Done() + controller.logger.Info("keepAlive goroutine exiting...") + }() + lastPongTime := time.Now() + keepalivePingTimeout := controller.KeepalivePingTimeout + + connection.SetPongHandler(func(msg string) error { + lastPongTime = time.Now() + return nil + }) + + for { + select { + case <-ctx.Done(): + controller.logger.Info("done keeping alive...") + return + default: + controller.logger.Info("sending keepalive ping message...") + controller.writeMutex.Lock() + if err := connection.WriteMessage(websocket.PingMessage, []byte("keepalive")); err != nil { + controller.writeMutex.Unlock() + controller.logger.Error("failed to write ping message", zap.Error(err)) + if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) { + controller.cancelFunc() + } + return + } + controller.writeMutex.Unlock() + + time.Sleep(keepalivePingTimeout / 2) + + if time.Now().Sub(lastPongTime) > keepalivePingTimeout { + controller.logger.Warn("failed to get response from ping, triggering disconnect now...") + return + } + controller.logger.Info("received response from ping successfully") + } + } +} + +func (controller *TerminalController) readFromTTY(ctx context.Context, connection *websocket.Conn, waiter *sync.WaitGroup) { + defer func() { + waiter.Done() + controller.logger.Info("readFromTTY goroutine exiting...") + }() + errorCounter := 0 + buffer := make([]byte, controller.MaxBufferSizeBytes) + for { + select { + case <-ctx.Done(): + controller.logger.Info("done reading from tty...") + return + default: + + readLength, err := controller.tty.Read(buffer) + if err != nil { + controller.logger.Warn("failed to read from tty", zap.Error(err)) + controller.writeMutex.Lock() + if err := connection.WriteMessage(websocket.TextMessage, []byte("bye!")); err != nil { + controller.logger.Warn("failed to send termination message from tty to xterm.js", zap.Error(err)) + } + controller.writeMutex.Unlock() + return + } + + controller.writeMutex.Lock() + // save to history buffer + controller.historyBuffer.Write(buffer[:readLength]) + if err := connection.WriteMessage(websocket.BinaryMessage, buffer[:readLength]); err != nil { + controller.writeMutex.Unlock() + controller.logger.Warn(fmt.Sprintf("failed to send %v bytes from tty to xterm.js", readLength), zap.Int("read_length", readLength), zap.Error(err)) + errorCounter++ + if errorCounter > controller.ConnectionErrorLimit { + return + } + if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) { + controller.logger.Info("WebSocket closed by client") + controller.cancelFunc() + return + } + continue + } + controller.writeMutex.Unlock() + + controller.logger.Info(fmt.Sprintf("sent message of size %v bytes from tty to xterm.js", readLength), zap.Int("read_length", readLength)) + errorCounter = 0 + } + } +} + +func (controller *TerminalController) writeToTTY(ctx context.Context, connection *websocket.Conn, waiter *sync.WaitGroup) { + defer func() { + waiter.Done() + controller.logger.Info("writeToTTY goroutine exiting...") + }() + for { + select { + case <-ctx.Done(): + controller.logger.Info("done writing from tty...") + return + default: + + messageType, data, err := connection.ReadMessage() + if err != nil { + if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) { + controller.logger.Info("WebSocket closed by client") + controller.cancelFunc() + return + } + controller.logger.Warn("failed to get next reader", zap.Error(err)) + return + } + + dataLength := len(data) + dataBuffer := bytes.Trim(data, "\x00") + dataType := WebsocketMessageType[messageType] + + controller.logger.Info(fmt.Sprintf("received %s (type: %v) message of size %v byte(s) from xterm.js with key sequence: %v", dataType, messageType, dataLength, dataBuffer)) + + if messageType == websocket.BinaryMessage && dataBuffer[0] == 1 { + controller.resizeTTY(dataBuffer) + continue + } + + bytesWritten, err := controller.tty.Write(dataBuffer) + if err != nil { + controller.logger.Error(fmt.Sprintf("failed to write %v bytes to tty: %s", len(dataBuffer), err), zap.Int("bytes_written", bytesWritten), zap.Error(err)) + continue + } + controller.logger.Info("bytes written to tty...", zap.Int("bytes_written", bytesWritten)) + } + } +} + +func (controller *TerminalController) resizeTTY(dataBuffer []byte) { + ttySize := &TTYSize{} + resizeMessage := bytes.Trim(dataBuffer[1:], " \n\r\t\x00\x01") + if err := json.Unmarshal(resizeMessage, ttySize); err != nil { + controller.logger.Warn(fmt.Sprintf("failed to unmarshal received resize message '%s'", resizeMessage), zap.ByteString("resizeMessage", resizeMessage), zap.Error(err)) + return + } + controller.logger.Info("resizing tty ", zap.Uint16("rows", ttySize.Rows), zap.Uint16("cols", ttySize.Cols)) + if err := pty.Setsize(controller.tty, &pty.Winsize{ + Rows: ttySize.Rows, + Cols: ttySize.Cols, + }); err != nil { + controller.logger.Warn("failed to resize tty", zap.Error(err)) + } +} diff --git a/app/types/request/run_command_request.go b/app/types/request/run_command_request.go new file mode 100644 index 00000000..468bd502 --- /dev/null +++ b/app/types/request/run_command_request.go @@ -0,0 +1,5 @@ +package request + +type RunCommandRequest struct { + Command string `json:"command"` +} diff --git a/app/utils/websocket_connection_upgrader.go b/app/utils/websocket_connection_upgrader.go new file mode 100644 index 00000000..74050e66 --- /dev/null +++ b/app/utils/websocket_connection_upgrader.go @@ -0,0 +1,33 @@ +package utils + +import ( + "fmt" + "net/http" + "strings" + + "github.com/gorilla/websocket" +) + +func GetConnectionUpgrader( + allowedHostnames []string, + maxBufferSizeBytes int, +) websocket.Upgrader { + return websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + requesterHostname := r.Host + if strings.Index(requesterHostname, ":") != -1 { + requesterHostname = strings.Split(requesterHostname, ":")[0] + } + for _, allowedHostname := range allowedHostnames { + if strings.HasSuffix(requesterHostname, allowedHostname) { + return true + } + } + fmt.Printf("failed to find '%s' in the list of allowed hostnames ('%s')\n", requesterHostname) + return false + }, + HandshakeTimeout: 0, + ReadBufferSize: maxBufferSizeBytes, + WriteBufferSize: maxBufferSizeBytes, + } +} diff --git a/docker-compose.yaml b/docker-compose.yaml index 93a99ef4..50beb0d2 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -118,6 +118,18 @@ services: dockerfile: Dockerfile target: node-executor + terminal: + restart: no + hostname: terminal + container_name: terminal + image: terminal:latest + ports: + - 8084:8080 + build: + context: . + dockerfile: Dockerfile + target: terminal + ws: hostname: ws restart: always diff --git a/go.mod b/go.mod index f67779e9..d0ccf1cf 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,16 @@ module ai-developer go 1.22.3 require ( + github.com/creack/pty v1.1.21 github.com/dgrijalva/jwt-go v3.2.0+incompatible + github.com/gin-contrib/zap v1.1.3 github.com/gin-gonic/gin v1.10.0 github.com/go-git/go-git/v5 v5.12.0 github.com/goccy/go-json v0.10.2 github.com/google/go-github v17.0.0+incompatible github.com/google/uuid v1.4.0 github.com/googollee/go-socket.io v1.7.0 + github.com/gorilla/websocket v1.5.3 github.com/hibiken/asynq v0.24.1 github.com/knadh/koanf/providers/confmap v0.1.0 github.com/knadh/koanf/providers/env v0.1.0 @@ -42,7 +45,6 @@ require ( github.com/emirpasic/gods v1.18.1 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gin-contrib/sse v0.1.0 // indirect - github.com/gin-contrib/zap v1.1.3 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/go-playground/locales v0.14.1 // indirect @@ -54,7 +56,6 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/gomodule/redigo v1.9.2 // indirect github.com/google/go-querystring v1.1.0 // indirect - github.com/gorilla/websocket v1.5.3 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgx/v5 v5.4.3 // indirect diff --git a/go.sum b/go.sum index e37b4852..095bc804 100644 --- a/go.sum +++ b/go.sum @@ -29,6 +29,8 @@ github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/ github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= +github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/gui/package.json b/gui/package.json index 2aa1006d..e48bf77f 100644 --- a/gui/package.json +++ b/gui/package.json @@ -12,6 +12,8 @@ "dependencies": { "@monaco-editor/react": "^4.6.0", "@nextui-org/react": "^2.3.6", + "@xterm/addon-fit": "^0.10.0", + "@xterm/xterm": "^5.5.0", "axios": "^1.7.2", "babel-plugin-styled-components": "^2.1.4", "cookie": "^0.6.0", @@ -33,7 +35,13 @@ "react-transition-group": "^4.4.5", "sharp": "^0.33.4", "socket.io-client": "^2.5.0", - "styled-components": "^6.1.11" + "styled-components": "^6.1.11", + "xterm": "^5.3.0", + "xterm-addon-attach": "^0.9.0", + "xterm-addon-fit": "^0.8.0", + "xterm-addon-serialize": "^0.11.0", + "xterm-addon-unicode11": "^0.6.0", + "xterm-addon-web-links": "^0.9.0" }, "devDependencies": { "@types/cookie": "^0.6.0", diff --git a/gui/public/icons/browser_icon_dark.svg b/gui/public/icons/browser_icon_dark.svg index 1ae018b3..ff928e77 100644 --- a/gui/public/icons/browser_icon_dark.svg +++ b/gui/public/icons/browser_icon_dark.svg @@ -1,10 +1,10 @@ - + - - + + diff --git a/gui/public/icons/selected/story_details.svg b/gui/public/icons/selected/story_details.svg new file mode 100644 index 00000000..8dec5670 --- /dev/null +++ b/gui/public/icons/selected/story_details.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/gui/public/icons/selected/terminal.svg b/gui/public/icons/selected/terminal.svg new file mode 100644 index 00000000..e8bbddbc --- /dev/null +++ b/gui/public/icons/selected/terminal.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/gui/public/icons/unselected/story_details.svg b/gui/public/icons/unselected/story_details.svg new file mode 100644 index 00000000..4fa27471 --- /dev/null +++ b/gui/public/icons/unselected/story_details.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/gui/public/icons/unselected/terminal.svg b/gui/public/icons/unselected/terminal.svg new file mode 100644 index 00000000..71049f0e --- /dev/null +++ b/gui/public/icons/unselected/terminal.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/gui/src/app/(programmer)/board/page.tsx b/gui/src/app/(programmer)/board/page.tsx index 19ce982a..a5bc7f79 100644 --- a/gui/src/app/(programmer)/board/page.tsx +++ b/gui/src/app/(programmer)/board/page.tsx @@ -88,10 +88,6 @@ export default function Board() { setOpenCreateStoryModal(true); }; - const handleSearchChange = (search: string) => { - toGetAllStoriesOfProject(); - }; - useEffect(() => { toGetAllStoriesOfProject(); }, [searchValue]); diff --git a/gui/src/app/_app.css b/gui/src/app/_app.css index 721d00ba..0728d127 100644 --- a/gui/src/app/_app.css +++ b/gui/src/app/_app.css @@ -280,6 +280,16 @@ body { border-radius: 8px 0 0; } +.terminal { + height: 100%; + width: 100%; + border: 1px solid var(--white-opacity-8); + background: var(--layout-bg-color); + color: #FFF; + padding: 10px; + border-radius: 8px; +} + .green-text { color: #32de84; -} +} \ No newline at end of file diff --git a/gui/src/app/imagePath.tsx b/gui/src/app/imagePath.tsx index 52dbb489..7145a177 100644 --- a/gui/src/app/imagePath.tsx +++ b/gui/src/app/imagePath.tsx @@ -34,6 +34,10 @@ export default { designIconUnselected: '/icons/unselected/design.svg', visualDiffIconSelected: '/icons/selected/visual_diff.svg', visualDiffIconUnselected: '/icons/unselected/visual_diff.svg', + storyDetailsIconSelected: '/icons/selected/story_details.svg', + storyDetailsIconUnselected: '/icons/unselected/story_details.svg', + terminalIconSelected: '/icons/selected/terminal.svg', + terminalIconUnselected: '/icons/unselected/terminal.svg', projectIcon: '/icons/project_icon.svg', bottomArrowGrey: '/arrows/bottom_arrow_grey.svg', bottomArrowWhite: '/arrows/bottom_arrow_white.svg', diff --git a/gui/src/app/utils.tsx b/gui/src/app/utils.tsx index acc62170..7cde6539 100644 --- a/gui/src/app/utils.tsx +++ b/gui/src/app/utils.tsx @@ -226,3 +226,16 @@ export function handleStoryInReviewIssue(data: { actions, }; } + +export function insertBeforePhrase(originalString, wordToInsert, phraseToFind) { + let index = originalString.indexOf(phraseToFind); + + if (index !== -1) { + let beforePhrase = originalString.substring(0, index); + let afterPhrase = originalString.substring(index); + + return beforePhrase + wordToInsert + afterPhrase; + } else { + return originalString; + } +} diff --git a/gui/src/components/CustomContainers/container.module.css b/gui/src/components/CustomContainers/container.module.css index f2ec22e3..39b23c53 100644 --- a/gui/src/components/CustomContainers/container.module.css +++ b/gui/src/components/CustomContainers/container.module.css @@ -25,7 +25,7 @@ display: flex; flex-direction: row; gap: 4px; - padding: 8px; + padding: 4px; border-bottom: 1px solid var(--white-opacity-8); cursor: pointer; } @@ -38,8 +38,20 @@ padding: 4px 8px; gap: 8px; border-radius: 8px; + overflow: hidden; + text-overflow: ellipsis; + font-family: "Proxima Nova", sans-serif; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 17px; +} + +.tab_item { + color: #888; } .tab_item_selected { - background: rgb(255 255 255 / 6%); + background: var(--white-opacity-6); + color: #FFFF; } diff --git a/gui/src/components/Terminal/Terminal.tsx b/gui/src/components/Terminal/Terminal.tsx new file mode 100644 index 00000000..5682c33b --- /dev/null +++ b/gui/src/components/Terminal/Terminal.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { useTerminal } from '@/hooks/useTerminal'; + +const TerminalComponent: React.FC = () => { + const terminalRef = useTerminal(); + + return ( +
+ ); +}; + +export default TerminalComponent; diff --git a/gui/src/components/Terminal/terminal.module.css b/gui/src/components/Terminal/terminal.module.css new file mode 100644 index 00000000..c20d5c84 --- /dev/null +++ b/gui/src/components/Terminal/terminal.module.css @@ -0,0 +1,9 @@ +.terminal { + height: 100%; + width: 100%; + border: 1px solid var(--white-opacity-8); + background: var(--layout-bg-color); + color: #FFFFFF; + padding: 10px; + border-radius: 8px; +} \ No newline at end of file diff --git a/gui/src/components/WorkBenchComponents/BackendWorkbench.tsx b/gui/src/components/WorkBenchComponents/BackendWorkbench.tsx index d9abc26e..5955aaef 100644 --- a/gui/src/components/WorkBenchComponents/BackendWorkbench.tsx +++ b/gui/src/components/WorkBenchComponents/BackendWorkbench.tsx @@ -7,6 +7,7 @@ import { CustomTabsNewProps } from '../../../types/customComponentTypes'; import imagePath from '@/app/imagePath'; import Browser from '@/components/WorkBenchComponents/Browser'; import { BackendWorkbenchProps } from '../../../types/workbenchTypes'; +import TerminalComponent from '@/components/Terminal/Terminal'; const BackendWorkbench: React.FC = ({ activityLogs, @@ -21,7 +22,8 @@ const BackendWorkbench: React.FC = ({ frontendURL.current = localStorage.getItem('projectURLFrontend'); } }, []); - const tabsProps: CustomTabsNewProps = { + + const browserTabsProps: CustomTabsNewProps = { options: [ { key: 'backend', @@ -38,6 +40,27 @@ const BackendWorkbench: React.FC = ({ ], }; + const actionTabsProps: CustomTabsNewProps = { + options: [ + { + key: 'story_details', + text: 'Story Details', + icon: imagePath.storyDetailsIconSelected, + content: ( + + + + ), + }, + { + key: 'terminal', + text: 'Terminal', + icon: imagePath.terminalIconSelected, + content: , + }, + ], + }; + return (
= ({ alignment={'items-center justify-center'} header={'Browser'} height={'100%'} - tabsProps={tabsProps} + tabsProps={browserTabsProps} type={'tabs'} />
- - - - + tabsProps={actionTabsProps} + type={'tabs'} + />
diff --git a/gui/src/components/WorkBenchComponents/Browser.tsx b/gui/src/components/WorkBenchComponents/Browser.tsx index ac43f85b..97077cec 100644 --- a/gui/src/components/WorkBenchComponents/Browser.tsx +++ b/gui/src/components/WorkBenchComponents/Browser.tsx @@ -18,7 +18,7 @@ export default function Browser({ return (
{showUrl && ( )}