From 82c8d65a7de427224530146d60e97dfdf7347b76 Mon Sep 17 00:00:00 2001 From: Acbox Liu Date: Wed, 11 Mar 2026 21:49:05 +0800 Subject: [PATCH] feat: add interactive web terminal for bot containers (#232) * feat(terminal): add interactive web terminal for bot containers Add WebSocket-based terminal endpoint (/container/terminal/ws) that provides a full PTY shell session inside the bot's MCP container. Extend the gRPC proto with pty and resize fields, implement PTY exec on the container side using creack/pty, and add an xterm.js-based terminal component in the frontend bot detail page. * chore: add /mcp in .gitignore * feat(terminal): add multi-tab support, localStorage cache, and reactivity fixes - Support unlimited terminal tabs with add/close/switch - Cache terminal content to localStorage via SerializeAddon for session persistence - Use shallowReactive for tab objects to ensure status updates trigger UI reactivity - Fix listener leak by tracking and disposing onData/onResize on reconnect - Fix bottom clipping by using inset offsets instead of padding --- .gitignore | 1 + apps/web/package.json | 3 + apps/web/src/composables/useTerminalCache.ts | 65 +++ apps/web/src/i18n/locales/en.json | 14 + apps/web/src/i18n/locales/zh.json | 14 + .../pages/bots/components/bot-terminal.vue | 458 ++++++++++++++++++ apps/web/src/pages/bots/detail.vue | 2 + cmd/mcp/server.go | 80 ++- go.mod | 1 + go.sum | 2 + internal/handlers/containerd.go | 3 + internal/handlers/containerd_terminal.go | 167 +++++++ internal/mcp/mcpclient/client.go | 28 ++ internal/mcp/mcpcontainer/mcpcontainer.pb.go | 247 ++++++---- internal/mcp/mcpcontainer/mcpcontainer.proto | 7 + .../mcp/mcpcontainer/mcpcontainer_grpc.pb.go | 26 +- pnpm-lock.yaml | 24 + 17 files changed, 1037 insertions(+), 105 deletions(-) create mode 100644 apps/web/src/composables/useTerminalCache.ts create mode 100644 apps/web/src/pages/bots/components/bot-terminal.vue create mode 100644 internal/handlers/containerd_terminal.go diff --git a/.gitignore b/.gitignore index 727465b3..215184b9 100644 --- a/.gitignore +++ b/.gitignore @@ -93,6 +93,7 @@ tmp/ # compiled files /memoh /agent +/mcp docs/docs/.vitepress/cache .pnpm-store diff --git a/apps/web/package.json b/apps/web/package.json index 30b65bfb..fa02ae1b 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -22,6 +22,9 @@ "@tanstack/vue-table": "^8.21.3", "@vee-validate/zod": "^4.15.1", "@vueuse/core": "^14.1.0", + "@xterm/addon-fit": "^0.11.0", + "@xterm/addon-serialize": "^0.14.0", + "@xterm/xterm": "^6.0.0", "dotenv": "^17.2.3", "echarts": "^6.0.0", "katex": "^0.16.28", diff --git a/apps/web/src/composables/useTerminalCache.ts b/apps/web/src/composables/useTerminalCache.ts new file mode 100644 index 00000000..0d41d400 --- /dev/null +++ b/apps/web/src/composables/useTerminalCache.ts @@ -0,0 +1,65 @@ +const STORAGE_PREFIX = 'terminal-cache:' +const MAX_CONTENT_BYTES = 100 * 1024 +const MAX_CACHED_TABS = 10 + +export interface TerminalTabState { + id: string + label: string + content: string + savedAt: number +} + +export interface TerminalCacheState { + tabs: TerminalTabState[] + activeTabId: string +} + +function storageKey(botId: string): string { + return `${STORAGE_PREFIX}${botId}` +} + +function truncateContent(content: string): string { + if (content.length <= MAX_CONTENT_BYTES) return content + return content.slice(content.length - MAX_CONTENT_BYTES) +} + +export function useTerminalCache() { + function loadCache(botId: string): TerminalCacheState | null { + try { + const raw = localStorage.getItem(storageKey(botId)) + if (!raw) return null + const parsed = JSON.parse(raw) as TerminalCacheState + if (!Array.isArray(parsed.tabs) || !parsed.activeTabId) return null + return parsed + } catch { + return null + } + } + + function saveCache(botId: string, state: TerminalCacheState) { + try { + const trimmed: TerminalCacheState = { + activeTabId: state.activeTabId, + tabs: state.tabs.slice(0, MAX_CACHED_TABS).map((tab) => ({ + id: tab.id, + label: tab.label, + content: truncateContent(tab.content), + savedAt: Date.now(), + })), + } + localStorage.setItem(storageKey(botId), JSON.stringify(trimmed)) + } catch { + // localStorage full or unavailable + } + } + + function clearCache(botId: string) { + try { + localStorage.removeItem(storageKey(botId)) + } catch { + // ignore + } + } + + return { loadCache, saveCache, clearCache } +} diff --git a/apps/web/src/i18n/locales/en.json b/apps/web/src/i18n/locales/en.json index 1bdb9608..af2ce4a4 100644 --- a/apps/web/src/i18n/locales/en.json +++ b/apps/web/src/i18n/locales/en.json @@ -493,8 +493,22 @@ "skills": "Skills", "email": "Email", "files": "Files", + "terminal": "Terminal", "settings": "Settings" }, + "terminal": { + "title": "Terminal", + "reconnect": "Reconnect", + "newTab": "New Terminal", + "closeTab": "Close", + "defaultTabLabel": "Terminal", + "status": { + "idle": "Idle", + "connecting": "Connecting...", + "connected": "Connected", + "disconnected": "Disconnected" + } + }, "files": { "name": "Name", "size": "Size", diff --git a/apps/web/src/i18n/locales/zh.json b/apps/web/src/i18n/locales/zh.json index 58519810..a81e42ba 100644 --- a/apps/web/src/i18n/locales/zh.json +++ b/apps/web/src/i18n/locales/zh.json @@ -489,8 +489,22 @@ "skills": "技能", "email": "邮件", "files": "文件", + "terminal": "终端", "settings": "设置" }, + "terminal": { + "title": "终端", + "reconnect": "重新连接", + "newTab": "新建终端", + "closeTab": "关闭", + "defaultTabLabel": "终端", + "status": { + "idle": "空闲", + "connecting": "连接中...", + "connected": "已连接", + "disconnected": "已断开" + } + }, "files": { "name": "名称", "size": "大小", diff --git a/apps/web/src/pages/bots/components/bot-terminal.vue b/apps/web/src/pages/bots/components/bot-terminal.vue new file mode 100644 index 00000000..cbf6b109 --- /dev/null +++ b/apps/web/src/pages/bots/components/bot-terminal.vue @@ -0,0 +1,458 @@ + + + + + diff --git a/apps/web/src/pages/bots/detail.vue b/apps/web/src/pages/bots/detail.vue index 18a44223..00159bfd 100644 --- a/apps/web/src/pages/bots/detail.vue +++ b/apps/web/src/pages/bots/detail.vue @@ -248,6 +248,7 @@ import BotSubagents from './components/bot-subagents.vue' import BotOverview from './components/bot-overview.vue' import BotSchedule from './components/bot-schedule.vue' import BotContainer from './components/bot-container.vue' +import BotTerminal from './components/bot-terminal.vue' import BotFiles from './components/bot-files.vue' import { resolveApiErrorMessage } from '@/utils/api-error' import { useAvatarInitials } from '@/composables/useAvatarInitials' @@ -280,6 +281,7 @@ const tabList = computed(() => { { value: 'channels', label: 'bots.tabs.channels', component: BotChannels, params: { 'bot-id': bot_id } }, { value: 'email', label: 'bots.tabs.email', component: BotEmail, params: { 'bot-id': bot_id } }, { value: 'container', label: 'bots.tabs.container', component: BotContainer, params: {} }, + { value: 'terminal', label: 'bots.tabs.terminal', component: BotTerminal, params: { 'bot-id': bot_id } }, { value: 'files', label: 'bots.tabs.files', component: BotFiles, params: { 'bot-id': bot_id, 'bot-type': bot.value?.type } }, { value: 'mcp', label: 'bots.tabs.mcp', component: BotMcp, params: { 'bot-id': bot_id } }, { value: 'subagents', label: 'bots.tabs.subagents', component: BotSubagents, params: { 'bot-id': bot_id } }, diff --git a/cmd/mcp/server.go b/cmd/mcp/server.go index 304f08b2..333b6805 100644 --- a/cmd/mcp/server.go +++ b/cmd/mcp/server.go @@ -16,6 +16,7 @@ import ( "time" "unicode/utf8" + "github.com/creack/pty" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -171,7 +172,6 @@ func (*containerServer) ListDir(_ context.Context, req *pb.ListDirRequest) (*pb. } func (*containerServer) Exec(stream pb.ContainerService_ExecServer) error { - // Receive first message to get command details firstMsg, err := stream.Recv() if err != nil { return status.Error(codes.InvalidArgument, "failed to receive exec config") @@ -182,6 +182,77 @@ func (*containerServer) Exec(stream pb.ContainerService_ExecServer) error { return status.Error(codes.InvalidArgument, "command is required") } + if firstMsg.GetPty() { + return execPTY(stream, firstMsg) + } + return execPipe(stream, firstMsg) +} + +func execPTY(stream pb.ContainerService_ExecServer, firstMsg *pb.ExecInput) error { + command := firstMsg.GetCommand() + workDir := firstMsg.GetWorkDir() + if workDir == "" { + workDir = defaultWorkDir + } + + cmd := exec.CommandContext(stream.Context(), "/bin/sh", "-c", command) //nolint:gosec // G204: intentional + cmd.Dir = workDir + cmd.Env = append(os.Environ(), firstMsg.GetEnv()...) + cmd.Env = append(cmd.Env, "TERM=xterm-256color") + + initialSize := &pty.Winsize{Rows: 24, Cols: 80} + if r := firstMsg.GetResize(); r != nil && r.GetCols() > 0 && r.GetRows() > 0 { + initialSize.Rows = uint16(r.GetRows()) //nolint:gosec // G115 + initialSize.Cols = uint16(r.GetCols()) //nolint:gosec // G115 + } + + ptmx, err := pty.StartWithSize(cmd, initialSize) + if err != nil { + return status.Errorf(codes.Internal, "pty start: %v", err) + } + defer func() { _ = ptmx.Close() }() + + // stdin + resize from stream + go func() { + for { + msg, recvErr := stream.Recv() + if recvErr != nil { + return + } + if r := msg.GetResize(); r != nil && r.GetCols() > 0 && r.GetRows() > 0 { + _ = pty.Setsize(ptmx, &pty.Winsize{ + Rows: uint16(r.GetRows()), //nolint:gosec // G115 + Cols: uint16(r.GetCols()), //nolint:gosec // G115 + }) + } + if data := msg.GetStdinData(); len(data) > 0 { + _, _ = ptmx.Write(data) + } + } + }() + + // PTY output -> stream (single fd merges stdout+stderr) + streamPipe(stream, ptmx, pb.ExecOutput_STDOUT) + + exitCode := int32(0) + if waitErr := cmd.Wait(); waitErr != nil { + exitErr := &exec.ExitError{} + if errors.As(waitErr, &exitErr) { + ec := exitErr.ExitCode() + exitCode = int32(max(math.MinInt32, min(math.MaxInt32, ec))) //nolint:gosec // G115 + } else { + exitCode = -1 + } + } + + return stream.Send(&pb.ExecOutput{ + Stream: pb.ExecOutput_EXIT, + ExitCode: exitCode, + }) +} + +func execPipe(stream pb.ContainerService_ExecServer, firstMsg *pb.ExecInput) error { + command := firstMsg.GetCommand() workDir := firstMsg.GetWorkDir() if workDir == "" { workDir = defaultWorkDir @@ -201,7 +272,6 @@ func (*containerServer) Exec(stream pb.ContainerService_ExecServer) error { cmd.Env = append(os.Environ(), firstMsg.GetEnv()...) } - // Setup stdin pipe for bidirectional streaming stdinPipe, err := cmd.StdinPipe() if err != nil { return status.Errorf(codes.Internal, "stdin pipe: %v", err) @@ -220,11 +290,10 @@ func (*containerServer) Exec(stream pb.ContainerService_ExecServer) error { return status.Errorf(codes.Internal, "start: %v", err) } - // Handle stdin from stream go func() { for { - msg, err := stream.Recv() - if err != nil { + msg, recvErr := stream.Recv() + if recvErr != nil { _ = stdinPipe.Close() return } @@ -234,7 +303,6 @@ func (*containerServer) Exec(stream pb.ContainerService_ExecServer) error { } }() - // Stream stdout/stderr to client done := make(chan struct{}) go func() { defer close(done) diff --git a/go.mod b/go.mod index 2a745b11..3d5f8590 100644 --- a/go.mod +++ b/go.mod @@ -58,6 +58,7 @@ require ( github.com/containerd/ttrpc v1.2.7 // indirect github.com/containerd/typeurl/v2 v2.2.3 // indirect github.com/containernetworking/cni v1.3.0 // indirect + github.com/creack/pty v1.1.24 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/distribution/reference v0.6.0 // indirect diff --git a/go.sum b/go.sum index 5d726cce..436cb4f9 100644 --- a/go.sum +++ b/go.sum @@ -61,6 +61,8 @@ github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsx github.com/containernetworking/cni v1.3.0 h1:v6EpN8RznAZj9765HhXQrtXgX+ECGebEYEmnuFjskwo= github.com/containernetworking/cni v1.3.0/go.mod h1:Bs8glZjjFfGPHMw6hQu82RUgEPNGEaBb9KS5KtNMnJ4= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/internal/handlers/containerd.go b/internal/handlers/containerd.go index 1310ad2e..252aa0fe 100644 --- a/internal/handlers/containerd.go +++ b/internal/handlers/containerd.go @@ -142,6 +142,9 @@ func (h *ContainerdHandler) Register(e *echo.Echo) { group.GET("/skills", h.ListSkills) group.POST("/skills", h.UpsertSkills) group.DELETE("/skills", h.DeleteSkills) + // Terminal routes + group.GET("/terminal", h.GetTerminalInfo) + group.GET("/terminal/ws", h.HandleTerminalWS) // File manager routes group.GET("/fs", h.FSStat) group.GET("/fs/list", h.FSList) diff --git a/internal/handlers/containerd_terminal.go b/internal/handlers/containerd_terminal.go new file mode 100644 index 00000000..c26fef65 --- /dev/null +++ b/internal/handlers/containerd_terminal.go @@ -0,0 +1,167 @@ +package handlers + +import ( + "encoding/json" + "log/slog" + "net/http" + "strconv" + + "github.com/gorilla/websocket" + "github.com/labstack/echo/v4" + + pb "github.com/memohai/memoh/internal/mcp/mcpcontainer" +) + +var terminalUpgrader = websocket.Upgrader{ + CheckOrigin: func(_ *http.Request) bool { return true }, +} + +type terminalInfoResponse struct { + Available bool `json:"available"` + Shell string `json:"shell"` +} + +type terminalControlMessage struct { + Type string `json:"type"` + Cols uint32 `json:"cols,omitempty"` + Rows uint32 `json:"rows,omitempty"` +} + +// GetTerminalInfo godoc +// @Summary Check terminal availability for bot container +// @Tags containerd +// @Param bot_id path string true "Bot ID" +// @Success 200 {object} terminalInfoResponse +// @Failure 404 {object} ErrorResponse +// @Router /bots/{bot_id}/container/terminal [get]. +func (h *ContainerdHandler) GetTerminalInfo(c echo.Context) error { + botID, err := h.requireBotAccess(c) + if err != nil { + return err + } + ctx := c.Request().Context() + + if h.manager == nil { + return c.JSON(http.StatusOK, terminalInfoResponse{Available: false}) + } + + client, clientErr := h.manager.MCPClient(ctx, botID) + if clientErr != nil || client == nil { + return c.JSON(http.StatusOK, terminalInfoResponse{Available: false}) + } + + return c.JSON(http.StatusOK, terminalInfoResponse{ + Available: true, + Shell: "/bin/sh", + }) +} + +// HandleTerminalWS godoc +// @Summary Interactive WebSocket terminal for bot container +// @Tags containerd +// @Param bot_id path string true "Bot ID" +// @Param cols query int false "Initial terminal columns" default(80) +// @Param rows query int false "Initial terminal rows" default(24) +// @Param token query string false "Auth token" +// @Success 101 "WebSocket upgrade" +// @Failure 400 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Router /bots/{bot_id}/container/terminal/ws [get]. +func (h *ContainerdHandler) HandleTerminalWS(c echo.Context) error { + botID, err := h.requireBotAccess(c) + if err != nil { + return err + } + ctx := c.Request().Context() + + if h.manager == nil { + return echo.NewHTTPError(http.StatusInternalServerError, "manager not configured") + } + + client, err := h.manager.MCPClient(ctx, botID) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "container not reachable: "+err.Error()) + } + + cols := parseUint32Query(c, "cols", 80) + rows := parseUint32Query(c, "rows", 24) + + conn, err := terminalUpgrader.Upgrade(c.Response(), c.Request(), nil) + if err != nil { + return err + } + defer func() { _ = conn.Close() }() + + execStream, err := client.ExecStreamPTY(ctx, "/bin/sh", "/data", cols, rows) + if err != nil { + _ = conn.WriteMessage(websocket.CloseMessage, + websocket.FormatCloseMessage(websocket.CloseInternalServerErr, "exec failed")) + return nil + } + defer func() { _ = execStream.Close() }() + + done := make(chan struct{}) + + // gRPC output -> WebSocket + go func() { + defer close(done) + for { + output, recvErr := execStream.Recv() + if recvErr != nil { + return + } + switch output.GetStream() { + case pb.ExecOutput_STDOUT, pb.ExecOutput_STDERR: + if data := output.GetData(); len(data) > 0 { + if writeErr := conn.WriteMessage(websocket.BinaryMessage, data); writeErr != nil { + return + } + } + case pb.ExecOutput_EXIT: + return + } + } + }() + + // WebSocket -> gRPC stdin/resize + go func() { + for { + msgType, data, readErr := conn.ReadMessage() + if readErr != nil { + _ = execStream.Close() + return + } + switch msgType { + case websocket.BinaryMessage: + if len(data) > 0 { + if sendErr := execStream.SendStdin(data); sendErr != nil { + return + } + } + case websocket.TextMessage: + var ctrl terminalControlMessage + if json.Unmarshal(data, &ctrl) == nil && ctrl.Type == "resize" && ctrl.Cols > 0 && ctrl.Rows > 0 { + if resizeErr := execStream.Resize(ctrl.Cols, ctrl.Rows); resizeErr != nil { + h.logger.Warn("terminal resize failed", + slog.String("bot_id", botID), slog.Any("error", resizeErr)) + } + } + } + } + }() + + <-done + return nil +} + +func parseUint32Query(c echo.Context, name string, fallback uint32) uint32 { + raw := c.QueryParam(name) + if raw == "" { + return fallback + } + v, err := strconv.ParseUint(raw, 10, 32) + if err != nil || v == 0 { + return fallback + } + return uint32(v) //nolint:gosec // G115 +} diff --git a/internal/mcp/mcpclient/client.go b/internal/mcp/mcpclient/client.go index 24c0aceb..9cb27ab9 100644 --- a/internal/mcp/mcpclient/client.go +++ b/internal/mcp/mcpclient/client.go @@ -204,11 +204,39 @@ func (s *ExecStream) Recv() (*pb.ExecOutput, error) { return s.stream.Recv() } +// Resize sends a terminal resize event to the running process. +func (s *ExecStream) Resize(cols, rows uint32) error { + return s.stream.Send(&pb.ExecInput{ + Resize: &pb.TerminalResize{Cols: cols, Rows: rows}, + }) +} + // Close closes the stream. func (s *ExecStream) Close() error { return s.stream.CloseSend() } +// ExecStreamPTY opens a bidirectional PTY exec stream. +// The command runs inside a pseudo-terminal with the given initial size. +func (c *Client) ExecStreamPTY(ctx context.Context, command, workDir string, cols, rows uint32) (*ExecStream, error) { + stream, err := c.svc.Exec(ctx) + if err != nil { + return nil, mapError(err) + } + + err = stream.Send(&pb.ExecInput{ + Command: command, + WorkDir: workDir, + Pty: true, + Resize: &pb.TerminalResize{Cols: cols, Rows: rows}, + }) + if err != nil { + return nil, err + } + + return &ExecStream{stream: stream}, nil +} + // ReadRaw streams raw file bytes. Caller must consume the returned reader. func (c *Client) ReadRaw(ctx context.Context, path string) (io.ReadCloser, error) { stream, err := c.svc.ReadRaw(ctx, &pb.ReadRawRequest{Path: path}) diff --git a/internal/mcp/mcpcontainer/mcpcontainer.pb.go b/internal/mcp/mcpcontainer/mcpcontainer.pb.go index f1808123..b84b4b67 100644 --- a/internal/mcp/mcpcontainer/mcpcontainer.pb.go +++ b/internal/mcp/mcpcontainer/mcpcontainer.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 -// protoc v4.25.3 +// protoc-gen-go v1.36.11 +// protoc v7.34.0 // source: internal/mcp/mcpcontainer/mcpcontainer.proto package mcpcontainer @@ -67,7 +67,7 @@ func (x ExecOutput_Stream) Number() protoreflect.EnumNumber { // Deprecated: Use ExecOutput_Stream.Descriptor instead. func (ExecOutput_Stream) EnumDescriptor() ([]byte, []int) { - return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{8, 0} + return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{9, 0} } type ReadFileRequest struct { @@ -457,6 +457,8 @@ type ExecInput struct { Env []string `protobuf:"bytes,3,rep,name=env,proto3" json:"env,omitempty"` TimeoutSeconds int32 `protobuf:"varint,4,opt,name=timeout_seconds,json=timeoutSeconds,proto3" json:"timeout_seconds,omitempty"` StdinData []byte `protobuf:"bytes,5,opt,name=stdin_data,json=stdinData,proto3" json:"stdin_data,omitempty"` + Pty bool `protobuf:"varint,6,opt,name=pty,proto3" json:"pty,omitempty"` + Resize *TerminalResize `protobuf:"bytes,7,opt,name=resize,proto3" json:"resize,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -526,6 +528,72 @@ func (x *ExecInput) GetStdinData() []byte { return nil } +func (x *ExecInput) GetPty() bool { + if x != nil { + return x.Pty + } + return false +} + +func (x *ExecInput) GetResize() *TerminalResize { + if x != nil { + return x.Resize + } + return nil +} + +type TerminalResize struct { + state protoimpl.MessageState `protogen:"open.v1"` + Cols uint32 `protobuf:"varint,1,opt,name=cols,proto3" json:"cols,omitempty"` + Rows uint32 `protobuf:"varint,2,opt,name=rows,proto3" json:"rows,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TerminalResize) Reset() { + *x = TerminalResize{} + mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TerminalResize) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TerminalResize) ProtoMessage() {} + +func (x *TerminalResize) ProtoReflect() protoreflect.Message { + mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TerminalResize.ProtoReflect.Descriptor instead. +func (*TerminalResize) Descriptor() ([]byte, []int) { + return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{8} +} + +func (x *TerminalResize) GetCols() uint32 { + if x != nil { + return x.Cols + } + return 0 +} + +func (x *TerminalResize) GetRows() uint32 { + if x != nil { + return x.Rows + } + return 0 +} + type ExecOutput struct { state protoimpl.MessageState `protogen:"open.v1"` Stream ExecOutput_Stream `protobuf:"varint,1,opt,name=stream,proto3,enum=mcpcontainer.ExecOutput_Stream" json:"stream,omitempty"` @@ -537,7 +605,7 @@ type ExecOutput struct { func (x *ExecOutput) Reset() { *x = ExecOutput{} - mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[8] + mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -549,7 +617,7 @@ func (x *ExecOutput) String() string { func (*ExecOutput) ProtoMessage() {} func (x *ExecOutput) ProtoReflect() protoreflect.Message { - mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[8] + mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -562,7 +630,7 @@ func (x *ExecOutput) ProtoReflect() protoreflect.Message { // Deprecated: Use ExecOutput.ProtoReflect.Descriptor instead. func (*ExecOutput) Descriptor() ([]byte, []int) { - return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{8} + return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{9} } func (x *ExecOutput) GetStream() ExecOutput_Stream { @@ -595,7 +663,7 @@ type ReadRawRequest struct { func (x *ReadRawRequest) Reset() { *x = ReadRawRequest{} - mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[9] + mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -607,7 +675,7 @@ func (x *ReadRawRequest) String() string { func (*ReadRawRequest) ProtoMessage() {} func (x *ReadRawRequest) ProtoReflect() protoreflect.Message { - mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[9] + mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -620,7 +688,7 @@ func (x *ReadRawRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ReadRawRequest.ProtoReflect.Descriptor instead. func (*ReadRawRequest) Descriptor() ([]byte, []int) { - return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{9} + return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{10} } func (x *ReadRawRequest) GetPath() string { @@ -639,7 +707,7 @@ type DataChunk struct { func (x *DataChunk) Reset() { *x = DataChunk{} - mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[10] + mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -651,7 +719,7 @@ func (x *DataChunk) String() string { func (*DataChunk) ProtoMessage() {} func (x *DataChunk) ProtoReflect() protoreflect.Message { - mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[10] + mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -664,7 +732,7 @@ func (x *DataChunk) ProtoReflect() protoreflect.Message { // Deprecated: Use DataChunk.ProtoReflect.Descriptor instead. func (*DataChunk) Descriptor() ([]byte, []int) { - return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{10} + return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{11} } func (x *DataChunk) GetData() []byte { @@ -684,7 +752,7 @@ type WriteRawChunk struct { func (x *WriteRawChunk) Reset() { *x = WriteRawChunk{} - mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[11] + mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -696,7 +764,7 @@ func (x *WriteRawChunk) String() string { func (*WriteRawChunk) ProtoMessage() {} func (x *WriteRawChunk) ProtoReflect() protoreflect.Message { - mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[11] + mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -709,7 +777,7 @@ func (x *WriteRawChunk) ProtoReflect() protoreflect.Message { // Deprecated: Use WriteRawChunk.ProtoReflect.Descriptor instead. func (*WriteRawChunk) Descriptor() ([]byte, []int) { - return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{11} + return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{12} } func (x *WriteRawChunk) GetPath() string { @@ -735,7 +803,7 @@ type WriteRawResponse struct { func (x *WriteRawResponse) Reset() { *x = WriteRawResponse{} - mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[12] + mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -747,7 +815,7 @@ func (x *WriteRawResponse) String() string { func (*WriteRawResponse) ProtoMessage() {} func (x *WriteRawResponse) ProtoReflect() protoreflect.Message { - mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[12] + mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -760,7 +828,7 @@ func (x *WriteRawResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use WriteRawResponse.ProtoReflect.Descriptor instead. func (*WriteRawResponse) Descriptor() ([]byte, []int) { - return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{12} + return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{13} } func (x *WriteRawResponse) GetBytesWritten() int64 { @@ -780,7 +848,7 @@ type DeleteFileRequest struct { func (x *DeleteFileRequest) Reset() { *x = DeleteFileRequest{} - mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[13] + mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -792,7 +860,7 @@ func (x *DeleteFileRequest) String() string { func (*DeleteFileRequest) ProtoMessage() {} func (x *DeleteFileRequest) ProtoReflect() protoreflect.Message { - mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[13] + mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -805,7 +873,7 @@ func (x *DeleteFileRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteFileRequest.ProtoReflect.Descriptor instead. func (*DeleteFileRequest) Descriptor() ([]byte, []int) { - return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{13} + return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{14} } func (x *DeleteFileRequest) GetPath() string { @@ -830,7 +898,7 @@ type DeleteFileResponse struct { func (x *DeleteFileResponse) Reset() { *x = DeleteFileResponse{} - mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[14] + mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -842,7 +910,7 @@ func (x *DeleteFileResponse) String() string { func (*DeleteFileResponse) ProtoMessage() {} func (x *DeleteFileResponse) ProtoReflect() protoreflect.Message { - mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[14] + mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -855,7 +923,7 @@ func (x *DeleteFileResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteFileResponse.ProtoReflect.Descriptor instead. func (*DeleteFileResponse) Descriptor() ([]byte, []int) { - return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{14} + return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{15} } type StatRequest struct { @@ -867,7 +935,7 @@ type StatRequest struct { func (x *StatRequest) Reset() { *x = StatRequest{} - mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[15] + mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -879,7 +947,7 @@ func (x *StatRequest) String() string { func (*StatRequest) ProtoMessage() {} func (x *StatRequest) ProtoReflect() protoreflect.Message { - mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[15] + mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -892,7 +960,7 @@ func (x *StatRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StatRequest.ProtoReflect.Descriptor instead. func (*StatRequest) Descriptor() ([]byte, []int) { - return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{15} + return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{16} } func (x *StatRequest) GetPath() string { @@ -911,7 +979,7 @@ type StatResponse struct { func (x *StatResponse) Reset() { *x = StatResponse{} - mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[16] + mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -923,7 +991,7 @@ func (x *StatResponse) String() string { func (*StatResponse) ProtoMessage() {} func (x *StatResponse) ProtoReflect() protoreflect.Message { - mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[16] + mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -936,7 +1004,7 @@ func (x *StatResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use StatResponse.ProtoReflect.Descriptor instead. func (*StatResponse) Descriptor() ([]byte, []int) { - return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{16} + return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{17} } func (x *StatResponse) GetEntry() *FileEntry { @@ -955,7 +1023,7 @@ type MkdirRequest struct { func (x *MkdirRequest) Reset() { *x = MkdirRequest{} - mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[17] + mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -967,7 +1035,7 @@ func (x *MkdirRequest) String() string { func (*MkdirRequest) ProtoMessage() {} func (x *MkdirRequest) ProtoReflect() protoreflect.Message { - mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[17] + mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -980,7 +1048,7 @@ func (x *MkdirRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use MkdirRequest.ProtoReflect.Descriptor instead. func (*MkdirRequest) Descriptor() ([]byte, []int) { - return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{17} + return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{18} } func (x *MkdirRequest) GetPath() string { @@ -998,7 +1066,7 @@ type MkdirResponse struct { func (x *MkdirResponse) Reset() { *x = MkdirResponse{} - mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[18] + mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1010,7 +1078,7 @@ func (x *MkdirResponse) String() string { func (*MkdirResponse) ProtoMessage() {} func (x *MkdirResponse) ProtoReflect() protoreflect.Message { - mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[18] + mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1023,7 +1091,7 @@ func (x *MkdirResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use MkdirResponse.ProtoReflect.Descriptor instead. func (*MkdirResponse) Descriptor() ([]byte, []int) { - return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{18} + return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{19} } type RenameRequest struct { @@ -1036,7 +1104,7 @@ type RenameRequest struct { func (x *RenameRequest) Reset() { *x = RenameRequest{} - mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[19] + mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1048,7 +1116,7 @@ func (x *RenameRequest) String() string { func (*RenameRequest) ProtoMessage() {} func (x *RenameRequest) ProtoReflect() protoreflect.Message { - mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[19] + mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1061,7 +1129,7 @@ func (x *RenameRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RenameRequest.ProtoReflect.Descriptor instead. func (*RenameRequest) Descriptor() ([]byte, []int) { - return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{19} + return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{20} } func (x *RenameRequest) GetOldPath() string { @@ -1086,7 +1154,7 @@ type RenameResponse struct { func (x *RenameResponse) Reset() { *x = RenameResponse{} - mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[20] + mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1098,7 +1166,7 @@ func (x *RenameResponse) String() string { func (*RenameResponse) ProtoMessage() {} func (x *RenameResponse) ProtoReflect() protoreflect.Message { - mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[20] + mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1111,7 +1179,7 @@ func (x *RenameResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RenameResponse.ProtoReflect.Descriptor instead. func (*RenameResponse) Descriptor() ([]byte, []int) { - return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{20} + return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{21} } var File_internal_mcp_mcpcontainer_mcpcontainer_proto protoreflect.FileDescriptor @@ -1143,14 +1211,19 @@ const file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDesc = "" + "\x04mode\x18\x04 \x01(\tR\x04mode\x12\x19\n" + "\bmod_time\x18\x05 \x01(\tR\amodTime\"D\n" + "\x0fListDirResponse\x121\n" + - "\aentries\x18\x01 \x03(\v2\x17.mcpcontainer.FileEntryR\aentries\"\x9a\x01\n" + + "\aentries\x18\x01 \x03(\v2\x17.mcpcontainer.FileEntryR\aentries\"\xe2\x01\n" + "\tExecInput\x12\x18\n" + "\acommand\x18\x01 \x01(\tR\acommand\x12\x19\n" + "\bwork_dir\x18\x02 \x01(\tR\aworkDir\x12\x10\n" + "\x03env\x18\x03 \x03(\tR\x03env\x12'\n" + "\x0ftimeout_seconds\x18\x04 \x01(\x05R\x0etimeoutSeconds\x12\x1d\n" + "\n" + - "stdin_data\x18\x05 \x01(\fR\tstdinData\"\xa2\x01\n" + + "stdin_data\x18\x05 \x01(\fR\tstdinData\x12\x10\n" + + "\x03pty\x18\x06 \x01(\bR\x03pty\x124\n" + + "\x06resize\x18\a \x01(\v2\x1c.mcpcontainer.TerminalResizeR\x06resize\"8\n" + + "\x0eTerminalResize\x12\x12\n" + + "\x04cols\x18\x01 \x01(\rR\x04cols\x12\x12\n" + + "\x04rows\x18\x02 \x01(\rR\x04rows\"\xa2\x01\n" + "\n" + "ExecOutput\x127\n" + "\x06stream\x18\x01 \x01(\x0e2\x1f.mcpcontainer.ExecOutput.StreamR\x06stream\x12\x12\n" + @@ -1212,7 +1285,7 @@ func file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP() []byte { } var file_internal_mcp_mcpcontainer_mcpcontainer_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes = make([]protoimpl.MessageInfo, 21) +var file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes = make([]protoimpl.MessageInfo, 22) var file_internal_mcp_mcpcontainer_mcpcontainer_proto_goTypes = []any{ (ExecOutput_Stream)(0), // 0: mcpcontainer.ExecOutput.Stream (*ReadFileRequest)(nil), // 1: mcpcontainer.ReadFileRequest @@ -1223,49 +1296,51 @@ var file_internal_mcp_mcpcontainer_mcpcontainer_proto_goTypes = []any{ (*FileEntry)(nil), // 6: mcpcontainer.FileEntry (*ListDirResponse)(nil), // 7: mcpcontainer.ListDirResponse (*ExecInput)(nil), // 8: mcpcontainer.ExecInput - (*ExecOutput)(nil), // 9: mcpcontainer.ExecOutput - (*ReadRawRequest)(nil), // 10: mcpcontainer.ReadRawRequest - (*DataChunk)(nil), // 11: mcpcontainer.DataChunk - (*WriteRawChunk)(nil), // 12: mcpcontainer.WriteRawChunk - (*WriteRawResponse)(nil), // 13: mcpcontainer.WriteRawResponse - (*DeleteFileRequest)(nil), // 14: mcpcontainer.DeleteFileRequest - (*DeleteFileResponse)(nil), // 15: mcpcontainer.DeleteFileResponse - (*StatRequest)(nil), // 16: mcpcontainer.StatRequest - (*StatResponse)(nil), // 17: mcpcontainer.StatResponse - (*MkdirRequest)(nil), // 18: mcpcontainer.MkdirRequest - (*MkdirResponse)(nil), // 19: mcpcontainer.MkdirResponse - (*RenameRequest)(nil), // 20: mcpcontainer.RenameRequest - (*RenameResponse)(nil), // 21: mcpcontainer.RenameResponse + (*TerminalResize)(nil), // 9: mcpcontainer.TerminalResize + (*ExecOutput)(nil), // 10: mcpcontainer.ExecOutput + (*ReadRawRequest)(nil), // 11: mcpcontainer.ReadRawRequest + (*DataChunk)(nil), // 12: mcpcontainer.DataChunk + (*WriteRawChunk)(nil), // 13: mcpcontainer.WriteRawChunk + (*WriteRawResponse)(nil), // 14: mcpcontainer.WriteRawResponse + (*DeleteFileRequest)(nil), // 15: mcpcontainer.DeleteFileRequest + (*DeleteFileResponse)(nil), // 16: mcpcontainer.DeleteFileResponse + (*StatRequest)(nil), // 17: mcpcontainer.StatRequest + (*StatResponse)(nil), // 18: mcpcontainer.StatResponse + (*MkdirRequest)(nil), // 19: mcpcontainer.MkdirRequest + (*MkdirResponse)(nil), // 20: mcpcontainer.MkdirResponse + (*RenameRequest)(nil), // 21: mcpcontainer.RenameRequest + (*RenameResponse)(nil), // 22: mcpcontainer.RenameResponse } var file_internal_mcp_mcpcontainer_mcpcontainer_proto_depIdxs = []int32{ 6, // 0: mcpcontainer.ListDirResponse.entries:type_name -> mcpcontainer.FileEntry - 0, // 1: mcpcontainer.ExecOutput.stream:type_name -> mcpcontainer.ExecOutput.Stream - 6, // 2: mcpcontainer.StatResponse.entry:type_name -> mcpcontainer.FileEntry - 1, // 3: mcpcontainer.ContainerService.ReadFile:input_type -> mcpcontainer.ReadFileRequest - 3, // 4: mcpcontainer.ContainerService.WriteFile:input_type -> mcpcontainer.WriteFileRequest - 5, // 5: mcpcontainer.ContainerService.ListDir:input_type -> mcpcontainer.ListDirRequest - 16, // 6: mcpcontainer.ContainerService.Stat:input_type -> mcpcontainer.StatRequest - 18, // 7: mcpcontainer.ContainerService.Mkdir:input_type -> mcpcontainer.MkdirRequest - 20, // 8: mcpcontainer.ContainerService.Rename:input_type -> mcpcontainer.RenameRequest - 8, // 9: mcpcontainer.ContainerService.Exec:input_type -> mcpcontainer.ExecInput - 10, // 10: mcpcontainer.ContainerService.ReadRaw:input_type -> mcpcontainer.ReadRawRequest - 12, // 11: mcpcontainer.ContainerService.WriteRaw:input_type -> mcpcontainer.WriteRawChunk - 14, // 12: mcpcontainer.ContainerService.DeleteFile:input_type -> mcpcontainer.DeleteFileRequest - 2, // 13: mcpcontainer.ContainerService.ReadFile:output_type -> mcpcontainer.ReadFileResponse - 4, // 14: mcpcontainer.ContainerService.WriteFile:output_type -> mcpcontainer.WriteFileResponse - 7, // 15: mcpcontainer.ContainerService.ListDir:output_type -> mcpcontainer.ListDirResponse - 17, // 16: mcpcontainer.ContainerService.Stat:output_type -> mcpcontainer.StatResponse - 19, // 17: mcpcontainer.ContainerService.Mkdir:output_type -> mcpcontainer.MkdirResponse - 21, // 18: mcpcontainer.ContainerService.Rename:output_type -> mcpcontainer.RenameResponse - 9, // 19: mcpcontainer.ContainerService.Exec:output_type -> mcpcontainer.ExecOutput - 11, // 20: mcpcontainer.ContainerService.ReadRaw:output_type -> mcpcontainer.DataChunk - 13, // 21: mcpcontainer.ContainerService.WriteRaw:output_type -> mcpcontainer.WriteRawResponse - 15, // 22: mcpcontainer.ContainerService.DeleteFile:output_type -> mcpcontainer.DeleteFileResponse - 13, // [13:23] is the sub-list for method output_type - 3, // [3:13] is the sub-list for method input_type - 3, // [3:3] is the sub-list for extension type_name - 3, // [3:3] is the sub-list for extension extendee - 0, // [0:3] is the sub-list for field type_name + 9, // 1: mcpcontainer.ExecInput.resize:type_name -> mcpcontainer.TerminalResize + 0, // 2: mcpcontainer.ExecOutput.stream:type_name -> mcpcontainer.ExecOutput.Stream + 6, // 3: mcpcontainer.StatResponse.entry:type_name -> mcpcontainer.FileEntry + 1, // 4: mcpcontainer.ContainerService.ReadFile:input_type -> mcpcontainer.ReadFileRequest + 3, // 5: mcpcontainer.ContainerService.WriteFile:input_type -> mcpcontainer.WriteFileRequest + 5, // 6: mcpcontainer.ContainerService.ListDir:input_type -> mcpcontainer.ListDirRequest + 17, // 7: mcpcontainer.ContainerService.Stat:input_type -> mcpcontainer.StatRequest + 19, // 8: mcpcontainer.ContainerService.Mkdir:input_type -> mcpcontainer.MkdirRequest + 21, // 9: mcpcontainer.ContainerService.Rename:input_type -> mcpcontainer.RenameRequest + 8, // 10: mcpcontainer.ContainerService.Exec:input_type -> mcpcontainer.ExecInput + 11, // 11: mcpcontainer.ContainerService.ReadRaw:input_type -> mcpcontainer.ReadRawRequest + 13, // 12: mcpcontainer.ContainerService.WriteRaw:input_type -> mcpcontainer.WriteRawChunk + 15, // 13: mcpcontainer.ContainerService.DeleteFile:input_type -> mcpcontainer.DeleteFileRequest + 2, // 14: mcpcontainer.ContainerService.ReadFile:output_type -> mcpcontainer.ReadFileResponse + 4, // 15: mcpcontainer.ContainerService.WriteFile:output_type -> mcpcontainer.WriteFileResponse + 7, // 16: mcpcontainer.ContainerService.ListDir:output_type -> mcpcontainer.ListDirResponse + 18, // 17: mcpcontainer.ContainerService.Stat:output_type -> mcpcontainer.StatResponse + 20, // 18: mcpcontainer.ContainerService.Mkdir:output_type -> mcpcontainer.MkdirResponse + 22, // 19: mcpcontainer.ContainerService.Rename:output_type -> mcpcontainer.RenameResponse + 10, // 20: mcpcontainer.ContainerService.Exec:output_type -> mcpcontainer.ExecOutput + 12, // 21: mcpcontainer.ContainerService.ReadRaw:output_type -> mcpcontainer.DataChunk + 14, // 22: mcpcontainer.ContainerService.WriteRaw:output_type -> mcpcontainer.WriteRawResponse + 16, // 23: mcpcontainer.ContainerService.DeleteFile:output_type -> mcpcontainer.DeleteFileResponse + 14, // [14:24] is the sub-list for method output_type + 4, // [4:14] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name } func init() { file_internal_mcp_mcpcontainer_mcpcontainer_proto_init() } @@ -1279,7 +1354,7 @@ func file_internal_mcp_mcpcontainer_mcpcontainer_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDesc), len(file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDesc)), NumEnums: 1, - NumMessages: 21, + NumMessages: 22, NumExtensions: 0, NumServices: 1, }, diff --git a/internal/mcp/mcpcontainer/mcpcontainer.proto b/internal/mcp/mcpcontainer/mcpcontainer.proto index 9e81a7a6..0b7be27f 100644 --- a/internal/mcp/mcpcontainer/mcpcontainer.proto +++ b/internal/mcp/mcpcontainer/mcpcontainer.proto @@ -59,6 +59,13 @@ message ExecInput { repeated string env = 3; int32 timeout_seconds = 4; bytes stdin_data = 5; + bool pty = 6; + TerminalResize resize = 7; +} + +message TerminalResize { + uint32 cols = 1; + uint32 rows = 2; } message ExecOutput { diff --git a/internal/mcp/mcpcontainer/mcpcontainer_grpc.pb.go b/internal/mcp/mcpcontainer/mcpcontainer_grpc.pb.go index 39d23f7e..142bd554 100644 --- a/internal/mcp/mcpcontainer/mcpcontainer_grpc.pb.go +++ b/internal/mcp/mcpcontainer/mcpcontainer_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.5.1 -// - protoc v4.25.3 +// - protoc-gen-go-grpc v1.6.1 +// - protoc v7.34.0 // source: internal/mcp/mcpcontainer/mcpcontainer.proto package mcpcontainer @@ -195,34 +195,34 @@ type ContainerServiceServer interface { type UnimplementedContainerServiceServer struct{} func (UnimplementedContainerServiceServer) ReadFile(context.Context, *ReadFileRequest) (*ReadFileResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method ReadFile not implemented") + return nil, status.Error(codes.Unimplemented, "method ReadFile not implemented") } func (UnimplementedContainerServiceServer) WriteFile(context.Context, *WriteFileRequest) (*WriteFileResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method WriteFile not implemented") + return nil, status.Error(codes.Unimplemented, "method WriteFile not implemented") } func (UnimplementedContainerServiceServer) ListDir(context.Context, *ListDirRequest) (*ListDirResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method ListDir not implemented") + return nil, status.Error(codes.Unimplemented, "method ListDir not implemented") } func (UnimplementedContainerServiceServer) Stat(context.Context, *StatRequest) (*StatResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method Stat not implemented") + return nil, status.Error(codes.Unimplemented, "method Stat not implemented") } func (UnimplementedContainerServiceServer) Mkdir(context.Context, *MkdirRequest) (*MkdirResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method Mkdir not implemented") + return nil, status.Error(codes.Unimplemented, "method Mkdir not implemented") } func (UnimplementedContainerServiceServer) Rename(context.Context, *RenameRequest) (*RenameResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method Rename not implemented") + return nil, status.Error(codes.Unimplemented, "method Rename not implemented") } func (UnimplementedContainerServiceServer) Exec(grpc.BidiStreamingServer[ExecInput, ExecOutput]) error { - return status.Errorf(codes.Unimplemented, "method Exec not implemented") + return status.Error(codes.Unimplemented, "method Exec not implemented") } func (UnimplementedContainerServiceServer) ReadRaw(*ReadRawRequest, grpc.ServerStreamingServer[DataChunk]) error { - return status.Errorf(codes.Unimplemented, "method ReadRaw not implemented") + return status.Error(codes.Unimplemented, "method ReadRaw not implemented") } func (UnimplementedContainerServiceServer) WriteRaw(grpc.ClientStreamingServer[WriteRawChunk, WriteRawResponse]) error { - return status.Errorf(codes.Unimplemented, "method WriteRaw not implemented") + return status.Error(codes.Unimplemented, "method WriteRaw not implemented") } func (UnimplementedContainerServiceServer) DeleteFile(context.Context, *DeleteFileRequest) (*DeleteFileResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method DeleteFile not implemented") + return nil, status.Error(codes.Unimplemented, "method DeleteFile not implemented") } func (UnimplementedContainerServiceServer) mustEmbedUnimplementedContainerServiceServer() {} func (UnimplementedContainerServiceServer) testEmbeddedByValue() {} @@ -235,7 +235,7 @@ type UnsafeContainerServiceServer interface { } func RegisterContainerServiceServer(s grpc.ServiceRegistrar, srv ContainerServiceServer) { - // If the following call pancis, it indicates UnimplementedContainerServiceServer was + // If the following call panics, it indicates UnimplementedContainerServiceServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 341d33d6..cb6d795a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -158,6 +158,15 @@ importers: '@vueuse/core': specifier: ^14.1.0 version: 14.1.0(vue@3.5.26(typescript@5.9.3)) + '@xterm/addon-fit': + specifier: ^0.11.0 + version: 0.11.0 + '@xterm/addon-serialize': + specifier: ^0.14.0 + version: 0.14.0 + '@xterm/xterm': + specifier: ^6.0.0 + version: 6.0.0 dotenv: specifier: ^17.2.3 version: 17.2.3 @@ -2562,6 +2571,15 @@ packages: peerDependencies: vue: ^3.5.0 + '@xterm/addon-fit@0.11.0': + resolution: {integrity: sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==} + + '@xterm/addon-serialize@0.14.0': + resolution: {integrity: sha512-uteyTU1EkrQa2Ux6P/uFl2fzmXI46jy5uoQMKEOM0fKTyiW7cSn0WrFenHm5vO5uEXX/GpwW/FgILvv3r0WbkA==} + + '@xterm/xterm@6.0.0': + resolution: {integrity: sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==} + accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} @@ -7426,6 +7444,12 @@ snapshots: dependencies: vue: 3.5.26(typescript@5.9.3) + '@xterm/addon-fit@0.11.0': {} + + '@xterm/addon-serialize@0.14.0': {} + + '@xterm/xterm@6.0.0': {} + accepts@2.0.0: dependencies: mime-types: 3.0.2