From b4dd1043f87c4437609976e10be06a52c40fdbc2 Mon Sep 17 00:00:00 2001
From: su-fen <715041@qq.com>
Date: Mon, 8 Jun 2026 18:47:32 +0800
Subject: [PATCH 1/9] feat(tunnel): add local project tunnel support
---
Cargo.lock | 2 +
.../internal/proto/v1/gateway.pb.go | 1726 ++++++++++++-----
crates/agent-gateway/internal/server/grpc.go | 22 +-
crates/agent-gateway/internal/server/http.go | 1 +
.../internal/server/http_test.go | 8 +-
.../agent-gateway/internal/server/tunnel.go | 722 +++++++
.../internal/server/tunnel_rewrite.go | 229 +++
.../internal/server/tunnel_rewrite_test.go | 269 +++
.../internal/server/websocket.go | 19 +
.../server/websocket_chat_handlers.go | 4 +-
.../internal/server/websocket_roundtrip.go | 2 +-
.../internal/server/websocket_routes.go | 4 +
.../internal/server/websocket_routes_test.go | 56 +-
.../server/websocket_tunnel_handlers.go | 322 +++
.../internal/session/agent_session.go | 67 +-
.../agent-gateway/internal/session/manager.go | 8 +-
.../internal/session/manager_chat_runs.go | 10 +
.../internal/session/manager_registry.go | 12 +
.../internal/session/manager_tunnel.go | 843 ++++++++
.../internal/session/manager_tunnel_test.go | 265 +++
crates/agent-gateway/proto/v1/gateway.proto | 75 +
.../test/session/manager_test.go | 23 +
.../test/upload/import_readable_files_test.go | 3 +-
.../test/websocket/chat_bridge_test.go | 47 +-
.../test/webui/gateway-socket-client.test.mjs | 202 ++
.../test/webui/web-settings.test.mjs | 68 +-
crates/agent-gateway/web/src/App.tsx | 129 +-
.../project-tools/LocalTunnelPanel.tsx | 783 ++++++++
.../project-tools/ProjectToolsPanel.tsx | 182 +-
crates/agent-gateway/web/src/i18n/config.ts | 107 +
.../web/src/lib/gatewaySocket.ts | 176 ++
.../web/src/lib/gatewaySocket.worker.ts | 65 +
.../web/src/lib/settings/index.ts | 103 +-
.../web/src/lib/settings/storage.ts | 6 +-
.../web/src/lib/settings/sync.ts | 99 +-
.../agent-gateway/web/src/lib/webSettings.ts | 2 +
.../web/src/pages/chat/ChatComposerBar.tsx | 48 +-
.../web/src/pages/settings/RemoteSection.tsx | 20 +
crates/agent-gui/src-tauri/Cargo.toml | 2 +
.../src-tauri/src/commands/gateway.rs | 49 +-
.../src-tauri/src/commands/settings.rs | 12 +
crates/agent-gui/src-tauri/src/lib.rs | 6 +
.../src-tauri/src/services/gateway.rs | 1411 +++++++++++++-
.../project-tools/LocalTunnelPanel.tsx | 804 ++++++++
.../project-tools/ProjectToolsPanel.tsx | 177 +-
crates/agent-gui/src/i18n/config.ts | 108 ++
crates/agent-gui/src/lib/settings/index.ts | 110 +-
crates/agent-gui/src/lib/settings/storage.ts | 6 +-
crates/agent-gui/src/lib/settings/sync.ts | 98 +-
.../src/lib/tools/builtinRegistry.ts | 27 +
.../src/lib/tools/tunnelManagerTools.ts | 322 +++
crates/agent-gui/src/pages/ChatPage.tsx | 101 +-
.../src/pages/chat/ChatComposerBar.tsx | 37 +
.../pages/chat/runAgentConversationTurn.ts | 26 +-
.../pages/chat/useGatewayBridgeListeners.ts | 201 +-
.../src/pages/settings/RemoteSection.tsx | 18 +
.../test/settings/normalization.test.mjs | 159 +-
.../test/tools/tunnel-manager-tools.test.mjs | 209 ++
58 files changed, 9899 insertions(+), 713 deletions(-)
create mode 100644 crates/agent-gateway/internal/server/tunnel.go
create mode 100644 crates/agent-gateway/internal/server/tunnel_rewrite.go
create mode 100644 crates/agent-gateway/internal/server/tunnel_rewrite_test.go
create mode 100644 crates/agent-gateway/internal/server/websocket_tunnel_handlers.go
create mode 100644 crates/agent-gateway/internal/session/manager_tunnel.go
create mode 100644 crates/agent-gateway/internal/session/manager_tunnel_test.go
create mode 100644 crates/agent-gateway/web/src/components/project-tools/LocalTunnelPanel.tsx
create mode 100644 crates/agent-gui/src/components/project-tools/LocalTunnelPanel.tsx
create mode 100644 crates/agent-gui/src/lib/tools/tunnelManagerTools.ts
create mode 100644 crates/agent-gui/test/tools/tunnel-manager-tools.test.mjs
diff --git a/Cargo.lock b/Cargo.lock
index c936b824e..5361af655 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2627,6 +2627,7 @@ dependencies = [
"base64 0.22.1",
"chrono",
"dirs",
+ "futures-util",
"globset",
"ignore",
"lopdf",
@@ -2652,6 +2653,7 @@ dependencies = [
"tokio",
"tokio-cron-scheduler",
"tokio-stream",
+ "tokio-tungstenite",
"tonic",
"tonic-build",
"uuid",
diff --git a/crates/agent-gateway/internal/proto/v1/gateway.pb.go b/crates/agent-gateway/internal/proto/v1/gateway.pb.go
index 0d65fb222..019569c1e 100644
--- a/crates/agent-gateway/internal/proto/v1/gateway.pb.go
+++ b/crates/agent-gateway/internal/proto/v1/gateway.pb.go
@@ -21,6 +21,82 @@ const (
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
+type TunnelFrameKind int32
+
+const (
+ TunnelFrameKind_TUNNEL_FRAME_KIND_UNSPECIFIED TunnelFrameKind = 0
+ TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_REQUEST_START TunnelFrameKind = 1
+ TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_REQUEST_BODY TunnelFrameKind = 2
+ TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_REQUEST_END TunnelFrameKind = 3
+ TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_RESPONSE_START TunnelFrameKind = 4
+ TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_RESPONSE_BODY TunnelFrameKind = 5
+ TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_RESPONSE_END TunnelFrameKind = 6
+ TunnelFrameKind_TUNNEL_FRAME_KIND_WS_OPEN TunnelFrameKind = 7
+ TunnelFrameKind_TUNNEL_FRAME_KIND_WS_FRAME TunnelFrameKind = 8
+ TunnelFrameKind_TUNNEL_FRAME_KIND_WS_CLOSE TunnelFrameKind = 9
+ TunnelFrameKind_TUNNEL_FRAME_KIND_ERROR TunnelFrameKind = 10
+ TunnelFrameKind_TUNNEL_FRAME_KIND_CANCEL TunnelFrameKind = 11
+)
+
+// Enum value maps for TunnelFrameKind.
+var (
+ TunnelFrameKind_name = map[int32]string{
+ 0: "TUNNEL_FRAME_KIND_UNSPECIFIED",
+ 1: "TUNNEL_FRAME_KIND_HTTP_REQUEST_START",
+ 2: "TUNNEL_FRAME_KIND_HTTP_REQUEST_BODY",
+ 3: "TUNNEL_FRAME_KIND_HTTP_REQUEST_END",
+ 4: "TUNNEL_FRAME_KIND_HTTP_RESPONSE_START",
+ 5: "TUNNEL_FRAME_KIND_HTTP_RESPONSE_BODY",
+ 6: "TUNNEL_FRAME_KIND_HTTP_RESPONSE_END",
+ 7: "TUNNEL_FRAME_KIND_WS_OPEN",
+ 8: "TUNNEL_FRAME_KIND_WS_FRAME",
+ 9: "TUNNEL_FRAME_KIND_WS_CLOSE",
+ 10: "TUNNEL_FRAME_KIND_ERROR",
+ 11: "TUNNEL_FRAME_KIND_CANCEL",
+ }
+ TunnelFrameKind_value = map[string]int32{
+ "TUNNEL_FRAME_KIND_UNSPECIFIED": 0,
+ "TUNNEL_FRAME_KIND_HTTP_REQUEST_START": 1,
+ "TUNNEL_FRAME_KIND_HTTP_REQUEST_BODY": 2,
+ "TUNNEL_FRAME_KIND_HTTP_REQUEST_END": 3,
+ "TUNNEL_FRAME_KIND_HTTP_RESPONSE_START": 4,
+ "TUNNEL_FRAME_KIND_HTTP_RESPONSE_BODY": 5,
+ "TUNNEL_FRAME_KIND_HTTP_RESPONSE_END": 6,
+ "TUNNEL_FRAME_KIND_WS_OPEN": 7,
+ "TUNNEL_FRAME_KIND_WS_FRAME": 8,
+ "TUNNEL_FRAME_KIND_WS_CLOSE": 9,
+ "TUNNEL_FRAME_KIND_ERROR": 10,
+ "TUNNEL_FRAME_KIND_CANCEL": 11,
+ }
+)
+
+func (x TunnelFrameKind) Enum() *TunnelFrameKind {
+ p := new(TunnelFrameKind)
+ *p = x
+ return p
+}
+
+func (x TunnelFrameKind) String() string {
+ return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (TunnelFrameKind) Descriptor() protoreflect.EnumDescriptor {
+ return file_proto_v1_gateway_proto_enumTypes[0].Descriptor()
+}
+
+func (TunnelFrameKind) Type() protoreflect.EnumType {
+ return &file_proto_v1_gateway_proto_enumTypes[0]
+}
+
+func (x TunnelFrameKind) Number() protoreflect.EnumNumber {
+ return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use TunnelFrameKind.Descriptor instead.
+func (TunnelFrameKind) EnumDescriptor() ([]byte, []int) {
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{0}
+}
+
type ChatEvent_ChatEventType int32
const (
@@ -69,11 +145,11 @@ func (x ChatEvent_ChatEventType) String() string {
}
func (ChatEvent_ChatEventType) Descriptor() protoreflect.EnumDescriptor {
- return file_proto_v1_gateway_proto_enumTypes[0].Descriptor()
+ return file_proto_v1_gateway_proto_enumTypes[1].Descriptor()
}
func (ChatEvent_ChatEventType) Type() protoreflect.EnumType {
- return &file_proto_v1_gateway_proto_enumTypes[0]
+ return &file_proto_v1_gateway_proto_enumTypes[1]
}
func (x ChatEvent_ChatEventType) Number() protoreflect.EnumNumber {
@@ -82,7 +158,7 @@ func (x ChatEvent_ChatEventType) Number() protoreflect.EnumNumber {
// Deprecated: Use ChatEvent_ChatEventType.Descriptor instead.
func (ChatEvent_ChatEventType) EnumDescriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{23, 0}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{28, 0}
}
type AuthRequest struct {
@@ -248,6 +324,9 @@ type GatewayEnvelope struct {
// *GatewayEnvelope_GitRequest
// *GatewayEnvelope_FsReadEditableText
// *GatewayEnvelope_FsReadWorkspaceImage
+ // *GatewayEnvelope_TunnelControl
+ // *GatewayEnvelope_TunnelControlResp
+ // *GatewayEnvelope_TunnelFrame
Payload isGatewayEnvelope_Payload `protobuf_oneof:"payload"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
@@ -637,6 +716,33 @@ func (x *GatewayEnvelope) GetFsReadWorkspaceImage() *FsReadWorkspaceImageRequest
return nil
}
+func (x *GatewayEnvelope) GetTunnelControl() *TunnelControlRequest {
+ if x != nil {
+ if x, ok := x.Payload.(*GatewayEnvelope_TunnelControl); ok {
+ return x.TunnelControl
+ }
+ }
+ return nil
+}
+
+func (x *GatewayEnvelope) GetTunnelControlResp() *TunnelControlResponse {
+ if x != nil {
+ if x, ok := x.Payload.(*GatewayEnvelope_TunnelControlResp); ok {
+ return x.TunnelControlResp
+ }
+ }
+ return nil
+}
+
+func (x *GatewayEnvelope) GetTunnelFrame() *TunnelFrame {
+ if x != nil {
+ if x, ok := x.Payload.(*GatewayEnvelope_TunnelFrame); ok {
+ return x.TunnelFrame
+ }
+ }
+ return nil
+}
+
type isGatewayEnvelope_Payload interface {
isGatewayEnvelope_Payload()
}
@@ -789,6 +895,18 @@ type GatewayEnvelope_FsReadWorkspaceImage struct {
FsReadWorkspaceImage *FsReadWorkspaceImageRequest `protobuf:"bytes,63,opt,name=fs_read_workspace_image,json=fsReadWorkspaceImage,proto3,oneof"`
}
+type GatewayEnvelope_TunnelControl struct {
+ TunnelControl *TunnelControlRequest `protobuf:"bytes,67,opt,name=tunnel_control,json=tunnelControl,proto3,oneof"`
+}
+
+type GatewayEnvelope_TunnelControlResp struct {
+ TunnelControlResp *TunnelControlResponse `protobuf:"bytes,68,opt,name=tunnel_control_resp,json=tunnelControlResp,proto3,oneof"`
+}
+
+type GatewayEnvelope_TunnelFrame struct {
+ TunnelFrame *TunnelFrame `protobuf:"bytes,69,opt,name=tunnel_frame,json=tunnelFrame,proto3,oneof"`
+}
+
func (*GatewayEnvelope_ChatRequest) isGatewayEnvelope_Payload() {}
func (*GatewayEnvelope_CancelChat) isGatewayEnvelope_Payload() {}
@@ -863,6 +981,12 @@ func (*GatewayEnvelope_FsReadEditableText) isGatewayEnvelope_Payload() {}
func (*GatewayEnvelope_FsReadWorkspaceImage) isGatewayEnvelope_Payload() {}
+func (*GatewayEnvelope_TunnelControl) isGatewayEnvelope_Payload() {}
+
+func (*GatewayEnvelope_TunnelControlResp) isGatewayEnvelope_Payload() {}
+
+func (*GatewayEnvelope_TunnelFrame) isGatewayEnvelope_Payload() {}
+
type AgentEnvelope struct {
state protoimpl.MessageState `protogen:"open.v1"`
RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"`
@@ -908,6 +1032,9 @@ type AgentEnvelope struct {
// *AgentEnvelope_GitResponse
// *AgentEnvelope_FsReadEditableTextResp
// *AgentEnvelope_FsReadWorkspaceImageResp
+ // *AgentEnvelope_TunnelControl
+ // *AgentEnvelope_TunnelControlResp
+ // *AgentEnvelope_TunnelFrame
// *AgentEnvelope_Error
Payload isAgentEnvelope_Payload `protobuf_oneof:"payload"`
unknownFields protoimpl.UnknownFields
@@ -1316,6 +1443,33 @@ func (x *AgentEnvelope) GetFsReadWorkspaceImageResp() *FsReadWorkspaceImageRespo
return nil
}
+func (x *AgentEnvelope) GetTunnelControl() *TunnelControlRequest {
+ if x != nil {
+ if x, ok := x.Payload.(*AgentEnvelope_TunnelControl); ok {
+ return x.TunnelControl
+ }
+ }
+ return nil
+}
+
+func (x *AgentEnvelope) GetTunnelControlResp() *TunnelControlResponse {
+ if x != nil {
+ if x, ok := x.Payload.(*AgentEnvelope_TunnelControlResp); ok {
+ return x.TunnelControlResp
+ }
+ }
+ return nil
+}
+
+func (x *AgentEnvelope) GetTunnelFrame() *TunnelFrame {
+ if x != nil {
+ if x, ok := x.Payload.(*AgentEnvelope_TunnelFrame); ok {
+ return x.TunnelFrame
+ }
+ }
+ return nil
+}
+
func (x *AgentEnvelope) GetError() *ErrorResponse {
if x != nil {
if x, ok := x.Payload.(*AgentEnvelope_Error); ok {
@@ -1485,6 +1639,18 @@ type AgentEnvelope_FsReadWorkspaceImageResp struct {
FsReadWorkspaceImageResp *FsReadWorkspaceImageResponse `protobuf:"bytes,66,opt,name=fs_read_workspace_image_resp,json=fsReadWorkspaceImageResp,proto3,oneof"`
}
+type AgentEnvelope_TunnelControl struct {
+ TunnelControl *TunnelControlRequest `protobuf:"bytes,67,opt,name=tunnel_control,json=tunnelControl,proto3,oneof"`
+}
+
+type AgentEnvelope_TunnelControlResp struct {
+ TunnelControlResp *TunnelControlResponse `protobuf:"bytes,68,opt,name=tunnel_control_resp,json=tunnelControlResp,proto3,oneof"`
+}
+
+type AgentEnvelope_TunnelFrame struct {
+ TunnelFrame *TunnelFrame `protobuf:"bytes,69,opt,name=tunnel_frame,json=tunnelFrame,proto3,oneof"`
+}
+
type AgentEnvelope_Error struct {
Error *ErrorResponse `protobuf:"bytes,99,opt,name=error,proto3,oneof"`
}
@@ -1567,6 +1733,12 @@ func (*AgentEnvelope_FsReadEditableTextResp) isAgentEnvelope_Payload() {}
func (*AgentEnvelope_FsReadWorkspaceImageResp) isAgentEnvelope_Payload() {}
+func (*AgentEnvelope_TunnelControl) isAgentEnvelope_Payload() {}
+
+func (*AgentEnvelope_TunnelControlResp) isAgentEnvelope_Payload() {}
+
+func (*AgentEnvelope_TunnelFrame) isAgentEnvelope_Payload() {}
+
func (*AgentEnvelope_Error) isAgentEnvelope_Payload() {}
type ChatSelectedModel struct {
@@ -2000,35 +2172,527 @@ func (x *UploadedImagePreviewResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
-func (*UploadedImagePreviewResponse) ProtoMessage() {}
+func (*UploadedImagePreviewResponse) ProtoMessage() {}
+
+func (x *UploadedImagePreviewResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_v1_gateway_proto_msgTypes[11]
+ 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 UploadedImagePreviewResponse.ProtoReflect.Descriptor instead.
+func (*UploadedImagePreviewResponse) Descriptor() ([]byte, []int) {
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{11}
+}
+
+func (x *UploadedImagePreviewResponse) GetMimeType() string {
+ if x != nil {
+ return x.MimeType
+ }
+ return ""
+}
+
+func (x *UploadedImagePreviewResponse) GetData() string {
+ if x != nil {
+ return x.Data
+ }
+ return ""
+}
+
+type TunnelControlRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Action string `protobuf:"bytes,1,opt,name=action,proto3" json:"action,omitempty"`
+ TunnelId string `protobuf:"bytes,2,opt,name=tunnel_id,json=tunnelId,proto3" json:"tunnel_id,omitempty"`
+ Slug string `protobuf:"bytes,3,opt,name=slug,proto3" json:"slug,omitempty"`
+ TargetUrl string `protobuf:"bytes,4,opt,name=target_url,json=targetUrl,proto3" json:"target_url,omitempty"`
+ Name string `protobuf:"bytes,5,opt,name=name,proto3" json:"name,omitempty"`
+ TtlSeconds uint32 `protobuf:"varint,6,opt,name=ttl_seconds,json=ttlSeconds,proto3" json:"ttl_seconds,omitempty"`
+ ExpiresAt int64 `protobuf:"varint,7,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"`
+ PublicUrl string `protobuf:"bytes,8,opt,name=public_url,json=publicUrl,proto3" json:"public_url,omitempty"`
+ PublicBaseUrl string `protobuf:"bytes,9,opt,name=public_base_url,json=publicBaseUrl,proto3" json:"public_base_url,omitempty"`
+ ProjectPathKey string `protobuf:"bytes,10,opt,name=project_path_key,json=projectPathKey,proto3" json:"project_path_key,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *TunnelControlRequest) Reset() {
+ *x = TunnelControlRequest{}
+ mi := &file_proto_v1_gateway_proto_msgTypes[12]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *TunnelControlRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*TunnelControlRequest) ProtoMessage() {}
+
+func (x *TunnelControlRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_v1_gateway_proto_msgTypes[12]
+ 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 TunnelControlRequest.ProtoReflect.Descriptor instead.
+func (*TunnelControlRequest) Descriptor() ([]byte, []int) {
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{12}
+}
+
+func (x *TunnelControlRequest) GetAction() string {
+ if x != nil {
+ return x.Action
+ }
+ return ""
+}
+
+func (x *TunnelControlRequest) GetTunnelId() string {
+ if x != nil {
+ return x.TunnelId
+ }
+ return ""
+}
+
+func (x *TunnelControlRequest) GetSlug() string {
+ if x != nil {
+ return x.Slug
+ }
+ return ""
+}
+
+func (x *TunnelControlRequest) GetTargetUrl() string {
+ if x != nil {
+ return x.TargetUrl
+ }
+ return ""
+}
+
+func (x *TunnelControlRequest) GetName() string {
+ if x != nil {
+ return x.Name
+ }
+ return ""
+}
+
+func (x *TunnelControlRequest) GetTtlSeconds() uint32 {
+ if x != nil {
+ return x.TtlSeconds
+ }
+ return 0
+}
+
+func (x *TunnelControlRequest) GetExpiresAt() int64 {
+ if x != nil {
+ return x.ExpiresAt
+ }
+ return 0
+}
+
+func (x *TunnelControlRequest) GetPublicUrl() string {
+ if x != nil {
+ return x.PublicUrl
+ }
+ return ""
+}
+
+func (x *TunnelControlRequest) GetPublicBaseUrl() string {
+ if x != nil {
+ return x.PublicBaseUrl
+ }
+ return ""
+}
+
+func (x *TunnelControlRequest) GetProjectPathKey() string {
+ if x != nil {
+ return x.ProjectPathKey
+ }
+ return ""
+}
+
+type TunnelControlResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Action string `protobuf:"bytes,1,opt,name=action,proto3" json:"action,omitempty"`
+ Tunnels []*TunnelSummary `protobuf:"bytes,2,rep,name=tunnels,proto3" json:"tunnels,omitempty"`
+ Tunnel *TunnelSummary `protobuf:"bytes,3,opt,name=tunnel,proto3" json:"tunnel,omitempty"`
+ ErrorCode string `protobuf:"bytes,4,opt,name=error_code,json=errorCode,proto3" json:"error_code,omitempty"`
+ ErrorMessage string `protobuf:"bytes,5,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *TunnelControlResponse) Reset() {
+ *x = TunnelControlResponse{}
+ mi := &file_proto_v1_gateway_proto_msgTypes[13]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *TunnelControlResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*TunnelControlResponse) ProtoMessage() {}
+
+func (x *TunnelControlResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_v1_gateway_proto_msgTypes[13]
+ 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 TunnelControlResponse.ProtoReflect.Descriptor instead.
+func (*TunnelControlResponse) Descriptor() ([]byte, []int) {
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{13}
+}
+
+func (x *TunnelControlResponse) GetAction() string {
+ if x != nil {
+ return x.Action
+ }
+ return ""
+}
+
+func (x *TunnelControlResponse) GetTunnels() []*TunnelSummary {
+ if x != nil {
+ return x.Tunnels
+ }
+ return nil
+}
+
+func (x *TunnelControlResponse) GetTunnel() *TunnelSummary {
+ if x != nil {
+ return x.Tunnel
+ }
+ return nil
+}
+
+func (x *TunnelControlResponse) GetErrorCode() string {
+ if x != nil {
+ return x.ErrorCode
+ }
+ return ""
+}
+
+func (x *TunnelControlResponse) GetErrorMessage() string {
+ if x != nil {
+ return x.ErrorMessage
+ }
+ return ""
+}
+
+type TunnelSummary struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
+ Slug string `protobuf:"bytes,2,opt,name=slug,proto3" json:"slug,omitempty"`
+ Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`
+ TargetUrl string `protobuf:"bytes,4,opt,name=target_url,json=targetUrl,proto3" json:"target_url,omitempty"`
+ PublicUrl string `protobuf:"bytes,5,opt,name=public_url,json=publicUrl,proto3" json:"public_url,omitempty"`
+ CreatedAt int64 `protobuf:"varint,6,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
+ ExpiresAt int64 `protobuf:"varint,7,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"`
+ ActiveConnections uint32 `protobuf:"varint,8,opt,name=active_connections,json=activeConnections,proto3" json:"active_connections,omitempty"`
+ Status string `protobuf:"bytes,9,opt,name=status,proto3" json:"status,omitempty"`
+ ProjectPathKey string `protobuf:"bytes,10,opt,name=project_path_key,json=projectPathKey,proto3" json:"project_path_key,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *TunnelSummary) Reset() {
+ *x = TunnelSummary{}
+ mi := &file_proto_v1_gateway_proto_msgTypes[14]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *TunnelSummary) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*TunnelSummary) ProtoMessage() {}
+
+func (x *TunnelSummary) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_v1_gateway_proto_msgTypes[14]
+ 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 TunnelSummary.ProtoReflect.Descriptor instead.
+func (*TunnelSummary) Descriptor() ([]byte, []int) {
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{14}
+}
+
+func (x *TunnelSummary) GetId() string {
+ if x != nil {
+ return x.Id
+ }
+ return ""
+}
+
+func (x *TunnelSummary) GetSlug() string {
+ if x != nil {
+ return x.Slug
+ }
+ return ""
+}
+
+func (x *TunnelSummary) GetName() string {
+ if x != nil {
+ return x.Name
+ }
+ return ""
+}
+
+func (x *TunnelSummary) GetTargetUrl() string {
+ if x != nil {
+ return x.TargetUrl
+ }
+ return ""
+}
+
+func (x *TunnelSummary) GetPublicUrl() string {
+ if x != nil {
+ return x.PublicUrl
+ }
+ return ""
+}
+
+func (x *TunnelSummary) GetCreatedAt() int64 {
+ if x != nil {
+ return x.CreatedAt
+ }
+ return 0
+}
+
+func (x *TunnelSummary) GetExpiresAt() int64 {
+ if x != nil {
+ return x.ExpiresAt
+ }
+ return 0
+}
+
+func (x *TunnelSummary) GetActiveConnections() uint32 {
+ if x != nil {
+ return x.ActiveConnections
+ }
+ return 0
+}
+
+func (x *TunnelSummary) GetStatus() string {
+ if x != nil {
+ return x.Status
+ }
+ return ""
+}
+
+func (x *TunnelSummary) GetProjectPathKey() string {
+ if x != nil {
+ return x.ProjectPathKey
+ }
+ return ""
+}
+
+type TunnelHeader struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
+ Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *TunnelHeader) Reset() {
+ *x = TunnelHeader{}
+ mi := &file_proto_v1_gateway_proto_msgTypes[15]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *TunnelHeader) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*TunnelHeader) ProtoMessage() {}
+
+func (x *TunnelHeader) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_v1_gateway_proto_msgTypes[15]
+ 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 TunnelHeader.ProtoReflect.Descriptor instead.
+func (*TunnelHeader) Descriptor() ([]byte, []int) {
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{15}
+}
+
+func (x *TunnelHeader) GetName() string {
+ if x != nil {
+ return x.Name
+ }
+ return ""
+}
+
+func (x *TunnelHeader) GetValue() string {
+ if x != nil {
+ return x.Value
+ }
+ return ""
+}
+
+type TunnelFrame struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ StreamId string `protobuf:"bytes,1,opt,name=stream_id,json=streamId,proto3" json:"stream_id,omitempty"`
+ TunnelId string `protobuf:"bytes,2,opt,name=tunnel_id,json=tunnelId,proto3" json:"tunnel_id,omitempty"`
+ Slug string `protobuf:"bytes,3,opt,name=slug,proto3" json:"slug,omitempty"`
+ Kind TunnelFrameKind `protobuf:"varint,4,opt,name=kind,proto3,enum=liveagent.gateway.v1.TunnelFrameKind" json:"kind,omitempty"`
+ Method string `protobuf:"bytes,5,opt,name=method,proto3" json:"method,omitempty"`
+ Path string `protobuf:"bytes,6,opt,name=path,proto3" json:"path,omitempty"`
+ Headers []*TunnelHeader `protobuf:"bytes,7,rep,name=headers,proto3" json:"headers,omitempty"`
+ StatusCode uint32 `protobuf:"varint,8,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"`
+ Body []byte `protobuf:"bytes,9,opt,name=body,proto3" json:"body,omitempty"`
+ EndStream bool `protobuf:"varint,10,opt,name=end_stream,json=endStream,proto3" json:"end_stream,omitempty"`
+ Error string `protobuf:"bytes,11,opt,name=error,proto3" json:"error,omitempty"`
+ WsMessageType string `protobuf:"bytes,12,opt,name=ws_message_type,json=wsMessageType,proto3" json:"ws_message_type,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *TunnelFrame) Reset() {
+ *x = TunnelFrame{}
+ mi := &file_proto_v1_gateway_proto_msgTypes[16]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *TunnelFrame) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*TunnelFrame) ProtoMessage() {}
+
+func (x *TunnelFrame) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_v1_gateway_proto_msgTypes[16]
+ 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 TunnelFrame.ProtoReflect.Descriptor instead.
+func (*TunnelFrame) Descriptor() ([]byte, []int) {
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{16}
+}
+
+func (x *TunnelFrame) GetStreamId() string {
+ if x != nil {
+ return x.StreamId
+ }
+ return ""
+}
+
+func (x *TunnelFrame) GetTunnelId() string {
+ if x != nil {
+ return x.TunnelId
+ }
+ return ""
+}
+
+func (x *TunnelFrame) GetSlug() string {
+ if x != nil {
+ return x.Slug
+ }
+ return ""
+}
+
+func (x *TunnelFrame) GetKind() TunnelFrameKind {
+ if x != nil {
+ return x.Kind
+ }
+ return TunnelFrameKind_TUNNEL_FRAME_KIND_UNSPECIFIED
+}
+
+func (x *TunnelFrame) GetMethod() string {
+ if x != nil {
+ return x.Method
+ }
+ return ""
+}
+
+func (x *TunnelFrame) GetPath() string {
+ if x != nil {
+ return x.Path
+ }
+ return ""
+}
+
+func (x *TunnelFrame) GetHeaders() []*TunnelHeader {
+ if x != nil {
+ return x.Headers
+ }
+ return nil
+}
+
+func (x *TunnelFrame) GetStatusCode() uint32 {
+ if x != nil {
+ return x.StatusCode
+ }
+ return 0
+}
-func (x *UploadedImagePreviewResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[11]
+func (x *TunnelFrame) GetBody() []byte {
if x != nil {
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- if ms.LoadMessageInfo() == nil {
- ms.StoreMessageInfo(mi)
- }
- return ms
+ return x.Body
}
- return mi.MessageOf(x)
+ return nil
}
-// Deprecated: Use UploadedImagePreviewResponse.ProtoReflect.Descriptor instead.
-func (*UploadedImagePreviewResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{11}
+func (x *TunnelFrame) GetEndStream() bool {
+ if x != nil {
+ return x.EndStream
+ }
+ return false
}
-func (x *UploadedImagePreviewResponse) GetMimeType() string {
+func (x *TunnelFrame) GetError() string {
if x != nil {
- return x.MimeType
+ return x.Error
}
return ""
}
-func (x *UploadedImagePreviewResponse) GetData() string {
+func (x *TunnelFrame) GetWsMessageType() string {
if x != nil {
- return x.Data
+ return x.WsMessageType
}
return ""
}
@@ -2043,7 +2707,7 @@ type MemoryManageRequest struct {
func (x *MemoryManageRequest) Reset() {
*x = MemoryManageRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[12]
+ mi := &file_proto_v1_gateway_proto_msgTypes[17]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2055,7 +2719,7 @@ func (x *MemoryManageRequest) String() string {
func (*MemoryManageRequest) ProtoMessage() {}
func (x *MemoryManageRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[12]
+ mi := &file_proto_v1_gateway_proto_msgTypes[17]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2068,7 +2732,7 @@ func (x *MemoryManageRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use MemoryManageRequest.ProtoReflect.Descriptor instead.
func (*MemoryManageRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{12}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{17}
}
func (x *MemoryManageRequest) GetCommand() string {
@@ -2094,7 +2758,7 @@ type MemoryManageResponse struct {
func (x *MemoryManageResponse) Reset() {
*x = MemoryManageResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[13]
+ mi := &file_proto_v1_gateway_proto_msgTypes[18]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2106,7 +2770,7 @@ func (x *MemoryManageResponse) String() string {
func (*MemoryManageResponse) ProtoMessage() {}
func (x *MemoryManageResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[13]
+ mi := &file_proto_v1_gateway_proto_msgTypes[18]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2119,7 +2783,7 @@ func (x *MemoryManageResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use MemoryManageResponse.ProtoReflect.Descriptor instead.
func (*MemoryManageResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{13}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{18}
}
func (x *MemoryManageResponse) GetResultJson() string {
@@ -2147,7 +2811,7 @@ type TerminalRequest struct {
func (x *TerminalRequest) Reset() {
*x = TerminalRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[14]
+ mi := &file_proto_v1_gateway_proto_msgTypes[19]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2159,7 +2823,7 @@ func (x *TerminalRequest) String() string {
func (*TerminalRequest) ProtoMessage() {}
func (x *TerminalRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[14]
+ mi := &file_proto_v1_gateway_proto_msgTypes[19]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2172,7 +2836,7 @@ func (x *TerminalRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use TerminalRequest.ProtoReflect.Descriptor instead.
func (*TerminalRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{14}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{19}
}
func (x *TerminalRequest) GetAction() string {
@@ -2266,7 +2930,7 @@ type TerminalSession struct {
func (x *TerminalSession) Reset() {
*x = TerminalSession{}
- mi := &file_proto_v1_gateway_proto_msgTypes[15]
+ mi := &file_proto_v1_gateway_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2278,7 +2942,7 @@ func (x *TerminalSession) String() string {
func (*TerminalSession) ProtoMessage() {}
func (x *TerminalSession) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[15]
+ mi := &file_proto_v1_gateway_proto_msgTypes[20]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2291,7 +2955,7 @@ func (x *TerminalSession) ProtoReflect() protoreflect.Message {
// Deprecated: Use TerminalSession.ProtoReflect.Descriptor instead.
func (*TerminalSession) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{15}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{20}
}
func (x *TerminalSession) GetId() string {
@@ -2396,7 +3060,7 @@ type TerminalShellOption struct {
func (x *TerminalShellOption) Reset() {
*x = TerminalShellOption{}
- mi := &file_proto_v1_gateway_proto_msgTypes[16]
+ mi := &file_proto_v1_gateway_proto_msgTypes[21]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2408,7 +3072,7 @@ func (x *TerminalShellOption) String() string {
func (*TerminalShellOption) ProtoMessage() {}
func (x *TerminalShellOption) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[16]
+ mi := &file_proto_v1_gateway_proto_msgTypes[21]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2421,7 +3085,7 @@ func (x *TerminalShellOption) ProtoReflect() protoreflect.Message {
// Deprecated: Use TerminalShellOption.ProtoReflect.Descriptor instead.
func (*TerminalShellOption) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{16}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{21}
}
func (x *TerminalShellOption) GetId() string {
@@ -2462,7 +3126,7 @@ type TerminalResponse struct {
func (x *TerminalResponse) Reset() {
*x = TerminalResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[17]
+ mi := &file_proto_v1_gateway_proto_msgTypes[22]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2474,7 +3138,7 @@ func (x *TerminalResponse) String() string {
func (*TerminalResponse) ProtoMessage() {}
func (x *TerminalResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[17]
+ mi := &file_proto_v1_gateway_proto_msgTypes[22]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2487,7 +3151,7 @@ func (x *TerminalResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use TerminalResponse.ProtoReflect.Descriptor instead.
func (*TerminalResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{17}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{22}
}
func (x *TerminalResponse) GetAction() string {
@@ -2568,7 +3232,7 @@ type TerminalEvent struct {
func (x *TerminalEvent) Reset() {
*x = TerminalEvent{}
- mi := &file_proto_v1_gateway_proto_msgTypes[18]
+ mi := &file_proto_v1_gateway_proto_msgTypes[23]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2580,7 +3244,7 @@ func (x *TerminalEvent) String() string {
func (*TerminalEvent) ProtoMessage() {}
func (x *TerminalEvent) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[18]
+ mi := &file_proto_v1_gateway_proto_msgTypes[23]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2593,7 +3257,7 @@ func (x *TerminalEvent) ProtoReflect() protoreflect.Message {
// Deprecated: Use TerminalEvent.ProtoReflect.Descriptor instead.
func (*TerminalEvent) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{18}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{23}
}
func (x *TerminalEvent) GetKind() string {
@@ -2656,7 +3320,7 @@ type GitRequest struct {
func (x *GitRequest) Reset() {
*x = GitRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[19]
+ mi := &file_proto_v1_gateway_proto_msgTypes[24]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2668,7 +3332,7 @@ func (x *GitRequest) String() string {
func (*GitRequest) ProtoMessage() {}
func (x *GitRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[19]
+ mi := &file_proto_v1_gateway_proto_msgTypes[24]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2681,7 +3345,7 @@ func (x *GitRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use GitRequest.ProtoReflect.Descriptor instead.
func (*GitRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{19}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{24}
}
func (x *GitRequest) GetAction() string {
@@ -2715,7 +3379,7 @@ type GitResponse struct {
func (x *GitResponse) Reset() {
*x = GitResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[20]
+ mi := &file_proto_v1_gateway_proto_msgTypes[25]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2727,7 +3391,7 @@ func (x *GitResponse) String() string {
func (*GitResponse) ProtoMessage() {}
func (x *GitResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[20]
+ mi := &file_proto_v1_gateway_proto_msgTypes[25]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2740,7 +3404,7 @@ func (x *GitResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use GitResponse.ProtoReflect.Descriptor instead.
func (*GitResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{20}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{25}
}
func (x *GitResponse) GetAction() string {
@@ -2774,7 +3438,7 @@ type ChatRequest struct {
func (x *ChatRequest) Reset() {
*x = ChatRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[21]
+ mi := &file_proto_v1_gateway_proto_msgTypes[26]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2786,7 +3450,7 @@ func (x *ChatRequest) String() string {
func (*ChatRequest) ProtoMessage() {}
func (x *ChatRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[21]
+ mi := &file_proto_v1_gateway_proto_msgTypes[26]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2799,7 +3463,7 @@ func (x *ChatRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ChatRequest.ProtoReflect.Descriptor instead.
func (*ChatRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{21}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{26}
}
func (x *ChatRequest) GetConversationId() string {
@@ -2874,7 +3538,7 @@ type CancelChatRequest struct {
func (x *CancelChatRequest) Reset() {
*x = CancelChatRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[22]
+ mi := &file_proto_v1_gateway_proto_msgTypes[27]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2886,7 +3550,7 @@ func (x *CancelChatRequest) String() string {
func (*CancelChatRequest) ProtoMessage() {}
func (x *CancelChatRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[22]
+ mi := &file_proto_v1_gateway_proto_msgTypes[27]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2899,7 +3563,7 @@ func (x *CancelChatRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use CancelChatRequest.ProtoReflect.Descriptor instead.
func (*CancelChatRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{22}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{27}
}
func (x *CancelChatRequest) GetConversationId() string {
@@ -2920,7 +3584,7 @@ type ChatEvent struct {
func (x *ChatEvent) Reset() {
*x = ChatEvent{}
- mi := &file_proto_v1_gateway_proto_msgTypes[23]
+ mi := &file_proto_v1_gateway_proto_msgTypes[28]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2932,7 +3596,7 @@ func (x *ChatEvent) String() string {
func (*ChatEvent) ProtoMessage() {}
func (x *ChatEvent) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[23]
+ mi := &file_proto_v1_gateway_proto_msgTypes[28]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2945,7 +3609,7 @@ func (x *ChatEvent) ProtoReflect() protoreflect.Message {
// Deprecated: Use ChatEvent.ProtoReflect.Descriptor instead.
func (*ChatEvent) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{23}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{28}
}
func (x *ChatEvent) GetType() ChatEvent_ChatEventType {
@@ -2980,7 +3644,7 @@ type CronManageRequest struct {
func (x *CronManageRequest) Reset() {
*x = CronManageRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[24]
+ mi := &file_proto_v1_gateway_proto_msgTypes[29]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2992,7 +3656,7 @@ func (x *CronManageRequest) String() string {
func (*CronManageRequest) ProtoMessage() {}
func (x *CronManageRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[24]
+ mi := &file_proto_v1_gateway_proto_msgTypes[29]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3005,7 +3669,7 @@ func (x *CronManageRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use CronManageRequest.ProtoReflect.Descriptor instead.
func (*CronManageRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{24}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{29}
}
func (x *CronManageRequest) GetAction() string {
@@ -3039,7 +3703,7 @@ type CronManageResponse struct {
func (x *CronManageResponse) Reset() {
*x = CronManageResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[25]
+ mi := &file_proto_v1_gateway_proto_msgTypes[30]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3051,7 +3715,7 @@ func (x *CronManageResponse) String() string {
func (*CronManageResponse) ProtoMessage() {}
func (x *CronManageResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[25]
+ mi := &file_proto_v1_gateway_proto_msgTypes[30]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3064,7 +3728,7 @@ func (x *CronManageResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use CronManageResponse.ProtoReflect.Descriptor instead.
func (*CronManageResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{25}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{30}
}
func (x *CronManageResponse) GetAction() string {
@@ -3093,7 +3757,7 @@ type HistoryListRequest struct {
func (x *HistoryListRequest) Reset() {
*x = HistoryListRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[26]
+ mi := &file_proto_v1_gateway_proto_msgTypes[31]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3105,7 +3769,7 @@ func (x *HistoryListRequest) String() string {
func (*HistoryListRequest) ProtoMessage() {}
func (x *HistoryListRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[26]
+ mi := &file_proto_v1_gateway_proto_msgTypes[31]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3118,7 +3782,7 @@ func (x *HistoryListRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryListRequest.ProtoReflect.Descriptor instead.
func (*HistoryListRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{26}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{31}
}
func (x *HistoryListRequest) GetPage() int32 {
@@ -3159,7 +3823,7 @@ type HistoryListResponse struct {
func (x *HistoryListResponse) Reset() {
*x = HistoryListResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[27]
+ mi := &file_proto_v1_gateway_proto_msgTypes[32]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3171,7 +3835,7 @@ func (x *HistoryListResponse) String() string {
func (*HistoryListResponse) ProtoMessage() {}
func (x *HistoryListResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[27]
+ mi := &file_proto_v1_gateway_proto_msgTypes[32]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3184,7 +3848,7 @@ func (x *HistoryListResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryListResponse.ProtoReflect.Descriptor instead.
func (*HistoryListResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{27}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{32}
}
func (x *HistoryListResponse) GetConversations() []*ConversationSummary {
@@ -3221,7 +3885,7 @@ type ConversationSummary struct {
func (x *ConversationSummary) Reset() {
*x = ConversationSummary{}
- mi := &file_proto_v1_gateway_proto_msgTypes[28]
+ mi := &file_proto_v1_gateway_proto_msgTypes[33]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3233,7 +3897,7 @@ func (x *ConversationSummary) String() string {
func (*ConversationSummary) ProtoMessage() {}
func (x *ConversationSummary) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[28]
+ mi := &file_proto_v1_gateway_proto_msgTypes[33]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3246,7 +3910,7 @@ func (x *ConversationSummary) ProtoReflect() protoreflect.Message {
// Deprecated: Use ConversationSummary.ProtoReflect.Descriptor instead.
func (*ConversationSummary) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{28}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{33}
}
func (x *ConversationSummary) GetId() string {
@@ -3343,7 +4007,7 @@ type HistoryGetRequest struct {
func (x *HistoryGetRequest) Reset() {
*x = HistoryGetRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[29]
+ mi := &file_proto_v1_gateway_proto_msgTypes[34]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3355,7 +4019,7 @@ func (x *HistoryGetRequest) String() string {
func (*HistoryGetRequest) ProtoMessage() {}
func (x *HistoryGetRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[29]
+ mi := &file_proto_v1_gateway_proto_msgTypes[34]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3368,7 +4032,7 @@ func (x *HistoryGetRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryGetRequest.ProtoReflect.Descriptor instead.
func (*HistoryGetRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{29}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{34}
}
func (x *HistoryGetRequest) GetConversationId() string {
@@ -3399,7 +4063,7 @@ type HistoryGetResponse struct {
func (x *HistoryGetResponse) Reset() {
*x = HistoryGetResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[30]
+ mi := &file_proto_v1_gateway_proto_msgTypes[35]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3411,7 +4075,7 @@ func (x *HistoryGetResponse) String() string {
func (*HistoryGetResponse) ProtoMessage() {}
func (x *HistoryGetResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[30]
+ mi := &file_proto_v1_gateway_proto_msgTypes[35]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3424,7 +4088,7 @@ func (x *HistoryGetResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryGetResponse.ProtoReflect.Descriptor instead.
func (*HistoryGetResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{30}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{35}
}
func (x *HistoryGetResponse) GetConversationId() string {
@@ -3479,7 +4143,7 @@ type HistoryRenameRequest struct {
func (x *HistoryRenameRequest) Reset() {
*x = HistoryRenameRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[31]
+ mi := &file_proto_v1_gateway_proto_msgTypes[36]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3491,7 +4155,7 @@ func (x *HistoryRenameRequest) String() string {
func (*HistoryRenameRequest) ProtoMessage() {}
func (x *HistoryRenameRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[31]
+ mi := &file_proto_v1_gateway_proto_msgTypes[36]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3504,7 +4168,7 @@ func (x *HistoryRenameRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryRenameRequest.ProtoReflect.Descriptor instead.
func (*HistoryRenameRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{31}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{36}
}
func (x *HistoryRenameRequest) GetConversationId() string {
@@ -3530,7 +4194,7 @@ type HistoryRenameResponse struct {
func (x *HistoryRenameResponse) Reset() {
*x = HistoryRenameResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[32]
+ mi := &file_proto_v1_gateway_proto_msgTypes[37]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3542,7 +4206,7 @@ func (x *HistoryRenameResponse) String() string {
func (*HistoryRenameResponse) ProtoMessage() {}
func (x *HistoryRenameResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[32]
+ mi := &file_proto_v1_gateway_proto_msgTypes[37]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3555,7 +4219,7 @@ func (x *HistoryRenameResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryRenameResponse.ProtoReflect.Descriptor instead.
func (*HistoryRenameResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{32}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{37}
}
func (x *HistoryRenameResponse) GetConversation() *ConversationSummary {
@@ -3575,7 +4239,7 @@ type HistoryPinRequest struct {
func (x *HistoryPinRequest) Reset() {
*x = HistoryPinRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[33]
+ mi := &file_proto_v1_gateway_proto_msgTypes[38]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3587,7 +4251,7 @@ func (x *HistoryPinRequest) String() string {
func (*HistoryPinRequest) ProtoMessage() {}
func (x *HistoryPinRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[33]
+ mi := &file_proto_v1_gateway_proto_msgTypes[38]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3600,7 +4264,7 @@ func (x *HistoryPinRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryPinRequest.ProtoReflect.Descriptor instead.
func (*HistoryPinRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{33}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{38}
}
func (x *HistoryPinRequest) GetConversationId() string {
@@ -3626,7 +4290,7 @@ type HistoryPinResponse struct {
func (x *HistoryPinResponse) Reset() {
*x = HistoryPinResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[34]
+ mi := &file_proto_v1_gateway_proto_msgTypes[39]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3638,7 +4302,7 @@ func (x *HistoryPinResponse) String() string {
func (*HistoryPinResponse) ProtoMessage() {}
func (x *HistoryPinResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[34]
+ mi := &file_proto_v1_gateway_proto_msgTypes[39]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3651,7 +4315,7 @@ func (x *HistoryPinResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryPinResponse.ProtoReflect.Descriptor instead.
func (*HistoryPinResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{34}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{39}
}
func (x *HistoryPinResponse) GetConversation() *ConversationSummary {
@@ -3675,7 +4339,7 @@ type HistoryShareStatus struct {
func (x *HistoryShareStatus) Reset() {
*x = HistoryShareStatus{}
- mi := &file_proto_v1_gateway_proto_msgTypes[35]
+ mi := &file_proto_v1_gateway_proto_msgTypes[40]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3687,7 +4351,7 @@ func (x *HistoryShareStatus) String() string {
func (*HistoryShareStatus) ProtoMessage() {}
func (x *HistoryShareStatus) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[35]
+ mi := &file_proto_v1_gateway_proto_msgTypes[40]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3700,7 +4364,7 @@ func (x *HistoryShareStatus) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryShareStatus.ProtoReflect.Descriptor instead.
func (*HistoryShareStatus) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{35}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{40}
}
func (x *HistoryShareStatus) GetConversationId() string {
@@ -3754,7 +4418,7 @@ type HistoryShareGetRequest struct {
func (x *HistoryShareGetRequest) Reset() {
*x = HistoryShareGetRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[36]
+ mi := &file_proto_v1_gateway_proto_msgTypes[41]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3766,7 +4430,7 @@ func (x *HistoryShareGetRequest) String() string {
func (*HistoryShareGetRequest) ProtoMessage() {}
func (x *HistoryShareGetRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[36]
+ mi := &file_proto_v1_gateway_proto_msgTypes[41]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3779,7 +4443,7 @@ func (x *HistoryShareGetRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryShareGetRequest.ProtoReflect.Descriptor instead.
func (*HistoryShareGetRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{36}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{41}
}
func (x *HistoryShareGetRequest) GetConversationId() string {
@@ -3798,7 +4462,7 @@ type HistoryShareGetResponse struct {
func (x *HistoryShareGetResponse) Reset() {
*x = HistoryShareGetResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[37]
+ mi := &file_proto_v1_gateway_proto_msgTypes[42]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3810,7 +4474,7 @@ func (x *HistoryShareGetResponse) String() string {
func (*HistoryShareGetResponse) ProtoMessage() {}
func (x *HistoryShareGetResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[37]
+ mi := &file_proto_v1_gateway_proto_msgTypes[42]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3823,7 +4487,7 @@ func (x *HistoryShareGetResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryShareGetResponse.ProtoReflect.Descriptor instead.
func (*HistoryShareGetResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{37}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{42}
}
func (x *HistoryShareGetResponse) GetShare() *HistoryShareStatus {
@@ -3844,7 +4508,7 @@ type HistoryShareSetRequest struct {
func (x *HistoryShareSetRequest) Reset() {
*x = HistoryShareSetRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[38]
+ mi := &file_proto_v1_gateway_proto_msgTypes[43]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3856,7 +4520,7 @@ func (x *HistoryShareSetRequest) String() string {
func (*HistoryShareSetRequest) ProtoMessage() {}
func (x *HistoryShareSetRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[38]
+ mi := &file_proto_v1_gateway_proto_msgTypes[43]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3869,7 +4533,7 @@ func (x *HistoryShareSetRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryShareSetRequest.ProtoReflect.Descriptor instead.
func (*HistoryShareSetRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{38}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{43}
}
func (x *HistoryShareSetRequest) GetConversationId() string {
@@ -3902,7 +4566,7 @@ type HistoryShareSetResponse struct {
func (x *HistoryShareSetResponse) Reset() {
*x = HistoryShareSetResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[39]
+ mi := &file_proto_v1_gateway_proto_msgTypes[44]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3914,7 +4578,7 @@ func (x *HistoryShareSetResponse) String() string {
func (*HistoryShareSetResponse) ProtoMessage() {}
func (x *HistoryShareSetResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[39]
+ mi := &file_proto_v1_gateway_proto_msgTypes[44]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3927,7 +4591,7 @@ func (x *HistoryShareSetResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryShareSetResponse.ProtoReflect.Descriptor instead.
func (*HistoryShareSetResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{39}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{44}
}
func (x *HistoryShareSetResponse) GetShare() *HistoryShareStatus {
@@ -3946,7 +4610,7 @@ type HistoryShareResolveRequest struct {
func (x *HistoryShareResolveRequest) Reset() {
*x = HistoryShareResolveRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[40]
+ mi := &file_proto_v1_gateway_proto_msgTypes[45]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3958,7 +4622,7 @@ func (x *HistoryShareResolveRequest) String() string {
func (*HistoryShareResolveRequest) ProtoMessage() {}
func (x *HistoryShareResolveRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[40]
+ mi := &file_proto_v1_gateway_proto_msgTypes[45]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3971,7 +4635,7 @@ func (x *HistoryShareResolveRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryShareResolveRequest.ProtoReflect.Descriptor instead.
func (*HistoryShareResolveRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{40}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{45}
}
func (x *HistoryShareResolveRequest) GetToken() string {
@@ -3994,7 +4658,7 @@ type HistoryShareResolveResponse struct {
func (x *HistoryShareResolveResponse) Reset() {
*x = HistoryShareResolveResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[41]
+ mi := &file_proto_v1_gateway_proto_msgTypes[46]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4006,7 +4670,7 @@ func (x *HistoryShareResolveResponse) String() string {
func (*HistoryShareResolveResponse) ProtoMessage() {}
func (x *HistoryShareResolveResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[41]
+ mi := &file_proto_v1_gateway_proto_msgTypes[46]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4019,7 +4683,7 @@ func (x *HistoryShareResolveResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryShareResolveResponse.ProtoReflect.Descriptor instead.
func (*HistoryShareResolveResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{41}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{46}
}
func (x *HistoryShareResolveResponse) GetConversationId() string {
@@ -4065,7 +4729,7 @@ type HistoryWorkdirsRequest struct {
func (x *HistoryWorkdirsRequest) Reset() {
*x = HistoryWorkdirsRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[42]
+ mi := &file_proto_v1_gateway_proto_msgTypes[47]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4077,7 +4741,7 @@ func (x *HistoryWorkdirsRequest) String() string {
func (*HistoryWorkdirsRequest) ProtoMessage() {}
func (x *HistoryWorkdirsRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[42]
+ mi := &file_proto_v1_gateway_proto_msgTypes[47]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4090,7 +4754,7 @@ func (x *HistoryWorkdirsRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryWorkdirsRequest.ProtoReflect.Descriptor instead.
func (*HistoryWorkdirsRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{42}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{47}
}
type HistoryWorkdirSummary struct {
@@ -4104,7 +4768,7 @@ type HistoryWorkdirSummary struct {
func (x *HistoryWorkdirSummary) Reset() {
*x = HistoryWorkdirSummary{}
- mi := &file_proto_v1_gateway_proto_msgTypes[43]
+ mi := &file_proto_v1_gateway_proto_msgTypes[48]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4116,7 +4780,7 @@ func (x *HistoryWorkdirSummary) String() string {
func (*HistoryWorkdirSummary) ProtoMessage() {}
func (x *HistoryWorkdirSummary) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[43]
+ mi := &file_proto_v1_gateway_proto_msgTypes[48]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4129,7 +4793,7 @@ func (x *HistoryWorkdirSummary) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryWorkdirSummary.ProtoReflect.Descriptor instead.
func (*HistoryWorkdirSummary) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{43}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{48}
}
func (x *HistoryWorkdirSummary) GetPath() string {
@@ -4162,7 +4826,7 @@ type HistoryWorkdirsResponse struct {
func (x *HistoryWorkdirsResponse) Reset() {
*x = HistoryWorkdirsResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[44]
+ mi := &file_proto_v1_gateway_proto_msgTypes[49]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4174,7 +4838,7 @@ func (x *HistoryWorkdirsResponse) String() string {
func (*HistoryWorkdirsResponse) ProtoMessage() {}
func (x *HistoryWorkdirsResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[44]
+ mi := &file_proto_v1_gateway_proto_msgTypes[49]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4187,7 +4851,7 @@ func (x *HistoryWorkdirsResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryWorkdirsResponse.ProtoReflect.Descriptor instead.
func (*HistoryWorkdirsResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{44}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{49}
}
func (x *HistoryWorkdirsResponse) GetWorkdirs() []*HistoryWorkdirSummary {
@@ -4206,7 +4870,7 @@ type HistoryDeleteRequest struct {
func (x *HistoryDeleteRequest) Reset() {
*x = HistoryDeleteRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[45]
+ mi := &file_proto_v1_gateway_proto_msgTypes[50]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4218,7 +4882,7 @@ func (x *HistoryDeleteRequest) String() string {
func (*HistoryDeleteRequest) ProtoMessage() {}
func (x *HistoryDeleteRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[45]
+ mi := &file_proto_v1_gateway_proto_msgTypes[50]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4231,7 +4895,7 @@ func (x *HistoryDeleteRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryDeleteRequest.ProtoReflect.Descriptor instead.
func (*HistoryDeleteRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{45}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{50}
}
func (x *HistoryDeleteRequest) GetConversationId() string {
@@ -4249,7 +4913,7 @@ type HistoryDeleteResponse struct {
func (x *HistoryDeleteResponse) Reset() {
*x = HistoryDeleteResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[46]
+ mi := &file_proto_v1_gateway_proto_msgTypes[51]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4261,7 +4925,7 @@ func (x *HistoryDeleteResponse) String() string {
func (*HistoryDeleteResponse) ProtoMessage() {}
func (x *HistoryDeleteResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[46]
+ mi := &file_proto_v1_gateway_proto_msgTypes[51]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4274,7 +4938,7 @@ func (x *HistoryDeleteResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryDeleteResponse.ProtoReflect.Descriptor instead.
func (*HistoryDeleteResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{46}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{51}
}
type HistoryTruncateRequest struct {
@@ -4289,7 +4953,7 @@ type HistoryTruncateRequest struct {
func (x *HistoryTruncateRequest) Reset() {
*x = HistoryTruncateRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[47]
+ mi := &file_proto_v1_gateway_proto_msgTypes[52]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4301,7 +4965,7 @@ func (x *HistoryTruncateRequest) String() string {
func (*HistoryTruncateRequest) ProtoMessage() {}
func (x *HistoryTruncateRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[47]
+ mi := &file_proto_v1_gateway_proto_msgTypes[52]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4314,7 +4978,7 @@ func (x *HistoryTruncateRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryTruncateRequest.ProtoReflect.Descriptor instead.
func (*HistoryTruncateRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{47}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{52}
}
func (x *HistoryTruncateRequest) GetConversationId() string {
@@ -4356,7 +5020,7 @@ type HistoryTruncateResponse struct {
func (x *HistoryTruncateResponse) Reset() {
*x = HistoryTruncateResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[48]
+ mi := &file_proto_v1_gateway_proto_msgTypes[53]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4368,7 +5032,7 @@ func (x *HistoryTruncateResponse) String() string {
func (*HistoryTruncateResponse) ProtoMessage() {}
func (x *HistoryTruncateResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[48]
+ mi := &file_proto_v1_gateway_proto_msgTypes[53]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4381,7 +5045,7 @@ func (x *HistoryTruncateResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryTruncateResponse.ProtoReflect.Descriptor instead.
func (*HistoryTruncateResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{48}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{53}
}
func (x *HistoryTruncateResponse) GetConversationId() string {
@@ -4416,7 +5080,7 @@ type HistorySyncEvent struct {
func (x *HistorySyncEvent) Reset() {
*x = HistorySyncEvent{}
- mi := &file_proto_v1_gateway_proto_msgTypes[49]
+ mi := &file_proto_v1_gateway_proto_msgTypes[54]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4428,7 +5092,7 @@ func (x *HistorySyncEvent) String() string {
func (*HistorySyncEvent) ProtoMessage() {}
func (x *HistorySyncEvent) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[49]
+ mi := &file_proto_v1_gateway_proto_msgTypes[54]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4441,7 +5105,7 @@ func (x *HistorySyncEvent) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistorySyncEvent.ProtoReflect.Descriptor instead.
func (*HistorySyncEvent) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{49}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{54}
}
func (x *HistorySyncEvent) GetKind() string {
@@ -4473,7 +5137,7 @@ type ProviderListRequest struct {
func (x *ProviderListRequest) Reset() {
*x = ProviderListRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[50]
+ mi := &file_proto_v1_gateway_proto_msgTypes[55]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4485,7 +5149,7 @@ func (x *ProviderListRequest) String() string {
func (*ProviderListRequest) ProtoMessage() {}
func (x *ProviderListRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[50]
+ mi := &file_proto_v1_gateway_proto_msgTypes[55]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4498,7 +5162,7 @@ func (x *ProviderListRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ProviderListRequest.ProtoReflect.Descriptor instead.
func (*ProviderListRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{50}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{55}
}
type ProviderListResponse struct {
@@ -4510,7 +5174,7 @@ type ProviderListResponse struct {
func (x *ProviderListResponse) Reset() {
*x = ProviderListResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[51]
+ mi := &file_proto_v1_gateway_proto_msgTypes[56]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4522,7 +5186,7 @@ func (x *ProviderListResponse) String() string {
func (*ProviderListResponse) ProtoMessage() {}
func (x *ProviderListResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[51]
+ mi := &file_proto_v1_gateway_proto_msgTypes[56]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4535,7 +5199,7 @@ func (x *ProviderListResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ProviderListResponse.ProtoReflect.Descriptor instead.
func (*ProviderListResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{51}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{56}
}
func (x *ProviderListResponse) GetProvidersJson() string {
@@ -4553,7 +5217,7 @@ type SettingsGetRequest struct {
func (x *SettingsGetRequest) Reset() {
*x = SettingsGetRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[52]
+ mi := &file_proto_v1_gateway_proto_msgTypes[57]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4565,7 +5229,7 @@ func (x *SettingsGetRequest) String() string {
func (*SettingsGetRequest) ProtoMessage() {}
func (x *SettingsGetRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[52]
+ mi := &file_proto_v1_gateway_proto_msgTypes[57]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4578,7 +5242,7 @@ func (x *SettingsGetRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use SettingsGetRequest.ProtoReflect.Descriptor instead.
func (*SettingsGetRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{52}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{57}
}
type SettingsGetResponse struct {
@@ -4590,7 +5254,7 @@ type SettingsGetResponse struct {
func (x *SettingsGetResponse) Reset() {
*x = SettingsGetResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[53]
+ mi := &file_proto_v1_gateway_proto_msgTypes[58]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4602,7 +5266,7 @@ func (x *SettingsGetResponse) String() string {
func (*SettingsGetResponse) ProtoMessage() {}
func (x *SettingsGetResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[53]
+ mi := &file_proto_v1_gateway_proto_msgTypes[58]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4615,7 +5279,7 @@ func (x *SettingsGetResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use SettingsGetResponse.ProtoReflect.Descriptor instead.
func (*SettingsGetResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{53}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{58}
}
func (x *SettingsGetResponse) GetSettingsJson() string {
@@ -4634,7 +5298,7 @@ type SettingsUpdateRequest struct {
func (x *SettingsUpdateRequest) Reset() {
*x = SettingsUpdateRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[54]
+ mi := &file_proto_v1_gateway_proto_msgTypes[59]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4646,7 +5310,7 @@ func (x *SettingsUpdateRequest) String() string {
func (*SettingsUpdateRequest) ProtoMessage() {}
func (x *SettingsUpdateRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[54]
+ mi := &file_proto_v1_gateway_proto_msgTypes[59]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4659,7 +5323,7 @@ func (x *SettingsUpdateRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use SettingsUpdateRequest.ProtoReflect.Descriptor instead.
func (*SettingsUpdateRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{54}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{59}
}
func (x *SettingsUpdateRequest) GetSettingsJson() string {
@@ -4679,7 +5343,7 @@ type SettingsUpdateResponse struct {
func (x *SettingsUpdateResponse) Reset() {
*x = SettingsUpdateResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[55]
+ mi := &file_proto_v1_gateway_proto_msgTypes[60]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4691,7 +5355,7 @@ func (x *SettingsUpdateResponse) String() string {
func (*SettingsUpdateResponse) ProtoMessage() {}
func (x *SettingsUpdateResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[55]
+ mi := &file_proto_v1_gateway_proto_msgTypes[60]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4704,7 +5368,7 @@ func (x *SettingsUpdateResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use SettingsUpdateResponse.ProtoReflect.Descriptor instead.
func (*SettingsUpdateResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{55}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{60}
}
func (x *SettingsUpdateResponse) GetAccepted() bool {
@@ -4730,7 +5394,7 @@ type SettingsSyncEvent struct {
func (x *SettingsSyncEvent) Reset() {
*x = SettingsSyncEvent{}
- mi := &file_proto_v1_gateway_proto_msgTypes[56]
+ mi := &file_proto_v1_gateway_proto_msgTypes[61]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4742,7 +5406,7 @@ func (x *SettingsSyncEvent) String() string {
func (*SettingsSyncEvent) ProtoMessage() {}
func (x *SettingsSyncEvent) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[56]
+ mi := &file_proto_v1_gateway_proto_msgTypes[61]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4755,7 +5419,7 @@ func (x *SettingsSyncEvent) ProtoReflect() protoreflect.Message {
// Deprecated: Use SettingsSyncEvent.ProtoReflect.Descriptor instead.
func (*SettingsSyncEvent) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{56}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{61}
}
func (x *SettingsSyncEvent) GetSettingsJson() string {
@@ -4773,7 +5437,7 @@ type SkillFilesListRequest struct {
func (x *SkillFilesListRequest) Reset() {
*x = SkillFilesListRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[57]
+ mi := &file_proto_v1_gateway_proto_msgTypes[62]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4785,7 +5449,7 @@ func (x *SkillFilesListRequest) String() string {
func (*SkillFilesListRequest) ProtoMessage() {}
func (x *SkillFilesListRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[57]
+ mi := &file_proto_v1_gateway_proto_msgTypes[62]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4798,7 +5462,7 @@ func (x *SkillFilesListRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use SkillFilesListRequest.ProtoReflect.Descriptor instead.
func (*SkillFilesListRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{57}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{62}
}
type SkillFilesListResponse struct {
@@ -4812,7 +5476,7 @@ type SkillFilesListResponse struct {
func (x *SkillFilesListResponse) Reset() {
*x = SkillFilesListResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[58]
+ mi := &file_proto_v1_gateway_proto_msgTypes[63]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4824,7 +5488,7 @@ func (x *SkillFilesListResponse) String() string {
func (*SkillFilesListResponse) ProtoMessage() {}
func (x *SkillFilesListResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[58]
+ mi := &file_proto_v1_gateway_proto_msgTypes[63]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4837,7 +5501,7 @@ func (x *SkillFilesListResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use SkillFilesListResponse.ProtoReflect.Descriptor instead.
func (*SkillFilesListResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{58}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{63}
}
func (x *SkillFilesListResponse) GetRootDir() string {
@@ -4870,7 +5534,7 @@ type SkillMetadataReadRequest struct {
func (x *SkillMetadataReadRequest) Reset() {
*x = SkillMetadataReadRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[59]
+ mi := &file_proto_v1_gateway_proto_msgTypes[64]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4882,7 +5546,7 @@ func (x *SkillMetadataReadRequest) String() string {
func (*SkillMetadataReadRequest) ProtoMessage() {}
func (x *SkillMetadataReadRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[59]
+ mi := &file_proto_v1_gateway_proto_msgTypes[64]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4895,7 +5559,7 @@ func (x *SkillMetadataReadRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use SkillMetadataReadRequest.ProtoReflect.Descriptor instead.
func (*SkillMetadataReadRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{59}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{64}
}
func (x *SkillMetadataReadRequest) GetPath() string {
@@ -4915,7 +5579,7 @@ type SkillMetadataReadResponse struct {
func (x *SkillMetadataReadResponse) Reset() {
*x = SkillMetadataReadResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[60]
+ mi := &file_proto_v1_gateway_proto_msgTypes[65]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4927,7 +5591,7 @@ func (x *SkillMetadataReadResponse) String() string {
func (*SkillMetadataReadResponse) ProtoMessage() {}
func (x *SkillMetadataReadResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[60]
+ mi := &file_proto_v1_gateway_proto_msgTypes[65]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4940,7 +5604,7 @@ func (x *SkillMetadataReadResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use SkillMetadataReadResponse.ProtoReflect.Descriptor instead.
func (*SkillMetadataReadResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{60}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{65}
}
func (x *SkillMetadataReadResponse) GetName() string {
@@ -4968,7 +5632,7 @@ type SkillTextReadRequest struct {
func (x *SkillTextReadRequest) Reset() {
*x = SkillTextReadRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[61]
+ mi := &file_proto_v1_gateway_proto_msgTypes[66]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4980,7 +5644,7 @@ func (x *SkillTextReadRequest) String() string {
func (*SkillTextReadRequest) ProtoMessage() {}
func (x *SkillTextReadRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[61]
+ mi := &file_proto_v1_gateway_proto_msgTypes[66]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4993,7 +5657,7 @@ func (x *SkillTextReadRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use SkillTextReadRequest.ProtoReflect.Descriptor instead.
func (*SkillTextReadRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{61}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{66}
}
func (x *SkillTextReadRequest) GetPath() string {
@@ -5027,7 +5691,7 @@ type SkillTextReadResponse struct {
func (x *SkillTextReadResponse) Reset() {
*x = SkillTextReadResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[62]
+ mi := &file_proto_v1_gateway_proto_msgTypes[67]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5039,7 +5703,7 @@ func (x *SkillTextReadResponse) String() string {
func (*SkillTextReadResponse) ProtoMessage() {}
func (x *SkillTextReadResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[62]
+ mi := &file_proto_v1_gateway_proto_msgTypes[67]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5052,7 +5716,7 @@ func (x *SkillTextReadResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use SkillTextReadResponse.ProtoReflect.Descriptor instead.
func (*SkillTextReadResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{62}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{67}
}
func (x *SkillTextReadResponse) GetContent() string {
@@ -5078,7 +5742,7 @@ type SkillManageRequest struct {
func (x *SkillManageRequest) Reset() {
*x = SkillManageRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[63]
+ mi := &file_proto_v1_gateway_proto_msgTypes[68]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5090,7 +5754,7 @@ func (x *SkillManageRequest) String() string {
func (*SkillManageRequest) ProtoMessage() {}
func (x *SkillManageRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[63]
+ mi := &file_proto_v1_gateway_proto_msgTypes[68]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5103,7 +5767,7 @@ func (x *SkillManageRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use SkillManageRequest.ProtoReflect.Descriptor instead.
func (*SkillManageRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{63}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{68}
}
func (x *SkillManageRequest) GetPayloadJson() string {
@@ -5122,7 +5786,7 @@ type SkillManageResponse struct {
func (x *SkillManageResponse) Reset() {
*x = SkillManageResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[64]
+ mi := &file_proto_v1_gateway_proto_msgTypes[69]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5134,7 +5798,7 @@ func (x *SkillManageResponse) String() string {
func (*SkillManageResponse) ProtoMessage() {}
func (x *SkillManageResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[64]
+ mi := &file_proto_v1_gateway_proto_msgTypes[69]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5147,7 +5811,7 @@ func (x *SkillManageResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use SkillManageResponse.ProtoReflect.Descriptor instead.
func (*SkillManageResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{64}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{69}
}
func (x *SkillManageResponse) GetResultJson() string {
@@ -5168,7 +5832,7 @@ type FileMentionListRequest struct {
func (x *FileMentionListRequest) Reset() {
*x = FileMentionListRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[65]
+ mi := &file_proto_v1_gateway_proto_msgTypes[70]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5180,7 +5844,7 @@ func (x *FileMentionListRequest) String() string {
func (*FileMentionListRequest) ProtoMessage() {}
func (x *FileMentionListRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[65]
+ mi := &file_proto_v1_gateway_proto_msgTypes[70]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5193,7 +5857,7 @@ func (x *FileMentionListRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use FileMentionListRequest.ProtoReflect.Descriptor instead.
func (*FileMentionListRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{65}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{70}
}
func (x *FileMentionListRequest) GetWorkdir() string {
@@ -5227,7 +5891,7 @@ type FileMentionEntry struct {
func (x *FileMentionEntry) Reset() {
*x = FileMentionEntry{}
- mi := &file_proto_v1_gateway_proto_msgTypes[66]
+ mi := &file_proto_v1_gateway_proto_msgTypes[71]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5239,7 +5903,7 @@ func (x *FileMentionEntry) String() string {
func (*FileMentionEntry) ProtoMessage() {}
func (x *FileMentionEntry) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[66]
+ mi := &file_proto_v1_gateway_proto_msgTypes[71]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5252,7 +5916,7 @@ func (x *FileMentionEntry) ProtoReflect() protoreflect.Message {
// Deprecated: Use FileMentionEntry.ProtoReflect.Descriptor instead.
func (*FileMentionEntry) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{66}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{71}
}
func (x *FileMentionEntry) GetPath() string {
@@ -5279,7 +5943,7 @@ type FileMentionListResponse struct {
func (x *FileMentionListResponse) Reset() {
*x = FileMentionListResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[67]
+ mi := &file_proto_v1_gateway_proto_msgTypes[72]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5291,7 +5955,7 @@ func (x *FileMentionListResponse) String() string {
func (*FileMentionListResponse) ProtoMessage() {}
func (x *FileMentionListResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[67]
+ mi := &file_proto_v1_gateway_proto_msgTypes[72]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5304,7 +5968,7 @@ func (x *FileMentionListResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use FileMentionListResponse.ProtoReflect.Descriptor instead.
func (*FileMentionListResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{67}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{72}
}
func (x *FileMentionListResponse) GetEntries() []*FileMentionEntry {
@@ -5333,7 +5997,7 @@ type FsRoot struct {
func (x *FsRoot) Reset() {
*x = FsRoot{}
- mi := &file_proto_v1_gateway_proto_msgTypes[68]
+ mi := &file_proto_v1_gateway_proto_msgTypes[73]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5345,7 +6009,7 @@ func (x *FsRoot) String() string {
func (*FsRoot) ProtoMessage() {}
func (x *FsRoot) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[68]
+ mi := &file_proto_v1_gateway_proto_msgTypes[73]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5358,7 +6022,7 @@ func (x *FsRoot) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsRoot.ProtoReflect.Descriptor instead.
func (*FsRoot) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{68}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{73}
}
func (x *FsRoot) GetId() string {
@@ -5397,7 +6061,7 @@ type FsRootsRequest struct {
func (x *FsRootsRequest) Reset() {
*x = FsRootsRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[69]
+ mi := &file_proto_v1_gateway_proto_msgTypes[74]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5409,7 +6073,7 @@ func (x *FsRootsRequest) String() string {
func (*FsRootsRequest) ProtoMessage() {}
func (x *FsRootsRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[69]
+ mi := &file_proto_v1_gateway_proto_msgTypes[74]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5422,7 +6086,7 @@ func (x *FsRootsRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsRootsRequest.ProtoReflect.Descriptor instead.
func (*FsRootsRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{69}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{74}
}
type FsRootsResponse struct {
@@ -5434,7 +6098,7 @@ type FsRootsResponse struct {
func (x *FsRootsResponse) Reset() {
*x = FsRootsResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[70]
+ mi := &file_proto_v1_gateway_proto_msgTypes[75]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5446,7 +6110,7 @@ func (x *FsRootsResponse) String() string {
func (*FsRootsResponse) ProtoMessage() {}
func (x *FsRootsResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[70]
+ mi := &file_proto_v1_gateway_proto_msgTypes[75]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5459,7 +6123,7 @@ func (x *FsRootsResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsRootsResponse.ProtoReflect.Descriptor instead.
func (*FsRootsResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{70}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{75}
}
func (x *FsRootsResponse) GetRoots() []*FsRoot {
@@ -5479,7 +6143,7 @@ type FsListDirsRequest struct {
func (x *FsListDirsRequest) Reset() {
*x = FsListDirsRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[71]
+ mi := &file_proto_v1_gateway_proto_msgTypes[76]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5491,7 +6155,7 @@ func (x *FsListDirsRequest) String() string {
func (*FsListDirsRequest) ProtoMessage() {}
func (x *FsListDirsRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[71]
+ mi := &file_proto_v1_gateway_proto_msgTypes[76]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5504,7 +6168,7 @@ func (x *FsListDirsRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsListDirsRequest.ProtoReflect.Descriptor instead.
func (*FsListDirsRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{71}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{76}
}
func (x *FsListDirsRequest) GetPath() string {
@@ -5531,7 +6195,7 @@ type FsDirEntry struct {
func (x *FsDirEntry) Reset() {
*x = FsDirEntry{}
- mi := &file_proto_v1_gateway_proto_msgTypes[72]
+ mi := &file_proto_v1_gateway_proto_msgTypes[77]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5543,7 +6207,7 @@ func (x *FsDirEntry) String() string {
func (*FsDirEntry) ProtoMessage() {}
func (x *FsDirEntry) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[72]
+ mi := &file_proto_v1_gateway_proto_msgTypes[77]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5556,7 +6220,7 @@ func (x *FsDirEntry) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsDirEntry.ProtoReflect.Descriptor instead.
func (*FsDirEntry) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{72}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{77}
}
func (x *FsDirEntry) GetPath() string {
@@ -5584,7 +6248,7 @@ type FsListDirsResponse struct {
func (x *FsListDirsResponse) Reset() {
*x = FsListDirsResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[73]
+ mi := &file_proto_v1_gateway_proto_msgTypes[78]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5596,7 +6260,7 @@ func (x *FsListDirsResponse) String() string {
func (*FsListDirsResponse) ProtoMessage() {}
func (x *FsListDirsResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[73]
+ mi := &file_proto_v1_gateway_proto_msgTypes[78]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5609,7 +6273,7 @@ func (x *FsListDirsResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsListDirsResponse.ProtoReflect.Descriptor instead.
func (*FsListDirsResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{73}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{78}
}
func (x *FsListDirsResponse) GetPath() string {
@@ -5643,7 +6307,7 @@ type FsCreateProjectFolderRequest struct {
func (x *FsCreateProjectFolderRequest) Reset() {
*x = FsCreateProjectFolderRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[74]
+ mi := &file_proto_v1_gateway_proto_msgTypes[79]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5655,7 +6319,7 @@ func (x *FsCreateProjectFolderRequest) String() string {
func (*FsCreateProjectFolderRequest) ProtoMessage() {}
func (x *FsCreateProjectFolderRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[74]
+ mi := &file_proto_v1_gateway_proto_msgTypes[79]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5668,7 +6332,7 @@ func (x *FsCreateProjectFolderRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsCreateProjectFolderRequest.ProtoReflect.Descriptor instead.
func (*FsCreateProjectFolderRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{74}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{79}
}
func (x *FsCreateProjectFolderRequest) GetParent() string {
@@ -5694,7 +6358,7 @@ type FsCreateProjectFolderResponse struct {
func (x *FsCreateProjectFolderResponse) Reset() {
*x = FsCreateProjectFolderResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[75]
+ mi := &file_proto_v1_gateway_proto_msgTypes[80]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5706,7 +6370,7 @@ func (x *FsCreateProjectFolderResponse) String() string {
func (*FsCreateProjectFolderResponse) ProtoMessage() {}
func (x *FsCreateProjectFolderResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[75]
+ mi := &file_proto_v1_gateway_proto_msgTypes[80]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5719,7 +6383,7 @@ func (x *FsCreateProjectFolderResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsCreateProjectFolderResponse.ProtoReflect.Descriptor instead.
func (*FsCreateProjectFolderResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{75}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{80}
}
func (x *FsCreateProjectFolderResponse) GetPath() string {
@@ -5742,7 +6406,7 @@ type FsListRequest struct {
func (x *FsListRequest) Reset() {
*x = FsListRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[76]
+ mi := &file_proto_v1_gateway_proto_msgTypes[81]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5754,7 +6418,7 @@ func (x *FsListRequest) String() string {
func (*FsListRequest) ProtoMessage() {}
func (x *FsListRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[76]
+ mi := &file_proto_v1_gateway_proto_msgTypes[81]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5767,7 +6431,7 @@ func (x *FsListRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsListRequest.ProtoReflect.Descriptor instead.
func (*FsListRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{76}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{81}
}
func (x *FsListRequest) GetWorkdir() string {
@@ -5815,7 +6479,7 @@ type FsListEntry struct {
func (x *FsListEntry) Reset() {
*x = FsListEntry{}
- mi := &file_proto_v1_gateway_proto_msgTypes[77]
+ mi := &file_proto_v1_gateway_proto_msgTypes[82]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5827,7 +6491,7 @@ func (x *FsListEntry) String() string {
func (*FsListEntry) ProtoMessage() {}
func (x *FsListEntry) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[77]
+ mi := &file_proto_v1_gateway_proto_msgTypes[82]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5840,7 +6504,7 @@ func (x *FsListEntry) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsListEntry.ProtoReflect.Descriptor instead.
func (*FsListEntry) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{77}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{82}
}
func (x *FsListEntry) GetPath() string {
@@ -5873,7 +6537,7 @@ type FsListResponse struct {
func (x *FsListResponse) Reset() {
*x = FsListResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[78]
+ mi := &file_proto_v1_gateway_proto_msgTypes[83]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5885,7 +6549,7 @@ func (x *FsListResponse) String() string {
func (*FsListResponse) ProtoMessage() {}
func (x *FsListResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[78]
+ mi := &file_proto_v1_gateway_proto_msgTypes[83]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5898,7 +6562,7 @@ func (x *FsListResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsListResponse.ProtoReflect.Descriptor instead.
func (*FsListResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{78}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{83}
}
func (x *FsListResponse) GetPath() string {
@@ -5967,7 +6631,7 @@ type FsReadEditableTextRequest struct {
func (x *FsReadEditableTextRequest) Reset() {
*x = FsReadEditableTextRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[79]
+ mi := &file_proto_v1_gateway_proto_msgTypes[84]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5979,7 +6643,7 @@ func (x *FsReadEditableTextRequest) String() string {
func (*FsReadEditableTextRequest) ProtoMessage() {}
func (x *FsReadEditableTextRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[79]
+ mi := &file_proto_v1_gateway_proto_msgTypes[84]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5992,7 +6656,7 @@ func (x *FsReadEditableTextRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsReadEditableTextRequest.ProtoReflect.Descriptor instead.
func (*FsReadEditableTextRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{79}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{84}
}
func (x *FsReadEditableTextRequest) GetWorkdir() string {
@@ -6023,7 +6687,7 @@ type FsReadEditableTextResponse struct {
func (x *FsReadEditableTextResponse) Reset() {
*x = FsReadEditableTextResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[80]
+ mi := &file_proto_v1_gateway_proto_msgTypes[85]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6035,7 +6699,7 @@ func (x *FsReadEditableTextResponse) String() string {
func (*FsReadEditableTextResponse) ProtoMessage() {}
func (x *FsReadEditableTextResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[80]
+ mi := &file_proto_v1_gateway_proto_msgTypes[85]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6048,7 +6712,7 @@ func (x *FsReadEditableTextResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsReadEditableTextResponse.ProtoReflect.Descriptor instead.
func (*FsReadEditableTextResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{80}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{85}
}
func (x *FsReadEditableTextResponse) GetPath() string {
@@ -6103,7 +6767,7 @@ type FsReadWorkspaceImageRequest struct {
func (x *FsReadWorkspaceImageRequest) Reset() {
*x = FsReadWorkspaceImageRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[81]
+ mi := &file_proto_v1_gateway_proto_msgTypes[86]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6115,7 +6779,7 @@ func (x *FsReadWorkspaceImageRequest) String() string {
func (*FsReadWorkspaceImageRequest) ProtoMessage() {}
func (x *FsReadWorkspaceImageRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[81]
+ mi := &file_proto_v1_gateway_proto_msgTypes[86]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6128,7 +6792,7 @@ func (x *FsReadWorkspaceImageRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsReadWorkspaceImageRequest.ProtoReflect.Descriptor instead.
func (*FsReadWorkspaceImageRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{81}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{86}
}
func (x *FsReadWorkspaceImageRequest) GetWorkdir() string {
@@ -6159,7 +6823,7 @@ type FsReadWorkspaceImageResponse struct {
func (x *FsReadWorkspaceImageResponse) Reset() {
*x = FsReadWorkspaceImageResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[82]
+ mi := &file_proto_v1_gateway_proto_msgTypes[87]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6171,7 +6835,7 @@ func (x *FsReadWorkspaceImageResponse) String() string {
func (*FsReadWorkspaceImageResponse) ProtoMessage() {}
func (x *FsReadWorkspaceImageResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[82]
+ mi := &file_proto_v1_gateway_proto_msgTypes[87]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6184,7 +6848,7 @@ func (x *FsReadWorkspaceImageResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsReadWorkspaceImageResponse.ProtoReflect.Descriptor instead.
func (*FsReadWorkspaceImageResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{82}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{87}
}
func (x *FsReadWorkspaceImageResponse) GetPath() string {
@@ -6245,7 +6909,7 @@ type FsWriteTextRequest struct {
func (x *FsWriteTextRequest) Reset() {
*x = FsWriteTextRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[83]
+ mi := &file_proto_v1_gateway_proto_msgTypes[88]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6257,7 +6921,7 @@ func (x *FsWriteTextRequest) String() string {
func (*FsWriteTextRequest) ProtoMessage() {}
func (x *FsWriteTextRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[83]
+ mi := &file_proto_v1_gateway_proto_msgTypes[88]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6270,7 +6934,7 @@ func (x *FsWriteTextRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsWriteTextRequest.ProtoReflect.Descriptor instead.
func (*FsWriteTextRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{83}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{88}
}
func (x *FsWriteTextRequest) GetWorkdir() string {
@@ -6344,7 +7008,7 @@ type FsWriteTextResponse struct {
func (x *FsWriteTextResponse) Reset() {
*x = FsWriteTextResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[84]
+ mi := &file_proto_v1_gateway_proto_msgTypes[89]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6356,7 +7020,7 @@ func (x *FsWriteTextResponse) String() string {
func (*FsWriteTextResponse) ProtoMessage() {}
func (x *FsWriteTextResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[84]
+ mi := &file_proto_v1_gateway_proto_msgTypes[89]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6369,7 +7033,7 @@ func (x *FsWriteTextResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsWriteTextResponse.ProtoReflect.Descriptor instead.
func (*FsWriteTextResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{84}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{89}
}
func (x *FsWriteTextResponse) GetPath() string {
@@ -6431,7 +7095,7 @@ type FsCreateDirRequest struct {
func (x *FsCreateDirRequest) Reset() {
*x = FsCreateDirRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[85]
+ mi := &file_proto_v1_gateway_proto_msgTypes[90]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6443,7 +7107,7 @@ func (x *FsCreateDirRequest) String() string {
func (*FsCreateDirRequest) ProtoMessage() {}
func (x *FsCreateDirRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[85]
+ mi := &file_proto_v1_gateway_proto_msgTypes[90]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6456,7 +7120,7 @@ func (x *FsCreateDirRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsCreateDirRequest.ProtoReflect.Descriptor instead.
func (*FsCreateDirRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{85}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{90}
}
func (x *FsCreateDirRequest) GetWorkdir() string {
@@ -6483,7 +7147,7 @@ type FsCreateDirResponse struct {
func (x *FsCreateDirResponse) Reset() {
*x = FsCreateDirResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[86]
+ mi := &file_proto_v1_gateway_proto_msgTypes[91]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6495,7 +7159,7 @@ func (x *FsCreateDirResponse) String() string {
func (*FsCreateDirResponse) ProtoMessage() {}
func (x *FsCreateDirResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[86]
+ mi := &file_proto_v1_gateway_proto_msgTypes[91]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6508,7 +7172,7 @@ func (x *FsCreateDirResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsCreateDirResponse.ProtoReflect.Descriptor instead.
func (*FsCreateDirResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{86}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{91}
}
func (x *FsCreateDirResponse) GetPath() string {
@@ -6536,7 +7200,7 @@ type FsRenameRequest struct {
func (x *FsRenameRequest) Reset() {
*x = FsRenameRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[87]
+ mi := &file_proto_v1_gateway_proto_msgTypes[92]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6548,7 +7212,7 @@ func (x *FsRenameRequest) String() string {
func (*FsRenameRequest) ProtoMessage() {}
func (x *FsRenameRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[87]
+ mi := &file_proto_v1_gateway_proto_msgTypes[92]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6561,7 +7225,7 @@ func (x *FsRenameRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsRenameRequest.ProtoReflect.Descriptor instead.
func (*FsRenameRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{87}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{92}
}
func (x *FsRenameRequest) GetWorkdir() string {
@@ -6596,7 +7260,7 @@ type FsRenameResponse struct {
func (x *FsRenameResponse) Reset() {
*x = FsRenameResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[88]
+ mi := &file_proto_v1_gateway_proto_msgTypes[93]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6608,7 +7272,7 @@ func (x *FsRenameResponse) String() string {
func (*FsRenameResponse) ProtoMessage() {}
func (x *FsRenameResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[88]
+ mi := &file_proto_v1_gateway_proto_msgTypes[93]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6621,7 +7285,7 @@ func (x *FsRenameResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsRenameResponse.ProtoReflect.Descriptor instead.
func (*FsRenameResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{88}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{93}
}
func (x *FsRenameResponse) GetFromPath() string {
@@ -6655,7 +7319,7 @@ type FsDeleteRequest struct {
func (x *FsDeleteRequest) Reset() {
*x = FsDeleteRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[89]
+ mi := &file_proto_v1_gateway_proto_msgTypes[94]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6667,7 +7331,7 @@ func (x *FsDeleteRequest) String() string {
func (*FsDeleteRequest) ProtoMessage() {}
func (x *FsDeleteRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[89]
+ mi := &file_proto_v1_gateway_proto_msgTypes[94]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6680,7 +7344,7 @@ func (x *FsDeleteRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsDeleteRequest.ProtoReflect.Descriptor instead.
func (*FsDeleteRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{89}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{94}
}
func (x *FsDeleteRequest) GetWorkdir() string {
@@ -6707,7 +7371,7 @@ type FsDeleteResponse struct {
func (x *FsDeleteResponse) Reset() {
*x = FsDeleteResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[90]
+ mi := &file_proto_v1_gateway_proto_msgTypes[95]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6719,7 +7383,7 @@ func (x *FsDeleteResponse) String() string {
func (*FsDeleteResponse) ProtoMessage() {}
func (x *FsDeleteResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[90]
+ mi := &file_proto_v1_gateway_proto_msgTypes[95]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6732,7 +7396,7 @@ func (x *FsDeleteResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsDeleteResponse.ProtoReflect.Descriptor instead.
func (*FsDeleteResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{90}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{95}
}
func (x *FsDeleteResponse) GetPath() string {
@@ -6758,7 +7422,7 @@ type PingRequest struct {
func (x *PingRequest) Reset() {
*x = PingRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[91]
+ mi := &file_proto_v1_gateway_proto_msgTypes[96]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6770,7 +7434,7 @@ func (x *PingRequest) String() string {
func (*PingRequest) ProtoMessage() {}
func (x *PingRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[91]
+ mi := &file_proto_v1_gateway_proto_msgTypes[96]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6783,7 +7447,7 @@ func (x *PingRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use PingRequest.ProtoReflect.Descriptor instead.
func (*PingRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{91}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{96}
}
func (x *PingRequest) GetTimestamp() int64 {
@@ -6802,7 +7466,7 @@ type PongResponse struct {
func (x *PongResponse) Reset() {
*x = PongResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[92]
+ mi := &file_proto_v1_gateway_proto_msgTypes[97]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6814,7 +7478,7 @@ func (x *PongResponse) String() string {
func (*PongResponse) ProtoMessage() {}
func (x *PongResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[92]
+ mi := &file_proto_v1_gateway_proto_msgTypes[97]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6827,7 +7491,7 @@ func (x *PongResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use PongResponse.ProtoReflect.Descriptor instead.
func (*PongResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{92}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{97}
}
func (x *PongResponse) GetTimestamp() int64 {
@@ -6847,7 +7511,7 @@ type ErrorResponse struct {
func (x *ErrorResponse) Reset() {
*x = ErrorResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[93]
+ mi := &file_proto_v1_gateway_proto_msgTypes[98]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6859,7 +7523,7 @@ func (x *ErrorResponse) String() string {
func (*ErrorResponse) ProtoMessage() {}
func (x *ErrorResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[93]
+ mi := &file_proto_v1_gateway_proto_msgTypes[98]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6872,7 +7536,7 @@ func (x *ErrorResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ErrorResponse.ProtoReflect.Descriptor instead.
func (*ErrorResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{93}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{98}
}
func (x *ErrorResponse) GetCode() int32 {
@@ -6902,7 +7566,7 @@ const file_proto_v1_gateway_proto_rawDesc = "" +
"\asuccess\x18\x01 \x01(\bR\asuccess\x12\x18\n" +
"\amessage\x18\x02 \x01(\tR\amessage\x12\x1d\n" +
"\n" +
- "session_id\x18\x03 \x01(\tR\tsessionId\"\x89\x19\n" +
+ "session_id\x18\x03 \x01(\tR\tsessionId\"\x85\x1b\n" +
"\x0fGatewayEnvelope\x12\x1d\n" +
"\n" +
"request_id\x18\x01 \x01(\tR\trequestId\x12\x1c\n" +
@@ -6950,8 +7614,11 @@ const file_proto_v1_gateway_proto_rawDesc = "" +
"\vgit_request\x18= \x01(\v2 .liveagent.gateway.v1.GitRequestH\x00R\n" +
"gitRequest\x12d\n" +
"\x15fs_read_editable_text\x18> \x01(\v2/.liveagent.gateway.v1.FsReadEditableTextRequestH\x00R\x12fsReadEditableText\x12j\n" +
- "\x17fs_read_workspace_image\x18? \x01(\v21.liveagent.gateway.v1.FsReadWorkspaceImageRequestH\x00R\x14fsReadWorkspaceImageB\t\n" +
- "\apayload\"\xa4\x1d\n" +
+ "\x17fs_read_workspace_image\x18? \x01(\v21.liveagent.gateway.v1.FsReadWorkspaceImageRequestH\x00R\x14fsReadWorkspaceImage\x12S\n" +
+ "\x0etunnel_control\x18C \x01(\v2*.liveagent.gateway.v1.TunnelControlRequestH\x00R\rtunnelControl\x12]\n" +
+ "\x13tunnel_control_resp\x18D \x01(\v2+.liveagent.gateway.v1.TunnelControlResponseH\x00R\x11tunnelControlResp\x12F\n" +
+ "\ftunnel_frame\x18E \x01(\v2!.liveagent.gateway.v1.TunnelFrameH\x00R\vtunnelFrameB\t\n" +
+ "\apayload\"\xa0\x1f\n" +
"\rAgentEnvelope\x12\x1d\n" +
"\n" +
"request_id\x18\x01 \x01(\tR\trequestId\x12\x1c\n" +
@@ -6997,7 +7664,10 @@ const file_proto_v1_gateway_proto_rawDesc = "" +
"\x0efs_delete_resp\x18? \x01(\v2&.liveagent.gateway.v1.FsDeleteResponseH\x00R\ffsDeleteResp\x12F\n" +
"\fgit_response\x18@ \x01(\v2!.liveagent.gateway.v1.GitResponseH\x00R\vgitResponse\x12n\n" +
"\x1afs_read_editable_text_resp\x18A \x01(\v20.liveagent.gateway.v1.FsReadEditableTextResponseH\x00R\x16fsReadEditableTextResp\x12t\n" +
- "\x1cfs_read_workspace_image_resp\x18B \x01(\v22.liveagent.gateway.v1.FsReadWorkspaceImageResponseH\x00R\x18fsReadWorkspaceImageResp\x12;\n" +
+ "\x1cfs_read_workspace_image_resp\x18B \x01(\v22.liveagent.gateway.v1.FsReadWorkspaceImageResponseH\x00R\x18fsReadWorkspaceImageResp\x12S\n" +
+ "\x0etunnel_control\x18C \x01(\v2*.liveagent.gateway.v1.TunnelControlRequestH\x00R\rtunnelControl\x12]\n" +
+ "\x13tunnel_control_resp\x18D \x01(\v2+.liveagent.gateway.v1.TunnelControlResponseH\x00R\x11tunnelControlResp\x12F\n" +
+ "\ftunnel_frame\x18E \x01(\v2!.liveagent.gateway.v1.TunnelFrameH\x00R\vtunnelFrame\x12;\n" +
"\x05error\x18c \x01(\v2#.liveagent.gateway.v1.ErrorResponseH\x00R\x05errorB\t\n" +
"\apayload\"|\n" +
"\x11ChatSelectedModel\x12,\n" +
@@ -7030,7 +7700,65 @@ const file_proto_v1_gateway_proto_rawDesc = "" +
"\rabsolute_path\x18\x02 \x01(\tR\fabsolutePath\"O\n" +
"\x1cUploadedImagePreviewResponse\x12\x1b\n" +
"\tmime_type\x18\x01 \x01(\tR\bmimeType\x12\x12\n" +
- "\x04data\x18\x02 \x01(\tR\x04data\"L\n" +
+ "\x04data\x18\x02 \x01(\tR\x04data\"\xc3\x02\n" +
+ "\x14TunnelControlRequest\x12\x16\n" +
+ "\x06action\x18\x01 \x01(\tR\x06action\x12\x1b\n" +
+ "\ttunnel_id\x18\x02 \x01(\tR\btunnelId\x12\x12\n" +
+ "\x04slug\x18\x03 \x01(\tR\x04slug\x12\x1d\n" +
+ "\n" +
+ "target_url\x18\x04 \x01(\tR\ttargetUrl\x12\x12\n" +
+ "\x04name\x18\x05 \x01(\tR\x04name\x12\x1f\n" +
+ "\vttl_seconds\x18\x06 \x01(\rR\n" +
+ "ttlSeconds\x12\x1d\n" +
+ "\n" +
+ "expires_at\x18\a \x01(\x03R\texpiresAt\x12\x1d\n" +
+ "\n" +
+ "public_url\x18\b \x01(\tR\tpublicUrl\x12&\n" +
+ "\x0fpublic_base_url\x18\t \x01(\tR\rpublicBaseUrl\x12(\n" +
+ "\x10project_path_key\x18\n" +
+ " \x01(\tR\x0eprojectPathKey\"\xef\x01\n" +
+ "\x15TunnelControlResponse\x12\x16\n" +
+ "\x06action\x18\x01 \x01(\tR\x06action\x12=\n" +
+ "\atunnels\x18\x02 \x03(\v2#.liveagent.gateway.v1.TunnelSummaryR\atunnels\x12;\n" +
+ "\x06tunnel\x18\x03 \x01(\v2#.liveagent.gateway.v1.TunnelSummaryR\x06tunnel\x12\x1d\n" +
+ "\n" +
+ "error_code\x18\x04 \x01(\tR\terrorCode\x12#\n" +
+ "\rerror_message\x18\x05 \x01(\tR\ferrorMessage\"\xb4\x02\n" +
+ "\rTunnelSummary\x12\x0e\n" +
+ "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" +
+ "\x04slug\x18\x02 \x01(\tR\x04slug\x12\x12\n" +
+ "\x04name\x18\x03 \x01(\tR\x04name\x12\x1d\n" +
+ "\n" +
+ "target_url\x18\x04 \x01(\tR\ttargetUrl\x12\x1d\n" +
+ "\n" +
+ "public_url\x18\x05 \x01(\tR\tpublicUrl\x12\x1d\n" +
+ "\n" +
+ "created_at\x18\x06 \x01(\x03R\tcreatedAt\x12\x1d\n" +
+ "\n" +
+ "expires_at\x18\a \x01(\x03R\texpiresAt\x12-\n" +
+ "\x12active_connections\x18\b \x01(\rR\x11activeConnections\x12\x16\n" +
+ "\x06status\x18\t \x01(\tR\x06status\x12(\n" +
+ "\x10project_path_key\x18\n" +
+ " \x01(\tR\x0eprojectPathKey\"8\n" +
+ "\fTunnelHeader\x12\x12\n" +
+ "\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" +
+ "\x05value\x18\x02 \x01(\tR\x05value\"\x92\x03\n" +
+ "\vTunnelFrame\x12\x1b\n" +
+ "\tstream_id\x18\x01 \x01(\tR\bstreamId\x12\x1b\n" +
+ "\ttunnel_id\x18\x02 \x01(\tR\btunnelId\x12\x12\n" +
+ "\x04slug\x18\x03 \x01(\tR\x04slug\x129\n" +
+ "\x04kind\x18\x04 \x01(\x0e2%.liveagent.gateway.v1.TunnelFrameKindR\x04kind\x12\x16\n" +
+ "\x06method\x18\x05 \x01(\tR\x06method\x12\x12\n" +
+ "\x04path\x18\x06 \x01(\tR\x04path\x12<\n" +
+ "\aheaders\x18\a \x03(\v2\".liveagent.gateway.v1.TunnelHeaderR\aheaders\x12\x1f\n" +
+ "\vstatus_code\x18\b \x01(\rR\n" +
+ "statusCode\x12\x12\n" +
+ "\x04body\x18\t \x01(\fR\x04body\x12\x1d\n" +
+ "\n" +
+ "end_stream\x18\n" +
+ " \x01(\bR\tendStream\x12\x14\n" +
+ "\x05error\x18\v \x01(\tR\x05error\x12&\n" +
+ "\x0fws_message_type\x18\f \x01(\tR\rwsMessageType\"L\n" +
"\x13MemoryManageRequest\x12\x18\n" +
"\acommand\x18\x01 \x01(\tR\acommand\x12\x1b\n" +
"\targs_json\x18\x02 \x01(\tR\bargsJson\"7\n" +
@@ -7390,7 +8118,21 @@ const file_proto_v1_gateway_proto_rawDesc = "" +
"\ttimestamp\x18\x01 \x01(\x03R\ttimestamp\"=\n" +
"\rErrorResponse\x12\x12\n" +
"\x04code\x18\x01 \x01(\x05R\x04code\x12\x18\n" +
- "\amessage\x18\x02 \x01(\tR\amessage2\xc5\x01\n" +
+ "\amessage\x18\x02 \x01(\tR\amessage*\xc7\x03\n" +
+ "\x0fTunnelFrameKind\x12!\n" +
+ "\x1dTUNNEL_FRAME_KIND_UNSPECIFIED\x10\x00\x12(\n" +
+ "$TUNNEL_FRAME_KIND_HTTP_REQUEST_START\x10\x01\x12'\n" +
+ "#TUNNEL_FRAME_KIND_HTTP_REQUEST_BODY\x10\x02\x12&\n" +
+ "\"TUNNEL_FRAME_KIND_HTTP_REQUEST_END\x10\x03\x12)\n" +
+ "%TUNNEL_FRAME_KIND_HTTP_RESPONSE_START\x10\x04\x12(\n" +
+ "$TUNNEL_FRAME_KIND_HTTP_RESPONSE_BODY\x10\x05\x12'\n" +
+ "#TUNNEL_FRAME_KIND_HTTP_RESPONSE_END\x10\x06\x12\x1d\n" +
+ "\x19TUNNEL_FRAME_KIND_WS_OPEN\x10\a\x12\x1e\n" +
+ "\x1aTUNNEL_FRAME_KIND_WS_FRAME\x10\b\x12\x1e\n" +
+ "\x1aTUNNEL_FRAME_KIND_WS_CLOSE\x10\t\x12\x1b\n" +
+ "\x17TUNNEL_FRAME_KIND_ERROR\x10\n" +
+ "\x12\x1c\n" +
+ "\x18TUNNEL_FRAME_KIND_CANCEL\x10\v2\xc5\x01\n" +
"\fAgentGateway\x12^\n" +
"\fAgentConnect\x12#.liveagent.gateway.v1.AgentEnvelope\x1a%.liveagent.gateway.v1.GatewayEnvelope(\x010\x01\x12U\n" +
"\fAuthenticate\x12!.liveagent.gateway.v1.AuthRequest\x1a\".liveagent.gateway.v1.AuthResponseB@Z>github.com/liveagent/agent-gateway/internal/proto/v1;gatewayv1b\x06proto3"
@@ -7407,216 +8149,232 @@ func file_proto_v1_gateway_proto_rawDescGZIP() []byte {
return file_proto_v1_gateway_proto_rawDescData
}
-var file_proto_v1_gateway_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
-var file_proto_v1_gateway_proto_msgTypes = make([]protoimpl.MessageInfo, 94)
+var file_proto_v1_gateway_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
+var file_proto_v1_gateway_proto_msgTypes = make([]protoimpl.MessageInfo, 99)
var file_proto_v1_gateway_proto_goTypes = []any{
- (ChatEvent_ChatEventType)(0), // 0: liveagent.gateway.v1.ChatEvent.ChatEventType
- (*AuthRequest)(nil), // 1: liveagent.gateway.v1.AuthRequest
- (*AuthResponse)(nil), // 2: liveagent.gateway.v1.AuthResponse
- (*GatewayEnvelope)(nil), // 3: liveagent.gateway.v1.GatewayEnvelope
- (*AgentEnvelope)(nil), // 4: liveagent.gateway.v1.AgentEnvelope
- (*ChatSelectedModel)(nil), // 5: liveagent.gateway.v1.ChatSelectedModel
- (*ChatRuntimeControls)(nil), // 6: liveagent.gateway.v1.ChatRuntimeControls
- (*ChatUploadedFile)(nil), // 7: liveagent.gateway.v1.ChatUploadedFile
- (*UploadReadableFile)(nil), // 8: liveagent.gateway.v1.UploadReadableFile
- (*UploadReadableFilesRequest)(nil), // 9: liveagent.gateway.v1.UploadReadableFilesRequest
- (*UploadReadableFilesResponse)(nil), // 10: liveagent.gateway.v1.UploadReadableFilesResponse
- (*UploadedImagePreviewRequest)(nil), // 11: liveagent.gateway.v1.UploadedImagePreviewRequest
- (*UploadedImagePreviewResponse)(nil), // 12: liveagent.gateway.v1.UploadedImagePreviewResponse
- (*MemoryManageRequest)(nil), // 13: liveagent.gateway.v1.MemoryManageRequest
- (*MemoryManageResponse)(nil), // 14: liveagent.gateway.v1.MemoryManageResponse
- (*TerminalRequest)(nil), // 15: liveagent.gateway.v1.TerminalRequest
- (*TerminalSession)(nil), // 16: liveagent.gateway.v1.TerminalSession
- (*TerminalShellOption)(nil), // 17: liveagent.gateway.v1.TerminalShellOption
- (*TerminalResponse)(nil), // 18: liveagent.gateway.v1.TerminalResponse
- (*TerminalEvent)(nil), // 19: liveagent.gateway.v1.TerminalEvent
- (*GitRequest)(nil), // 20: liveagent.gateway.v1.GitRequest
- (*GitResponse)(nil), // 21: liveagent.gateway.v1.GitResponse
- (*ChatRequest)(nil), // 22: liveagent.gateway.v1.ChatRequest
- (*CancelChatRequest)(nil), // 23: liveagent.gateway.v1.CancelChatRequest
- (*ChatEvent)(nil), // 24: liveagent.gateway.v1.ChatEvent
- (*CronManageRequest)(nil), // 25: liveagent.gateway.v1.CronManageRequest
- (*CronManageResponse)(nil), // 26: liveagent.gateway.v1.CronManageResponse
- (*HistoryListRequest)(nil), // 27: liveagent.gateway.v1.HistoryListRequest
- (*HistoryListResponse)(nil), // 28: liveagent.gateway.v1.HistoryListResponse
- (*ConversationSummary)(nil), // 29: liveagent.gateway.v1.ConversationSummary
- (*HistoryGetRequest)(nil), // 30: liveagent.gateway.v1.HistoryGetRequest
- (*HistoryGetResponse)(nil), // 31: liveagent.gateway.v1.HistoryGetResponse
- (*HistoryRenameRequest)(nil), // 32: liveagent.gateway.v1.HistoryRenameRequest
- (*HistoryRenameResponse)(nil), // 33: liveagent.gateway.v1.HistoryRenameResponse
- (*HistoryPinRequest)(nil), // 34: liveagent.gateway.v1.HistoryPinRequest
- (*HistoryPinResponse)(nil), // 35: liveagent.gateway.v1.HistoryPinResponse
- (*HistoryShareStatus)(nil), // 36: liveagent.gateway.v1.HistoryShareStatus
- (*HistoryShareGetRequest)(nil), // 37: liveagent.gateway.v1.HistoryShareGetRequest
- (*HistoryShareGetResponse)(nil), // 38: liveagent.gateway.v1.HistoryShareGetResponse
- (*HistoryShareSetRequest)(nil), // 39: liveagent.gateway.v1.HistoryShareSetRequest
- (*HistoryShareSetResponse)(nil), // 40: liveagent.gateway.v1.HistoryShareSetResponse
- (*HistoryShareResolveRequest)(nil), // 41: liveagent.gateway.v1.HistoryShareResolveRequest
- (*HistoryShareResolveResponse)(nil), // 42: liveagent.gateway.v1.HistoryShareResolveResponse
- (*HistoryWorkdirsRequest)(nil), // 43: liveagent.gateway.v1.HistoryWorkdirsRequest
- (*HistoryWorkdirSummary)(nil), // 44: liveagent.gateway.v1.HistoryWorkdirSummary
- (*HistoryWorkdirsResponse)(nil), // 45: liveagent.gateway.v1.HistoryWorkdirsResponse
- (*HistoryDeleteRequest)(nil), // 46: liveagent.gateway.v1.HistoryDeleteRequest
- (*HistoryDeleteResponse)(nil), // 47: liveagent.gateway.v1.HistoryDeleteResponse
- (*HistoryTruncateRequest)(nil), // 48: liveagent.gateway.v1.HistoryTruncateRequest
- (*HistoryTruncateResponse)(nil), // 49: liveagent.gateway.v1.HistoryTruncateResponse
- (*HistorySyncEvent)(nil), // 50: liveagent.gateway.v1.HistorySyncEvent
- (*ProviderListRequest)(nil), // 51: liveagent.gateway.v1.ProviderListRequest
- (*ProviderListResponse)(nil), // 52: liveagent.gateway.v1.ProviderListResponse
- (*SettingsGetRequest)(nil), // 53: liveagent.gateway.v1.SettingsGetRequest
- (*SettingsGetResponse)(nil), // 54: liveagent.gateway.v1.SettingsGetResponse
- (*SettingsUpdateRequest)(nil), // 55: liveagent.gateway.v1.SettingsUpdateRequest
- (*SettingsUpdateResponse)(nil), // 56: liveagent.gateway.v1.SettingsUpdateResponse
- (*SettingsSyncEvent)(nil), // 57: liveagent.gateway.v1.SettingsSyncEvent
- (*SkillFilesListRequest)(nil), // 58: liveagent.gateway.v1.SkillFilesListRequest
- (*SkillFilesListResponse)(nil), // 59: liveagent.gateway.v1.SkillFilesListResponse
- (*SkillMetadataReadRequest)(nil), // 60: liveagent.gateway.v1.SkillMetadataReadRequest
- (*SkillMetadataReadResponse)(nil), // 61: liveagent.gateway.v1.SkillMetadataReadResponse
- (*SkillTextReadRequest)(nil), // 62: liveagent.gateway.v1.SkillTextReadRequest
- (*SkillTextReadResponse)(nil), // 63: liveagent.gateway.v1.SkillTextReadResponse
- (*SkillManageRequest)(nil), // 64: liveagent.gateway.v1.SkillManageRequest
- (*SkillManageResponse)(nil), // 65: liveagent.gateway.v1.SkillManageResponse
- (*FileMentionListRequest)(nil), // 66: liveagent.gateway.v1.FileMentionListRequest
- (*FileMentionEntry)(nil), // 67: liveagent.gateway.v1.FileMentionEntry
- (*FileMentionListResponse)(nil), // 68: liveagent.gateway.v1.FileMentionListResponse
- (*FsRoot)(nil), // 69: liveagent.gateway.v1.FsRoot
- (*FsRootsRequest)(nil), // 70: liveagent.gateway.v1.FsRootsRequest
- (*FsRootsResponse)(nil), // 71: liveagent.gateway.v1.FsRootsResponse
- (*FsListDirsRequest)(nil), // 72: liveagent.gateway.v1.FsListDirsRequest
- (*FsDirEntry)(nil), // 73: liveagent.gateway.v1.FsDirEntry
- (*FsListDirsResponse)(nil), // 74: liveagent.gateway.v1.FsListDirsResponse
- (*FsCreateProjectFolderRequest)(nil), // 75: liveagent.gateway.v1.FsCreateProjectFolderRequest
- (*FsCreateProjectFolderResponse)(nil), // 76: liveagent.gateway.v1.FsCreateProjectFolderResponse
- (*FsListRequest)(nil), // 77: liveagent.gateway.v1.FsListRequest
- (*FsListEntry)(nil), // 78: liveagent.gateway.v1.FsListEntry
- (*FsListResponse)(nil), // 79: liveagent.gateway.v1.FsListResponse
- (*FsReadEditableTextRequest)(nil), // 80: liveagent.gateway.v1.FsReadEditableTextRequest
- (*FsReadEditableTextResponse)(nil), // 81: liveagent.gateway.v1.FsReadEditableTextResponse
- (*FsReadWorkspaceImageRequest)(nil), // 82: liveagent.gateway.v1.FsReadWorkspaceImageRequest
- (*FsReadWorkspaceImageResponse)(nil), // 83: liveagent.gateway.v1.FsReadWorkspaceImageResponse
- (*FsWriteTextRequest)(nil), // 84: liveagent.gateway.v1.FsWriteTextRequest
- (*FsWriteTextResponse)(nil), // 85: liveagent.gateway.v1.FsWriteTextResponse
- (*FsCreateDirRequest)(nil), // 86: liveagent.gateway.v1.FsCreateDirRequest
- (*FsCreateDirResponse)(nil), // 87: liveagent.gateway.v1.FsCreateDirResponse
- (*FsRenameRequest)(nil), // 88: liveagent.gateway.v1.FsRenameRequest
- (*FsRenameResponse)(nil), // 89: liveagent.gateway.v1.FsRenameResponse
- (*FsDeleteRequest)(nil), // 90: liveagent.gateway.v1.FsDeleteRequest
- (*FsDeleteResponse)(nil), // 91: liveagent.gateway.v1.FsDeleteResponse
- (*PingRequest)(nil), // 92: liveagent.gateway.v1.PingRequest
- (*PongResponse)(nil), // 93: liveagent.gateway.v1.PongResponse
- (*ErrorResponse)(nil), // 94: liveagent.gateway.v1.ErrorResponse
+ (TunnelFrameKind)(0), // 0: liveagent.gateway.v1.TunnelFrameKind
+ (ChatEvent_ChatEventType)(0), // 1: liveagent.gateway.v1.ChatEvent.ChatEventType
+ (*AuthRequest)(nil), // 2: liveagent.gateway.v1.AuthRequest
+ (*AuthResponse)(nil), // 3: liveagent.gateway.v1.AuthResponse
+ (*GatewayEnvelope)(nil), // 4: liveagent.gateway.v1.GatewayEnvelope
+ (*AgentEnvelope)(nil), // 5: liveagent.gateway.v1.AgentEnvelope
+ (*ChatSelectedModel)(nil), // 6: liveagent.gateway.v1.ChatSelectedModel
+ (*ChatRuntimeControls)(nil), // 7: liveagent.gateway.v1.ChatRuntimeControls
+ (*ChatUploadedFile)(nil), // 8: liveagent.gateway.v1.ChatUploadedFile
+ (*UploadReadableFile)(nil), // 9: liveagent.gateway.v1.UploadReadableFile
+ (*UploadReadableFilesRequest)(nil), // 10: liveagent.gateway.v1.UploadReadableFilesRequest
+ (*UploadReadableFilesResponse)(nil), // 11: liveagent.gateway.v1.UploadReadableFilesResponse
+ (*UploadedImagePreviewRequest)(nil), // 12: liveagent.gateway.v1.UploadedImagePreviewRequest
+ (*UploadedImagePreviewResponse)(nil), // 13: liveagent.gateway.v1.UploadedImagePreviewResponse
+ (*TunnelControlRequest)(nil), // 14: liveagent.gateway.v1.TunnelControlRequest
+ (*TunnelControlResponse)(nil), // 15: liveagent.gateway.v1.TunnelControlResponse
+ (*TunnelSummary)(nil), // 16: liveagent.gateway.v1.TunnelSummary
+ (*TunnelHeader)(nil), // 17: liveagent.gateway.v1.TunnelHeader
+ (*TunnelFrame)(nil), // 18: liveagent.gateway.v1.TunnelFrame
+ (*MemoryManageRequest)(nil), // 19: liveagent.gateway.v1.MemoryManageRequest
+ (*MemoryManageResponse)(nil), // 20: liveagent.gateway.v1.MemoryManageResponse
+ (*TerminalRequest)(nil), // 21: liveagent.gateway.v1.TerminalRequest
+ (*TerminalSession)(nil), // 22: liveagent.gateway.v1.TerminalSession
+ (*TerminalShellOption)(nil), // 23: liveagent.gateway.v1.TerminalShellOption
+ (*TerminalResponse)(nil), // 24: liveagent.gateway.v1.TerminalResponse
+ (*TerminalEvent)(nil), // 25: liveagent.gateway.v1.TerminalEvent
+ (*GitRequest)(nil), // 26: liveagent.gateway.v1.GitRequest
+ (*GitResponse)(nil), // 27: liveagent.gateway.v1.GitResponse
+ (*ChatRequest)(nil), // 28: liveagent.gateway.v1.ChatRequest
+ (*CancelChatRequest)(nil), // 29: liveagent.gateway.v1.CancelChatRequest
+ (*ChatEvent)(nil), // 30: liveagent.gateway.v1.ChatEvent
+ (*CronManageRequest)(nil), // 31: liveagent.gateway.v1.CronManageRequest
+ (*CronManageResponse)(nil), // 32: liveagent.gateway.v1.CronManageResponse
+ (*HistoryListRequest)(nil), // 33: liveagent.gateway.v1.HistoryListRequest
+ (*HistoryListResponse)(nil), // 34: liveagent.gateway.v1.HistoryListResponse
+ (*ConversationSummary)(nil), // 35: liveagent.gateway.v1.ConversationSummary
+ (*HistoryGetRequest)(nil), // 36: liveagent.gateway.v1.HistoryGetRequest
+ (*HistoryGetResponse)(nil), // 37: liveagent.gateway.v1.HistoryGetResponse
+ (*HistoryRenameRequest)(nil), // 38: liveagent.gateway.v1.HistoryRenameRequest
+ (*HistoryRenameResponse)(nil), // 39: liveagent.gateway.v1.HistoryRenameResponse
+ (*HistoryPinRequest)(nil), // 40: liveagent.gateway.v1.HistoryPinRequest
+ (*HistoryPinResponse)(nil), // 41: liveagent.gateway.v1.HistoryPinResponse
+ (*HistoryShareStatus)(nil), // 42: liveagent.gateway.v1.HistoryShareStatus
+ (*HistoryShareGetRequest)(nil), // 43: liveagent.gateway.v1.HistoryShareGetRequest
+ (*HistoryShareGetResponse)(nil), // 44: liveagent.gateway.v1.HistoryShareGetResponse
+ (*HistoryShareSetRequest)(nil), // 45: liveagent.gateway.v1.HistoryShareSetRequest
+ (*HistoryShareSetResponse)(nil), // 46: liveagent.gateway.v1.HistoryShareSetResponse
+ (*HistoryShareResolveRequest)(nil), // 47: liveagent.gateway.v1.HistoryShareResolveRequest
+ (*HistoryShareResolveResponse)(nil), // 48: liveagent.gateway.v1.HistoryShareResolveResponse
+ (*HistoryWorkdirsRequest)(nil), // 49: liveagent.gateway.v1.HistoryWorkdirsRequest
+ (*HistoryWorkdirSummary)(nil), // 50: liveagent.gateway.v1.HistoryWorkdirSummary
+ (*HistoryWorkdirsResponse)(nil), // 51: liveagent.gateway.v1.HistoryWorkdirsResponse
+ (*HistoryDeleteRequest)(nil), // 52: liveagent.gateway.v1.HistoryDeleteRequest
+ (*HistoryDeleteResponse)(nil), // 53: liveagent.gateway.v1.HistoryDeleteResponse
+ (*HistoryTruncateRequest)(nil), // 54: liveagent.gateway.v1.HistoryTruncateRequest
+ (*HistoryTruncateResponse)(nil), // 55: liveagent.gateway.v1.HistoryTruncateResponse
+ (*HistorySyncEvent)(nil), // 56: liveagent.gateway.v1.HistorySyncEvent
+ (*ProviderListRequest)(nil), // 57: liveagent.gateway.v1.ProviderListRequest
+ (*ProviderListResponse)(nil), // 58: liveagent.gateway.v1.ProviderListResponse
+ (*SettingsGetRequest)(nil), // 59: liveagent.gateway.v1.SettingsGetRequest
+ (*SettingsGetResponse)(nil), // 60: liveagent.gateway.v1.SettingsGetResponse
+ (*SettingsUpdateRequest)(nil), // 61: liveagent.gateway.v1.SettingsUpdateRequest
+ (*SettingsUpdateResponse)(nil), // 62: liveagent.gateway.v1.SettingsUpdateResponse
+ (*SettingsSyncEvent)(nil), // 63: liveagent.gateway.v1.SettingsSyncEvent
+ (*SkillFilesListRequest)(nil), // 64: liveagent.gateway.v1.SkillFilesListRequest
+ (*SkillFilesListResponse)(nil), // 65: liveagent.gateway.v1.SkillFilesListResponse
+ (*SkillMetadataReadRequest)(nil), // 66: liveagent.gateway.v1.SkillMetadataReadRequest
+ (*SkillMetadataReadResponse)(nil), // 67: liveagent.gateway.v1.SkillMetadataReadResponse
+ (*SkillTextReadRequest)(nil), // 68: liveagent.gateway.v1.SkillTextReadRequest
+ (*SkillTextReadResponse)(nil), // 69: liveagent.gateway.v1.SkillTextReadResponse
+ (*SkillManageRequest)(nil), // 70: liveagent.gateway.v1.SkillManageRequest
+ (*SkillManageResponse)(nil), // 71: liveagent.gateway.v1.SkillManageResponse
+ (*FileMentionListRequest)(nil), // 72: liveagent.gateway.v1.FileMentionListRequest
+ (*FileMentionEntry)(nil), // 73: liveagent.gateway.v1.FileMentionEntry
+ (*FileMentionListResponse)(nil), // 74: liveagent.gateway.v1.FileMentionListResponse
+ (*FsRoot)(nil), // 75: liveagent.gateway.v1.FsRoot
+ (*FsRootsRequest)(nil), // 76: liveagent.gateway.v1.FsRootsRequest
+ (*FsRootsResponse)(nil), // 77: liveagent.gateway.v1.FsRootsResponse
+ (*FsListDirsRequest)(nil), // 78: liveagent.gateway.v1.FsListDirsRequest
+ (*FsDirEntry)(nil), // 79: liveagent.gateway.v1.FsDirEntry
+ (*FsListDirsResponse)(nil), // 80: liveagent.gateway.v1.FsListDirsResponse
+ (*FsCreateProjectFolderRequest)(nil), // 81: liveagent.gateway.v1.FsCreateProjectFolderRequest
+ (*FsCreateProjectFolderResponse)(nil), // 82: liveagent.gateway.v1.FsCreateProjectFolderResponse
+ (*FsListRequest)(nil), // 83: liveagent.gateway.v1.FsListRequest
+ (*FsListEntry)(nil), // 84: liveagent.gateway.v1.FsListEntry
+ (*FsListResponse)(nil), // 85: liveagent.gateway.v1.FsListResponse
+ (*FsReadEditableTextRequest)(nil), // 86: liveagent.gateway.v1.FsReadEditableTextRequest
+ (*FsReadEditableTextResponse)(nil), // 87: liveagent.gateway.v1.FsReadEditableTextResponse
+ (*FsReadWorkspaceImageRequest)(nil), // 88: liveagent.gateway.v1.FsReadWorkspaceImageRequest
+ (*FsReadWorkspaceImageResponse)(nil), // 89: liveagent.gateway.v1.FsReadWorkspaceImageResponse
+ (*FsWriteTextRequest)(nil), // 90: liveagent.gateway.v1.FsWriteTextRequest
+ (*FsWriteTextResponse)(nil), // 91: liveagent.gateway.v1.FsWriteTextResponse
+ (*FsCreateDirRequest)(nil), // 92: liveagent.gateway.v1.FsCreateDirRequest
+ (*FsCreateDirResponse)(nil), // 93: liveagent.gateway.v1.FsCreateDirResponse
+ (*FsRenameRequest)(nil), // 94: liveagent.gateway.v1.FsRenameRequest
+ (*FsRenameResponse)(nil), // 95: liveagent.gateway.v1.FsRenameResponse
+ (*FsDeleteRequest)(nil), // 96: liveagent.gateway.v1.FsDeleteRequest
+ (*FsDeleteResponse)(nil), // 97: liveagent.gateway.v1.FsDeleteResponse
+ (*PingRequest)(nil), // 98: liveagent.gateway.v1.PingRequest
+ (*PongResponse)(nil), // 99: liveagent.gateway.v1.PongResponse
+ (*ErrorResponse)(nil), // 100: liveagent.gateway.v1.ErrorResponse
}
var file_proto_v1_gateway_proto_depIdxs = []int32{
- 22, // 0: liveagent.gateway.v1.GatewayEnvelope.chat_request:type_name -> liveagent.gateway.v1.ChatRequest
- 23, // 1: liveagent.gateway.v1.GatewayEnvelope.cancel_chat:type_name -> liveagent.gateway.v1.CancelChatRequest
- 25, // 2: liveagent.gateway.v1.GatewayEnvelope.cron_manage:type_name -> liveagent.gateway.v1.CronManageRequest
- 27, // 3: liveagent.gateway.v1.GatewayEnvelope.history_list:type_name -> liveagent.gateway.v1.HistoryListRequest
- 30, // 4: liveagent.gateway.v1.GatewayEnvelope.history_get:type_name -> liveagent.gateway.v1.HistoryGetRequest
- 32, // 5: liveagent.gateway.v1.GatewayEnvelope.history_rename:type_name -> liveagent.gateway.v1.HistoryRenameRequest
- 46, // 6: liveagent.gateway.v1.GatewayEnvelope.history_delete:type_name -> liveagent.gateway.v1.HistoryDeleteRequest
- 48, // 7: liveagent.gateway.v1.GatewayEnvelope.history_truncate:type_name -> liveagent.gateway.v1.HistoryTruncateRequest
- 34, // 8: liveagent.gateway.v1.GatewayEnvelope.history_pin:type_name -> liveagent.gateway.v1.HistoryPinRequest
- 37, // 9: liveagent.gateway.v1.GatewayEnvelope.history_share_get:type_name -> liveagent.gateway.v1.HistoryShareGetRequest
- 39, // 10: liveagent.gateway.v1.GatewayEnvelope.history_share_set:type_name -> liveagent.gateway.v1.HistoryShareSetRequest
- 41, // 11: liveagent.gateway.v1.GatewayEnvelope.history_share_resolve:type_name -> liveagent.gateway.v1.HistoryShareResolveRequest
- 43, // 12: liveagent.gateway.v1.GatewayEnvelope.history_workdirs:type_name -> liveagent.gateway.v1.HistoryWorkdirsRequest
- 51, // 13: liveagent.gateway.v1.GatewayEnvelope.provider_list:type_name -> liveagent.gateway.v1.ProviderListRequest
- 53, // 14: liveagent.gateway.v1.GatewayEnvelope.settings_get:type_name -> liveagent.gateway.v1.SettingsGetRequest
- 55, // 15: liveagent.gateway.v1.GatewayEnvelope.settings_update:type_name -> liveagent.gateway.v1.SettingsUpdateRequest
- 58, // 16: liveagent.gateway.v1.GatewayEnvelope.skill_files_list:type_name -> liveagent.gateway.v1.SkillFilesListRequest
- 60, // 17: liveagent.gateway.v1.GatewayEnvelope.skill_metadata_read:type_name -> liveagent.gateway.v1.SkillMetadataReadRequest
- 62, // 18: liveagent.gateway.v1.GatewayEnvelope.skill_text_read:type_name -> liveagent.gateway.v1.SkillTextReadRequest
- 66, // 19: liveagent.gateway.v1.GatewayEnvelope.file_mention_list:type_name -> liveagent.gateway.v1.FileMentionListRequest
- 9, // 20: liveagent.gateway.v1.GatewayEnvelope.upload_readable_files:type_name -> liveagent.gateway.v1.UploadReadableFilesRequest
- 70, // 21: liveagent.gateway.v1.GatewayEnvelope.fs_roots:type_name -> liveagent.gateway.v1.FsRootsRequest
- 72, // 22: liveagent.gateway.v1.GatewayEnvelope.fs_list_dirs:type_name -> liveagent.gateway.v1.FsListDirsRequest
- 92, // 23: liveagent.gateway.v1.GatewayEnvelope.ping:type_name -> liveagent.gateway.v1.PingRequest
- 11, // 24: liveagent.gateway.v1.GatewayEnvelope.uploaded_image_preview:type_name -> liveagent.gateway.v1.UploadedImagePreviewRequest
- 13, // 25: liveagent.gateway.v1.GatewayEnvelope.memory_manage:type_name -> liveagent.gateway.v1.MemoryManageRequest
- 64, // 26: liveagent.gateway.v1.GatewayEnvelope.skill_manage:type_name -> liveagent.gateway.v1.SkillManageRequest
- 75, // 27: liveagent.gateway.v1.GatewayEnvelope.fs_create_project_folder:type_name -> liveagent.gateway.v1.FsCreateProjectFolderRequest
- 15, // 28: liveagent.gateway.v1.GatewayEnvelope.terminal_request:type_name -> liveagent.gateway.v1.TerminalRequest
- 77, // 29: liveagent.gateway.v1.GatewayEnvelope.fs_list:type_name -> liveagent.gateway.v1.FsListRequest
- 84, // 30: liveagent.gateway.v1.GatewayEnvelope.fs_write_text:type_name -> liveagent.gateway.v1.FsWriteTextRequest
- 86, // 31: liveagent.gateway.v1.GatewayEnvelope.fs_create_dir:type_name -> liveagent.gateway.v1.FsCreateDirRequest
- 88, // 32: liveagent.gateway.v1.GatewayEnvelope.fs_rename:type_name -> liveagent.gateway.v1.FsRenameRequest
- 90, // 33: liveagent.gateway.v1.GatewayEnvelope.fs_delete:type_name -> liveagent.gateway.v1.FsDeleteRequest
- 20, // 34: liveagent.gateway.v1.GatewayEnvelope.git_request:type_name -> liveagent.gateway.v1.GitRequest
- 80, // 35: liveagent.gateway.v1.GatewayEnvelope.fs_read_editable_text:type_name -> liveagent.gateway.v1.FsReadEditableTextRequest
- 82, // 36: liveagent.gateway.v1.GatewayEnvelope.fs_read_workspace_image:type_name -> liveagent.gateway.v1.FsReadWorkspaceImageRequest
- 24, // 37: liveagent.gateway.v1.AgentEnvelope.chat_event:type_name -> liveagent.gateway.v1.ChatEvent
- 26, // 38: liveagent.gateway.v1.AgentEnvelope.cron_manage_resp:type_name -> liveagent.gateway.v1.CronManageResponse
- 28, // 39: liveagent.gateway.v1.AgentEnvelope.history_list_resp:type_name -> liveagent.gateway.v1.HistoryListResponse
- 31, // 40: liveagent.gateway.v1.AgentEnvelope.history_get_resp:type_name -> liveagent.gateway.v1.HistoryGetResponse
- 33, // 41: liveagent.gateway.v1.AgentEnvelope.history_rename_resp:type_name -> liveagent.gateway.v1.HistoryRenameResponse
- 47, // 42: liveagent.gateway.v1.AgentEnvelope.history_delete_resp:type_name -> liveagent.gateway.v1.HistoryDeleteResponse
- 50, // 43: liveagent.gateway.v1.AgentEnvelope.history_sync:type_name -> liveagent.gateway.v1.HistorySyncEvent
- 49, // 44: liveagent.gateway.v1.AgentEnvelope.history_truncate_resp:type_name -> liveagent.gateway.v1.HistoryTruncateResponse
- 35, // 45: liveagent.gateway.v1.AgentEnvelope.history_pin_resp:type_name -> liveagent.gateway.v1.HistoryPinResponse
- 38, // 46: liveagent.gateway.v1.AgentEnvelope.history_share_get_resp:type_name -> liveagent.gateway.v1.HistoryShareGetResponse
- 40, // 47: liveagent.gateway.v1.AgentEnvelope.history_share_set_resp:type_name -> liveagent.gateway.v1.HistoryShareSetResponse
- 42, // 48: liveagent.gateway.v1.AgentEnvelope.history_share_resolve_resp:type_name -> liveagent.gateway.v1.HistoryShareResolveResponse
- 45, // 49: liveagent.gateway.v1.AgentEnvelope.history_workdirs_resp:type_name -> liveagent.gateway.v1.HistoryWorkdirsResponse
- 52, // 50: liveagent.gateway.v1.AgentEnvelope.provider_list_resp:type_name -> liveagent.gateway.v1.ProviderListResponse
- 54, // 51: liveagent.gateway.v1.AgentEnvelope.settings_get_resp:type_name -> liveagent.gateway.v1.SettingsGetResponse
- 56, // 52: liveagent.gateway.v1.AgentEnvelope.settings_update_resp:type_name -> liveagent.gateway.v1.SettingsUpdateResponse
- 57, // 53: liveagent.gateway.v1.AgentEnvelope.settings_sync:type_name -> liveagent.gateway.v1.SettingsSyncEvent
- 59, // 54: liveagent.gateway.v1.AgentEnvelope.skill_files_list_resp:type_name -> liveagent.gateway.v1.SkillFilesListResponse
- 61, // 55: liveagent.gateway.v1.AgentEnvelope.skill_metadata_read_resp:type_name -> liveagent.gateway.v1.SkillMetadataReadResponse
- 63, // 56: liveagent.gateway.v1.AgentEnvelope.skill_text_read_resp:type_name -> liveagent.gateway.v1.SkillTextReadResponse
- 68, // 57: liveagent.gateway.v1.AgentEnvelope.file_mention_list_resp:type_name -> liveagent.gateway.v1.FileMentionListResponse
- 10, // 58: liveagent.gateway.v1.AgentEnvelope.upload_readable_files_resp:type_name -> liveagent.gateway.v1.UploadReadableFilesResponse
- 71, // 59: liveagent.gateway.v1.AgentEnvelope.fs_roots_resp:type_name -> liveagent.gateway.v1.FsRootsResponse
- 93, // 60: liveagent.gateway.v1.AgentEnvelope.pong:type_name -> liveagent.gateway.v1.PongResponse
- 74, // 61: liveagent.gateway.v1.AgentEnvelope.fs_list_dirs_resp:type_name -> liveagent.gateway.v1.FsListDirsResponse
- 12, // 62: liveagent.gateway.v1.AgentEnvelope.uploaded_image_preview_resp:type_name -> liveagent.gateway.v1.UploadedImagePreviewResponse
- 14, // 63: liveagent.gateway.v1.AgentEnvelope.memory_manage_resp:type_name -> liveagent.gateway.v1.MemoryManageResponse
- 65, // 64: liveagent.gateway.v1.AgentEnvelope.skill_manage_resp:type_name -> liveagent.gateway.v1.SkillManageResponse
- 76, // 65: liveagent.gateway.v1.AgentEnvelope.fs_create_project_folder_resp:type_name -> liveagent.gateway.v1.FsCreateProjectFolderResponse
- 18, // 66: liveagent.gateway.v1.AgentEnvelope.terminal_response:type_name -> liveagent.gateway.v1.TerminalResponse
- 19, // 67: liveagent.gateway.v1.AgentEnvelope.terminal_event:type_name -> liveagent.gateway.v1.TerminalEvent
- 79, // 68: liveagent.gateway.v1.AgentEnvelope.fs_list_resp:type_name -> liveagent.gateway.v1.FsListResponse
- 85, // 69: liveagent.gateway.v1.AgentEnvelope.fs_write_text_resp:type_name -> liveagent.gateway.v1.FsWriteTextResponse
- 87, // 70: liveagent.gateway.v1.AgentEnvelope.fs_create_dir_resp:type_name -> liveagent.gateway.v1.FsCreateDirResponse
- 89, // 71: liveagent.gateway.v1.AgentEnvelope.fs_rename_resp:type_name -> liveagent.gateway.v1.FsRenameResponse
- 91, // 72: liveagent.gateway.v1.AgentEnvelope.fs_delete_resp:type_name -> liveagent.gateway.v1.FsDeleteResponse
- 21, // 73: liveagent.gateway.v1.AgentEnvelope.git_response:type_name -> liveagent.gateway.v1.GitResponse
- 81, // 74: liveagent.gateway.v1.AgentEnvelope.fs_read_editable_text_resp:type_name -> liveagent.gateway.v1.FsReadEditableTextResponse
- 83, // 75: liveagent.gateway.v1.AgentEnvelope.fs_read_workspace_image_resp:type_name -> liveagent.gateway.v1.FsReadWorkspaceImageResponse
- 94, // 76: liveagent.gateway.v1.AgentEnvelope.error:type_name -> liveagent.gateway.v1.ErrorResponse
- 8, // 77: liveagent.gateway.v1.UploadReadableFilesRequest.files:type_name -> liveagent.gateway.v1.UploadReadableFile
- 7, // 78: liveagent.gateway.v1.UploadReadableFilesResponse.files:type_name -> liveagent.gateway.v1.ChatUploadedFile
- 16, // 79: liveagent.gateway.v1.TerminalResponse.sessions:type_name -> liveagent.gateway.v1.TerminalSession
- 16, // 80: liveagent.gateway.v1.TerminalResponse.session:type_name -> liveagent.gateway.v1.TerminalSession
- 17, // 81: liveagent.gateway.v1.TerminalResponse.shell_options:type_name -> liveagent.gateway.v1.TerminalShellOption
- 16, // 82: liveagent.gateway.v1.TerminalEvent.session:type_name -> liveagent.gateway.v1.TerminalSession
- 5, // 83: liveagent.gateway.v1.ChatRequest.selected_model:type_name -> liveagent.gateway.v1.ChatSelectedModel
- 7, // 84: liveagent.gateway.v1.ChatRequest.uploaded_files:type_name -> liveagent.gateway.v1.ChatUploadedFile
- 6, // 85: liveagent.gateway.v1.ChatRequest.runtime_controls:type_name -> liveagent.gateway.v1.ChatRuntimeControls
- 0, // 86: liveagent.gateway.v1.ChatEvent.type:type_name -> liveagent.gateway.v1.ChatEvent.ChatEventType
- 29, // 87: liveagent.gateway.v1.HistoryListResponse.conversations:type_name -> liveagent.gateway.v1.ConversationSummary
- 29, // 88: liveagent.gateway.v1.HistoryGetResponse.conversation:type_name -> liveagent.gateway.v1.ConversationSummary
- 29, // 89: liveagent.gateway.v1.HistoryRenameResponse.conversation:type_name -> liveagent.gateway.v1.ConversationSummary
- 29, // 90: liveagent.gateway.v1.HistoryPinResponse.conversation:type_name -> liveagent.gateway.v1.ConversationSummary
- 36, // 91: liveagent.gateway.v1.HistoryShareGetResponse.share:type_name -> liveagent.gateway.v1.HistoryShareStatus
- 36, // 92: liveagent.gateway.v1.HistoryShareSetResponse.share:type_name -> liveagent.gateway.v1.HistoryShareStatus
- 29, // 93: liveagent.gateway.v1.HistoryShareResolveResponse.conversation:type_name -> liveagent.gateway.v1.ConversationSummary
- 44, // 94: liveagent.gateway.v1.HistoryWorkdirsResponse.workdirs:type_name -> liveagent.gateway.v1.HistoryWorkdirSummary
- 29, // 95: liveagent.gateway.v1.HistoryTruncateResponse.conversation:type_name -> liveagent.gateway.v1.ConversationSummary
- 29, // 96: liveagent.gateway.v1.HistorySyncEvent.conversation:type_name -> liveagent.gateway.v1.ConversationSummary
- 67, // 97: liveagent.gateway.v1.FileMentionListResponse.entries:type_name -> liveagent.gateway.v1.FileMentionEntry
- 69, // 98: liveagent.gateway.v1.FsRootsResponse.roots:type_name -> liveagent.gateway.v1.FsRoot
- 73, // 99: liveagent.gateway.v1.FsListDirsResponse.entries:type_name -> liveagent.gateway.v1.FsDirEntry
- 78, // 100: liveagent.gateway.v1.FsListResponse.entries:type_name -> liveagent.gateway.v1.FsListEntry
- 4, // 101: liveagent.gateway.v1.AgentGateway.AgentConnect:input_type -> liveagent.gateway.v1.AgentEnvelope
- 1, // 102: liveagent.gateway.v1.AgentGateway.Authenticate:input_type -> liveagent.gateway.v1.AuthRequest
- 3, // 103: liveagent.gateway.v1.AgentGateway.AgentConnect:output_type -> liveagent.gateway.v1.GatewayEnvelope
- 2, // 104: liveagent.gateway.v1.AgentGateway.Authenticate:output_type -> liveagent.gateway.v1.AuthResponse
- 103, // [103:105] is the sub-list for method output_type
- 101, // [101:103] is the sub-list for method input_type
- 101, // [101:101] is the sub-list for extension type_name
- 101, // [101:101] is the sub-list for extension extendee
- 0, // [0:101] is the sub-list for field type_name
+ 28, // 0: liveagent.gateway.v1.GatewayEnvelope.chat_request:type_name -> liveagent.gateway.v1.ChatRequest
+ 29, // 1: liveagent.gateway.v1.GatewayEnvelope.cancel_chat:type_name -> liveagent.gateway.v1.CancelChatRequest
+ 31, // 2: liveagent.gateway.v1.GatewayEnvelope.cron_manage:type_name -> liveagent.gateway.v1.CronManageRequest
+ 33, // 3: liveagent.gateway.v1.GatewayEnvelope.history_list:type_name -> liveagent.gateway.v1.HistoryListRequest
+ 36, // 4: liveagent.gateway.v1.GatewayEnvelope.history_get:type_name -> liveagent.gateway.v1.HistoryGetRequest
+ 38, // 5: liveagent.gateway.v1.GatewayEnvelope.history_rename:type_name -> liveagent.gateway.v1.HistoryRenameRequest
+ 52, // 6: liveagent.gateway.v1.GatewayEnvelope.history_delete:type_name -> liveagent.gateway.v1.HistoryDeleteRequest
+ 54, // 7: liveagent.gateway.v1.GatewayEnvelope.history_truncate:type_name -> liveagent.gateway.v1.HistoryTruncateRequest
+ 40, // 8: liveagent.gateway.v1.GatewayEnvelope.history_pin:type_name -> liveagent.gateway.v1.HistoryPinRequest
+ 43, // 9: liveagent.gateway.v1.GatewayEnvelope.history_share_get:type_name -> liveagent.gateway.v1.HistoryShareGetRequest
+ 45, // 10: liveagent.gateway.v1.GatewayEnvelope.history_share_set:type_name -> liveagent.gateway.v1.HistoryShareSetRequest
+ 47, // 11: liveagent.gateway.v1.GatewayEnvelope.history_share_resolve:type_name -> liveagent.gateway.v1.HistoryShareResolveRequest
+ 49, // 12: liveagent.gateway.v1.GatewayEnvelope.history_workdirs:type_name -> liveagent.gateway.v1.HistoryWorkdirsRequest
+ 57, // 13: liveagent.gateway.v1.GatewayEnvelope.provider_list:type_name -> liveagent.gateway.v1.ProviderListRequest
+ 59, // 14: liveagent.gateway.v1.GatewayEnvelope.settings_get:type_name -> liveagent.gateway.v1.SettingsGetRequest
+ 61, // 15: liveagent.gateway.v1.GatewayEnvelope.settings_update:type_name -> liveagent.gateway.v1.SettingsUpdateRequest
+ 64, // 16: liveagent.gateway.v1.GatewayEnvelope.skill_files_list:type_name -> liveagent.gateway.v1.SkillFilesListRequest
+ 66, // 17: liveagent.gateway.v1.GatewayEnvelope.skill_metadata_read:type_name -> liveagent.gateway.v1.SkillMetadataReadRequest
+ 68, // 18: liveagent.gateway.v1.GatewayEnvelope.skill_text_read:type_name -> liveagent.gateway.v1.SkillTextReadRequest
+ 72, // 19: liveagent.gateway.v1.GatewayEnvelope.file_mention_list:type_name -> liveagent.gateway.v1.FileMentionListRequest
+ 10, // 20: liveagent.gateway.v1.GatewayEnvelope.upload_readable_files:type_name -> liveagent.gateway.v1.UploadReadableFilesRequest
+ 76, // 21: liveagent.gateway.v1.GatewayEnvelope.fs_roots:type_name -> liveagent.gateway.v1.FsRootsRequest
+ 78, // 22: liveagent.gateway.v1.GatewayEnvelope.fs_list_dirs:type_name -> liveagent.gateway.v1.FsListDirsRequest
+ 98, // 23: liveagent.gateway.v1.GatewayEnvelope.ping:type_name -> liveagent.gateway.v1.PingRequest
+ 12, // 24: liveagent.gateway.v1.GatewayEnvelope.uploaded_image_preview:type_name -> liveagent.gateway.v1.UploadedImagePreviewRequest
+ 19, // 25: liveagent.gateway.v1.GatewayEnvelope.memory_manage:type_name -> liveagent.gateway.v1.MemoryManageRequest
+ 70, // 26: liveagent.gateway.v1.GatewayEnvelope.skill_manage:type_name -> liveagent.gateway.v1.SkillManageRequest
+ 81, // 27: liveagent.gateway.v1.GatewayEnvelope.fs_create_project_folder:type_name -> liveagent.gateway.v1.FsCreateProjectFolderRequest
+ 21, // 28: liveagent.gateway.v1.GatewayEnvelope.terminal_request:type_name -> liveagent.gateway.v1.TerminalRequest
+ 83, // 29: liveagent.gateway.v1.GatewayEnvelope.fs_list:type_name -> liveagent.gateway.v1.FsListRequest
+ 90, // 30: liveagent.gateway.v1.GatewayEnvelope.fs_write_text:type_name -> liveagent.gateway.v1.FsWriteTextRequest
+ 92, // 31: liveagent.gateway.v1.GatewayEnvelope.fs_create_dir:type_name -> liveagent.gateway.v1.FsCreateDirRequest
+ 94, // 32: liveagent.gateway.v1.GatewayEnvelope.fs_rename:type_name -> liveagent.gateway.v1.FsRenameRequest
+ 96, // 33: liveagent.gateway.v1.GatewayEnvelope.fs_delete:type_name -> liveagent.gateway.v1.FsDeleteRequest
+ 26, // 34: liveagent.gateway.v1.GatewayEnvelope.git_request:type_name -> liveagent.gateway.v1.GitRequest
+ 86, // 35: liveagent.gateway.v1.GatewayEnvelope.fs_read_editable_text:type_name -> liveagent.gateway.v1.FsReadEditableTextRequest
+ 88, // 36: liveagent.gateway.v1.GatewayEnvelope.fs_read_workspace_image:type_name -> liveagent.gateway.v1.FsReadWorkspaceImageRequest
+ 14, // 37: liveagent.gateway.v1.GatewayEnvelope.tunnel_control:type_name -> liveagent.gateway.v1.TunnelControlRequest
+ 15, // 38: liveagent.gateway.v1.GatewayEnvelope.tunnel_control_resp:type_name -> liveagent.gateway.v1.TunnelControlResponse
+ 18, // 39: liveagent.gateway.v1.GatewayEnvelope.tunnel_frame:type_name -> liveagent.gateway.v1.TunnelFrame
+ 30, // 40: liveagent.gateway.v1.AgentEnvelope.chat_event:type_name -> liveagent.gateway.v1.ChatEvent
+ 32, // 41: liveagent.gateway.v1.AgentEnvelope.cron_manage_resp:type_name -> liveagent.gateway.v1.CronManageResponse
+ 34, // 42: liveagent.gateway.v1.AgentEnvelope.history_list_resp:type_name -> liveagent.gateway.v1.HistoryListResponse
+ 37, // 43: liveagent.gateway.v1.AgentEnvelope.history_get_resp:type_name -> liveagent.gateway.v1.HistoryGetResponse
+ 39, // 44: liveagent.gateway.v1.AgentEnvelope.history_rename_resp:type_name -> liveagent.gateway.v1.HistoryRenameResponse
+ 53, // 45: liveagent.gateway.v1.AgentEnvelope.history_delete_resp:type_name -> liveagent.gateway.v1.HistoryDeleteResponse
+ 56, // 46: liveagent.gateway.v1.AgentEnvelope.history_sync:type_name -> liveagent.gateway.v1.HistorySyncEvent
+ 55, // 47: liveagent.gateway.v1.AgentEnvelope.history_truncate_resp:type_name -> liveagent.gateway.v1.HistoryTruncateResponse
+ 41, // 48: liveagent.gateway.v1.AgentEnvelope.history_pin_resp:type_name -> liveagent.gateway.v1.HistoryPinResponse
+ 44, // 49: liveagent.gateway.v1.AgentEnvelope.history_share_get_resp:type_name -> liveagent.gateway.v1.HistoryShareGetResponse
+ 46, // 50: liveagent.gateway.v1.AgentEnvelope.history_share_set_resp:type_name -> liveagent.gateway.v1.HistoryShareSetResponse
+ 48, // 51: liveagent.gateway.v1.AgentEnvelope.history_share_resolve_resp:type_name -> liveagent.gateway.v1.HistoryShareResolveResponse
+ 51, // 52: liveagent.gateway.v1.AgentEnvelope.history_workdirs_resp:type_name -> liveagent.gateway.v1.HistoryWorkdirsResponse
+ 58, // 53: liveagent.gateway.v1.AgentEnvelope.provider_list_resp:type_name -> liveagent.gateway.v1.ProviderListResponse
+ 60, // 54: liveagent.gateway.v1.AgentEnvelope.settings_get_resp:type_name -> liveagent.gateway.v1.SettingsGetResponse
+ 62, // 55: liveagent.gateway.v1.AgentEnvelope.settings_update_resp:type_name -> liveagent.gateway.v1.SettingsUpdateResponse
+ 63, // 56: liveagent.gateway.v1.AgentEnvelope.settings_sync:type_name -> liveagent.gateway.v1.SettingsSyncEvent
+ 65, // 57: liveagent.gateway.v1.AgentEnvelope.skill_files_list_resp:type_name -> liveagent.gateway.v1.SkillFilesListResponse
+ 67, // 58: liveagent.gateway.v1.AgentEnvelope.skill_metadata_read_resp:type_name -> liveagent.gateway.v1.SkillMetadataReadResponse
+ 69, // 59: liveagent.gateway.v1.AgentEnvelope.skill_text_read_resp:type_name -> liveagent.gateway.v1.SkillTextReadResponse
+ 74, // 60: liveagent.gateway.v1.AgentEnvelope.file_mention_list_resp:type_name -> liveagent.gateway.v1.FileMentionListResponse
+ 11, // 61: liveagent.gateway.v1.AgentEnvelope.upload_readable_files_resp:type_name -> liveagent.gateway.v1.UploadReadableFilesResponse
+ 77, // 62: liveagent.gateway.v1.AgentEnvelope.fs_roots_resp:type_name -> liveagent.gateway.v1.FsRootsResponse
+ 99, // 63: liveagent.gateway.v1.AgentEnvelope.pong:type_name -> liveagent.gateway.v1.PongResponse
+ 80, // 64: liveagent.gateway.v1.AgentEnvelope.fs_list_dirs_resp:type_name -> liveagent.gateway.v1.FsListDirsResponse
+ 13, // 65: liveagent.gateway.v1.AgentEnvelope.uploaded_image_preview_resp:type_name -> liveagent.gateway.v1.UploadedImagePreviewResponse
+ 20, // 66: liveagent.gateway.v1.AgentEnvelope.memory_manage_resp:type_name -> liveagent.gateway.v1.MemoryManageResponse
+ 71, // 67: liveagent.gateway.v1.AgentEnvelope.skill_manage_resp:type_name -> liveagent.gateway.v1.SkillManageResponse
+ 82, // 68: liveagent.gateway.v1.AgentEnvelope.fs_create_project_folder_resp:type_name -> liveagent.gateway.v1.FsCreateProjectFolderResponse
+ 24, // 69: liveagent.gateway.v1.AgentEnvelope.terminal_response:type_name -> liveagent.gateway.v1.TerminalResponse
+ 25, // 70: liveagent.gateway.v1.AgentEnvelope.terminal_event:type_name -> liveagent.gateway.v1.TerminalEvent
+ 85, // 71: liveagent.gateway.v1.AgentEnvelope.fs_list_resp:type_name -> liveagent.gateway.v1.FsListResponse
+ 91, // 72: liveagent.gateway.v1.AgentEnvelope.fs_write_text_resp:type_name -> liveagent.gateway.v1.FsWriteTextResponse
+ 93, // 73: liveagent.gateway.v1.AgentEnvelope.fs_create_dir_resp:type_name -> liveagent.gateway.v1.FsCreateDirResponse
+ 95, // 74: liveagent.gateway.v1.AgentEnvelope.fs_rename_resp:type_name -> liveagent.gateway.v1.FsRenameResponse
+ 97, // 75: liveagent.gateway.v1.AgentEnvelope.fs_delete_resp:type_name -> liveagent.gateway.v1.FsDeleteResponse
+ 27, // 76: liveagent.gateway.v1.AgentEnvelope.git_response:type_name -> liveagent.gateway.v1.GitResponse
+ 87, // 77: liveagent.gateway.v1.AgentEnvelope.fs_read_editable_text_resp:type_name -> liveagent.gateway.v1.FsReadEditableTextResponse
+ 89, // 78: liveagent.gateway.v1.AgentEnvelope.fs_read_workspace_image_resp:type_name -> liveagent.gateway.v1.FsReadWorkspaceImageResponse
+ 14, // 79: liveagent.gateway.v1.AgentEnvelope.tunnel_control:type_name -> liveagent.gateway.v1.TunnelControlRequest
+ 15, // 80: liveagent.gateway.v1.AgentEnvelope.tunnel_control_resp:type_name -> liveagent.gateway.v1.TunnelControlResponse
+ 18, // 81: liveagent.gateway.v1.AgentEnvelope.tunnel_frame:type_name -> liveagent.gateway.v1.TunnelFrame
+ 100, // 82: liveagent.gateway.v1.AgentEnvelope.error:type_name -> liveagent.gateway.v1.ErrorResponse
+ 9, // 83: liveagent.gateway.v1.UploadReadableFilesRequest.files:type_name -> liveagent.gateway.v1.UploadReadableFile
+ 8, // 84: liveagent.gateway.v1.UploadReadableFilesResponse.files:type_name -> liveagent.gateway.v1.ChatUploadedFile
+ 16, // 85: liveagent.gateway.v1.TunnelControlResponse.tunnels:type_name -> liveagent.gateway.v1.TunnelSummary
+ 16, // 86: liveagent.gateway.v1.TunnelControlResponse.tunnel:type_name -> liveagent.gateway.v1.TunnelSummary
+ 0, // 87: liveagent.gateway.v1.TunnelFrame.kind:type_name -> liveagent.gateway.v1.TunnelFrameKind
+ 17, // 88: liveagent.gateway.v1.TunnelFrame.headers:type_name -> liveagent.gateway.v1.TunnelHeader
+ 22, // 89: liveagent.gateway.v1.TerminalResponse.sessions:type_name -> liveagent.gateway.v1.TerminalSession
+ 22, // 90: liveagent.gateway.v1.TerminalResponse.session:type_name -> liveagent.gateway.v1.TerminalSession
+ 23, // 91: liveagent.gateway.v1.TerminalResponse.shell_options:type_name -> liveagent.gateway.v1.TerminalShellOption
+ 22, // 92: liveagent.gateway.v1.TerminalEvent.session:type_name -> liveagent.gateway.v1.TerminalSession
+ 6, // 93: liveagent.gateway.v1.ChatRequest.selected_model:type_name -> liveagent.gateway.v1.ChatSelectedModel
+ 8, // 94: liveagent.gateway.v1.ChatRequest.uploaded_files:type_name -> liveagent.gateway.v1.ChatUploadedFile
+ 7, // 95: liveagent.gateway.v1.ChatRequest.runtime_controls:type_name -> liveagent.gateway.v1.ChatRuntimeControls
+ 1, // 96: liveagent.gateway.v1.ChatEvent.type:type_name -> liveagent.gateway.v1.ChatEvent.ChatEventType
+ 35, // 97: liveagent.gateway.v1.HistoryListResponse.conversations:type_name -> liveagent.gateway.v1.ConversationSummary
+ 35, // 98: liveagent.gateway.v1.HistoryGetResponse.conversation:type_name -> liveagent.gateway.v1.ConversationSummary
+ 35, // 99: liveagent.gateway.v1.HistoryRenameResponse.conversation:type_name -> liveagent.gateway.v1.ConversationSummary
+ 35, // 100: liveagent.gateway.v1.HistoryPinResponse.conversation:type_name -> liveagent.gateway.v1.ConversationSummary
+ 42, // 101: liveagent.gateway.v1.HistoryShareGetResponse.share:type_name -> liveagent.gateway.v1.HistoryShareStatus
+ 42, // 102: liveagent.gateway.v1.HistoryShareSetResponse.share:type_name -> liveagent.gateway.v1.HistoryShareStatus
+ 35, // 103: liveagent.gateway.v1.HistoryShareResolveResponse.conversation:type_name -> liveagent.gateway.v1.ConversationSummary
+ 50, // 104: liveagent.gateway.v1.HistoryWorkdirsResponse.workdirs:type_name -> liveagent.gateway.v1.HistoryWorkdirSummary
+ 35, // 105: liveagent.gateway.v1.HistoryTruncateResponse.conversation:type_name -> liveagent.gateway.v1.ConversationSummary
+ 35, // 106: liveagent.gateway.v1.HistorySyncEvent.conversation:type_name -> liveagent.gateway.v1.ConversationSummary
+ 73, // 107: liveagent.gateway.v1.FileMentionListResponse.entries:type_name -> liveagent.gateway.v1.FileMentionEntry
+ 75, // 108: liveagent.gateway.v1.FsRootsResponse.roots:type_name -> liveagent.gateway.v1.FsRoot
+ 79, // 109: liveagent.gateway.v1.FsListDirsResponse.entries:type_name -> liveagent.gateway.v1.FsDirEntry
+ 84, // 110: liveagent.gateway.v1.FsListResponse.entries:type_name -> liveagent.gateway.v1.FsListEntry
+ 5, // 111: liveagent.gateway.v1.AgentGateway.AgentConnect:input_type -> liveagent.gateway.v1.AgentEnvelope
+ 2, // 112: liveagent.gateway.v1.AgentGateway.Authenticate:input_type -> liveagent.gateway.v1.AuthRequest
+ 4, // 113: liveagent.gateway.v1.AgentGateway.AgentConnect:output_type -> liveagent.gateway.v1.GatewayEnvelope
+ 3, // 114: liveagent.gateway.v1.AgentGateway.Authenticate:output_type -> liveagent.gateway.v1.AuthResponse
+ 113, // [113:115] is the sub-list for method output_type
+ 111, // [111:113] is the sub-list for method input_type
+ 111, // [111:111] is the sub-list for extension type_name
+ 111, // [111:111] is the sub-list for extension extendee
+ 0, // [0:111] is the sub-list for field type_name
}
func init() { file_proto_v1_gateway_proto_init() }
@@ -7662,6 +8420,9 @@ func file_proto_v1_gateway_proto_init() {
(*GatewayEnvelope_GitRequest)(nil),
(*GatewayEnvelope_FsReadEditableText)(nil),
(*GatewayEnvelope_FsReadWorkspaceImage)(nil),
+ (*GatewayEnvelope_TunnelControl)(nil),
+ (*GatewayEnvelope_TunnelControlResp)(nil),
+ (*GatewayEnvelope_TunnelFrame)(nil),
}
file_proto_v1_gateway_proto_msgTypes[3].OneofWrappers = []any{
(*AgentEnvelope_ChatEvent)(nil),
@@ -7703,16 +8464,19 @@ func file_proto_v1_gateway_proto_init() {
(*AgentEnvelope_GitResponse)(nil),
(*AgentEnvelope_FsReadEditableTextResp)(nil),
(*AgentEnvelope_FsReadWorkspaceImageResp)(nil),
+ (*AgentEnvelope_TunnelControl)(nil),
+ (*AgentEnvelope_TunnelControlResp)(nil),
+ (*AgentEnvelope_TunnelFrame)(nil),
(*AgentEnvelope_Error)(nil),
}
- file_proto_v1_gateway_proto_msgTypes[38].OneofWrappers = []any{}
+ file_proto_v1_gateway_proto_msgTypes[43].OneofWrappers = []any{}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_v1_gateway_proto_rawDesc), len(file_proto_v1_gateway_proto_rawDesc)),
- NumEnums: 1,
- NumMessages: 94,
+ NumEnums: 2,
+ NumMessages: 99,
NumExtensions: 0,
NumServices: 1,
},
diff --git a/crates/agent-gateway/internal/server/grpc.go b/crates/agent-gateway/internal/server/grpc.go
index a51f0a494..9234b0970 100644
--- a/crates/agent-gateway/internal/server/grpc.go
+++ b/crates/agent-gateway/internal/server/grpc.go
@@ -59,6 +59,13 @@ func (s *GRPCServer) AgentConnect(stream gatewayv1.AgentGateway_AgentConnectServ
defer cancel()
go s.heartbeatLoop(ctx, sess)
+ go func() {
+ select {
+ case <-ctx.Done():
+ case <-sess.Done():
+ cancel()
+ }
+ }()
sendErrCh := make(chan error, 1)
go func() {
@@ -71,12 +78,23 @@ func (s *GRPCServer) AgentConnect(stream gatewayv1.AgentGateway_AgentConnectServ
sendErrCh <- nil
cancel()
return
- case env := <-toAgent:
- if err := stream.Send(env); err != nil {
+ case outbound := <-toAgent:
+ if outbound == nil || outbound.GatewayEnvelope == nil {
+ continue
+ }
+ select {
+ case <-outbound.Context().Done():
+ outbound.Ack(outbound.Context().Err())
+ continue
+ default:
+ }
+ if err := stream.Send(outbound.GatewayEnvelope); err != nil {
+ outbound.Ack(err)
sendErrCh <- err
cancel()
return
}
+ outbound.Ack(nil)
}
}
}()
diff --git a/crates/agent-gateway/internal/server/http.go b/crates/agent-gateway/internal/server/http.go
index 89e96eebb..db8124167 100644
--- a/crates/agent-gateway/internal/server/http.go
+++ b/crates/agent-gateway/internal/server/http.go
@@ -24,6 +24,7 @@ func NewHTTPServer(cfg *config.Config, sm *session.Manager) http.Handler {
rootMux := http.NewServeMux()
rootMux.HandleFunc("GET /healthz", handler.Health())
rootMux.Handle("/ws", NewWebSocketServer(cfg, sm))
+ rootMux.HandleFunc("/t/", publicTunnelProxy(sm))
rootMux.HandleFunc("GET /image-proxy", handler.ImageProxy(cfg.RequestTimeout))
rootMux.HandleFunc("GET /api/public/history-shares/{token}", publicHistoryShare(cfg, sm))
diff --git a/crates/agent-gateway/internal/server/http_test.go b/crates/agent-gateway/internal/server/http_test.go
index 2f780b98d..0e28201a6 100644
--- a/crates/agent-gateway/internal/server/http_test.go
+++ b/crates/agent-gateway/internal/server/http_test.go
@@ -69,7 +69,9 @@ func TestPublicHistoryShareResolvesWithoutAuthorization(t *testing.T) {
var outbound *gatewayv1.GatewayEnvelope
select {
- case outbound = <-agentSession.Outbound():
+ case delivered := <-agentSession.Outbound():
+ delivered.Ack(nil)
+ outbound = delivered.GatewayEnvelope
case <-time.After(time.Second):
t.Fatal("timed out waiting for public share request")
}
@@ -164,7 +166,9 @@ func publicHistoryShareErrorStatusForTest(t *testing.T, code int, message string
var outbound *gatewayv1.GatewayEnvelope
select {
- case outbound = <-agentSession.Outbound():
+ case delivered := <-agentSession.Outbound():
+ delivered.Ack(nil)
+ outbound = delivered.GatewayEnvelope
case <-time.After(time.Second):
t.Fatal("timed out waiting for public share request")
}
diff --git a/crates/agent-gateway/internal/server/tunnel.go b/crates/agent-gateway/internal/server/tunnel.go
new file mode 100644
index 000000000..33fbc97ab
--- /dev/null
+++ b/crates/agent-gateway/internal/server/tunnel.go
@@ -0,0 +1,722 @@
+package server
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
+ "unicode/utf8"
+
+ "github.com/google/uuid"
+ gatewayv1 "github.com/liveagent/agent-gateway/internal/proto/v1"
+ "github.com/liveagent/agent-gateway/internal/session"
+ "golang.org/x/net/websocket"
+)
+
+const tunnelBodyChunkSize = 64 * 1024
+
+func publicTunnelProxy(sm *session.Manager) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if slug, ok := parseTunnelPublicPathWithoutTrailingSlash(r.URL.Path); ok {
+ target := "/t/" + slug + "/"
+ if r.URL.RawQuery != "" {
+ target += "?" + r.URL.RawQuery
+ }
+ http.Redirect(w, r, target, http.StatusPermanentRedirect)
+ return
+ }
+
+ slug, restPath, ok := parseTunnelPublicPath(r.URL.Path)
+ if !ok {
+ writeTunnelError(w, http.StatusNotFound, "tunnel not found")
+ return
+ }
+ if r.URL.RawQuery != "" {
+ restPath += "?" + r.URL.RawQuery
+ }
+
+ if isWebSocketUpgrade(r) {
+ serveTunnelWebSocket(w, r, sm, slug, restPath)
+ return
+ }
+ serveTunnelHTTP(w, r, sm, slug, restPath)
+ }
+}
+
+func serveTunnelHTTP(
+ w http.ResponseWriter,
+ r *http.Request,
+ sm *session.Manager,
+ slug string,
+ restPath string,
+) {
+ streamID := "http-" + uuid.NewString()
+ lease, err := sm.AcquireTunnel(slug, streamID)
+ if err != nil {
+ writeTunnelAcquireError(w, err)
+ return
+ }
+
+ ctx, cancel := context.WithCancel(r.Context())
+ completed := false
+ defer func() {
+ cancel()
+ if !completed {
+ _ = sm.SendTunnelFrameToAgent(&gatewayv1.TunnelFrame{
+ StreamId: streamID,
+ TunnelId: lease.TunnelID(),
+ Slug: slug,
+ Kind: gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_CANCEL,
+ })
+ }
+ lease.Release()
+ }()
+
+ start := &gatewayv1.TunnelFrame{
+ StreamId: streamID,
+ TunnelId: lease.TunnelID(),
+ Slug: slug,
+ Kind: gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_REQUEST_START,
+ Method: r.Method,
+ Path: restPath,
+ Headers: filteredTunnelRequestHeaders(r.Header),
+ }
+ if err := sm.SendTunnelFrameToAgent(start); err != nil {
+ writeTunnelAcquireError(w, err)
+ return
+ }
+
+ bodyDone := make(chan struct{})
+ go streamTunnelHTTPRequestBody(ctx, sm, lease.TunnelID(), slug, streamID, r.Body, bodyDone)
+
+ responseStarted := false
+ responseHeadersWritten := false
+ responseStatus := http.StatusOK
+ responseHeaders := http.Header{}
+ responseRewriteKind := tunnelResponseRewriteNone
+ var responseBody []byte
+ writeResponseHeaders := func() {
+ if responseHeadersWritten {
+ return
+ }
+ writeTunnelHTTPHeaders(w, responseHeaders)
+ w.WriteHeader(responseStatus)
+ responseHeadersWritten = true
+ }
+ writeBufferedResponse := func() {
+ if responseRewriteKind == tunnelResponseRewriteNone {
+ return
+ }
+ writeResponseHeaders()
+ if len(responseBody) > 0 {
+ _, _ = w.Write(responseBody)
+ responseBody = nil
+ }
+ flushTunnelResponse(w)
+ }
+ for {
+ select {
+ case <-r.Context().Done():
+ return
+ case <-lease.Done():
+ if !responseStarted {
+ writeTunnelError(w, http.StatusBadGateway, "tunnel stream closed")
+ } else if !responseHeadersWritten {
+ writeBufferedResponse()
+ }
+ return
+ case <-bodyDone:
+ bodyDone = nil
+ case frame := <-lease.Frames():
+ if frame == nil {
+ continue
+ }
+ switch frame.GetKind() {
+ case gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_RESPONSE_START:
+ if responseStarted {
+ continue
+ }
+ responseStarted = true
+ status := int(frame.GetStatusCode())
+ if status <= 0 {
+ status = http.StatusOK
+ }
+ responseStatus = status
+ responseHeaders = tunnelResponseHeaders(frame, lease.Tunnel())
+ responseRewriteKind = tunnelResponseRewriteKindFor(r.Method, responseStatus, responseHeaders)
+ if responseRewriteKind == tunnelResponseRewriteNone {
+ writeResponseHeaders()
+ flushTunnelResponse(w)
+ }
+ case gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_RESPONSE_BODY:
+ if !responseStarted {
+ responseStarted = true
+ responseStatus = http.StatusOK
+ responseHeaders = http.Header{}
+ responseRewriteKind = tunnelResponseRewriteNone
+ writeResponseHeaders()
+ }
+ if body := frame.GetBody(); len(body) > 0 {
+ if responseRewriteKind != tunnelResponseRewriteNone {
+ if len(responseBody)+len(body) <= tunnelRewriteBodyMaxBytes {
+ responseBody = append(responseBody, body...)
+ continue
+ }
+ responseRewriteKind = tunnelResponseRewriteNone
+ writeResponseHeaders()
+ if len(responseBody) > 0 {
+ if _, err := w.Write(responseBody); err != nil {
+ return
+ }
+ responseBody = nil
+ }
+ }
+ writeResponseHeaders()
+ if _, err := w.Write(body); err != nil {
+ return
+ }
+ flushTunnelResponse(w)
+ }
+ case gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_RESPONSE_END:
+ if responseRewriteKind != tunnelResponseRewriteNone {
+ body := responseBody
+ if rewritten, changed := rewriteTunnelResponseBody(body, lease.Tunnel(), responseRewriteKind); changed {
+ body = rewritten
+ responseHeaders.Del("Content-Length")
+ responseHeaders.Del("Etag")
+ responseHeaders.Del("ETag")
+ }
+ writeResponseHeaders()
+ if len(body) > 0 {
+ if _, err := w.Write(body); err != nil {
+ return
+ }
+ }
+ responseBody = nil
+ } else if responseStarted && !responseHeadersWritten {
+ writeResponseHeaders()
+ }
+ completed = true
+ flushTunnelResponse(w)
+ return
+ case gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_ERROR:
+ if !responseStarted {
+ writeTunnelError(w, http.StatusBadGateway, frame.GetError())
+ } else if !responseHeadersWritten {
+ writeBufferedResponse()
+ }
+ return
+ case gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_CANCEL:
+ if !responseStarted {
+ writeTunnelError(w, http.StatusServiceUnavailable, "tunnel canceled")
+ } else if !responseHeadersWritten {
+ writeBufferedResponse()
+ }
+ return
+ }
+ }
+ }
+}
+
+func serveTunnelWebSocket(
+ w http.ResponseWriter,
+ r *http.Request,
+ sm *session.Manager,
+ slug string,
+ restPath string,
+) {
+ streamID := "ws-" + uuid.NewString()
+ lease, err := sm.AcquireTunnel(slug, streamID)
+ if err != nil {
+ writeTunnelAcquireError(w, err)
+ return
+ }
+ handlerStarted := false
+ handler := websocket.Handler(func(ws *websocket.Conn) {
+ handlerStarted = true
+ defer lease.Release()
+ handleTunnelWebSocket(ws, r, sm, lease, slug, streamID, restPath)
+ })
+ handler.ServeHTTP(w, r)
+ if !handlerStarted {
+ lease.Release()
+ }
+}
+
+func handleTunnelWebSocket(
+ ws *websocket.Conn,
+ r *http.Request,
+ sm *session.Manager,
+ lease *session.TunnelStreamLease,
+ slug string,
+ streamID string,
+ restPath string,
+) {
+ ws.MaxPayloadBytes = 16 * 1024 * 1024
+ closed := false
+ defer func() {
+ if !closed {
+ _ = sm.SendTunnelFrameToAgent(&gatewayv1.TunnelFrame{
+ StreamId: streamID,
+ TunnelId: lease.TunnelID(),
+ Slug: slug,
+ Kind: gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_WS_CLOSE,
+ })
+ }
+ _ = ws.Close()
+ }()
+
+ if err := sm.SendTunnelFrameToAgent(&gatewayv1.TunnelFrame{
+ StreamId: streamID,
+ TunnelId: lease.TunnelID(),
+ Slug: slug,
+ Kind: gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_WS_OPEN,
+ Method: r.Method,
+ Path: restPath,
+ Headers: filteredTunnelRequestHeaders(r.Header),
+ }); err != nil {
+ return
+ }
+
+ if !awaitTunnelWebSocketOpen(ws, lease) {
+ closed = true
+ return
+ }
+
+ browserFrames := make(chan *gatewayv1.TunnelFrame, 64)
+ readerDone := make(chan struct{})
+ go func() {
+ defer close(readerDone)
+ for {
+ var body []byte
+ if err := websocket.Message.Receive(ws, &body); err != nil {
+ return
+ }
+ messageType := "binary"
+ if utf8.Valid(body) {
+ messageType = "text"
+ }
+ frame := &gatewayv1.TunnelFrame{
+ StreamId: streamID,
+ TunnelId: lease.TunnelID(),
+ Slug: slug,
+ Kind: gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_WS_FRAME,
+ Body: body,
+ WsMessageType: messageType,
+ }
+ select {
+ case browserFrames <- frame:
+ case <-lease.Done():
+ return
+ }
+ }
+ }()
+
+ for {
+ select {
+ case <-lease.Done():
+ closed = true
+ return
+ case <-readerDone:
+ closed = true
+ _ = sm.SendTunnelFrameToAgent(&gatewayv1.TunnelFrame{
+ StreamId: streamID,
+ TunnelId: lease.TunnelID(),
+ Slug: slug,
+ Kind: gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_WS_CLOSE,
+ })
+ return
+ case frame := <-browserFrames:
+ if frame != nil {
+ _ = sm.SendTunnelFrameToAgent(frame)
+ }
+ case frame := <-lease.Frames():
+ if frame == nil {
+ continue
+ }
+ switch frame.GetKind() {
+ case gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_WS_FRAME:
+ if strings.EqualFold(frame.GetWsMessageType(), "text") {
+ if err := websocket.Message.Send(ws, string(frame.GetBody())); err != nil {
+ return
+ }
+ } else {
+ if err := websocket.Message.Send(ws, frame.GetBody()); err != nil {
+ return
+ }
+ }
+ case gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_WS_CLOSE,
+ gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_CANCEL,
+ gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_ERROR:
+ closed = true
+ return
+ }
+ }
+ }
+}
+
+func awaitTunnelWebSocketOpen(ws *websocket.Conn, lease *session.TunnelStreamLease) bool {
+ timer := time.NewTimer(30 * time.Second)
+ defer timer.Stop()
+ for {
+ select {
+ case <-timer.C:
+ return false
+ case <-lease.Done():
+ return false
+ case frame := <-lease.Frames():
+ if frame == nil {
+ continue
+ }
+ switch frame.GetKind() {
+ case gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_WS_OPEN:
+ return true
+ case gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_ERROR,
+ gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_CANCEL,
+ gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_WS_CLOSE:
+ if frame.GetError() != "" {
+ _ = websocket.Message.Send(ws, frame.GetError())
+ }
+ return false
+ }
+ }
+ }
+}
+
+func streamTunnelHTTPRequestBody(
+ ctx context.Context,
+ sm *session.Manager,
+ tunnelID string,
+ slug string,
+ streamID string,
+ body io.ReadCloser,
+ done chan<- struct{},
+) {
+ defer close(done)
+ defer body.Close()
+
+ buffer := make([]byte, tunnelBodyChunkSize)
+ for {
+ n, err := body.Read(buffer)
+ if n > 0 {
+ chunk := make([]byte, n)
+ copy(chunk, buffer[:n])
+ if sendErr := sm.SendTunnelFrameToAgent(&gatewayv1.TunnelFrame{
+ StreamId: streamID,
+ TunnelId: tunnelID,
+ Slug: slug,
+ Kind: gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_REQUEST_BODY,
+ Body: chunk,
+ }); sendErr != nil {
+ return
+ }
+ }
+ if errors.Is(err, io.EOF) {
+ _ = sm.SendTunnelFrameToAgent(&gatewayv1.TunnelFrame{
+ StreamId: streamID,
+ TunnelId: tunnelID,
+ Slug: slug,
+ Kind: gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_REQUEST_END,
+ })
+ return
+ }
+ if err != nil {
+ _ = sm.SendTunnelFrameToAgent(&gatewayv1.TunnelFrame{
+ StreamId: streamID,
+ TunnelId: tunnelID,
+ Slug: slug,
+ Kind: gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_CANCEL,
+ Error: err.Error(),
+ })
+ return
+ }
+ select {
+ case <-ctx.Done():
+ return
+ default:
+ }
+ }
+}
+
+func parseTunnelPublicPath(rawPath string) (string, string, bool) {
+ if !strings.HasPrefix(rawPath, "/t/") {
+ return "", "", false
+ }
+ trimmed := strings.TrimPrefix(rawPath, "/t/")
+ parts := strings.SplitN(trimmed, "/", 2)
+ slug := strings.TrimSpace(parts[0])
+ if slug == "" {
+ return "", "", false
+ }
+ if len(parts) == 1 || parts[1] == "" {
+ return slug, "/", true
+ }
+ return slug, "/" + parts[1], true
+}
+
+func parseTunnelPublicPathWithoutTrailingSlash(rawPath string) (string, bool) {
+ if !strings.HasPrefix(rawPath, "/t/") {
+ return "", false
+ }
+ trimmed := strings.TrimPrefix(rawPath, "/t/")
+ if trimmed == "" || strings.Contains(trimmed, "/") {
+ return "", false
+ }
+ return strings.TrimSpace(trimmed), strings.TrimSpace(trimmed) != ""
+}
+
+func writeTunnelAcquireError(w http.ResponseWriter, err error) {
+ switch {
+ case errors.Is(err, session.ErrTunnelNotFound), errors.Is(err, session.ErrTunnelExpired):
+ writeTunnelError(w, http.StatusNotFound, "tunnel not found")
+ case errors.Is(err, session.ErrAgentOffline):
+ writeTunnelError(w, http.StatusServiceUnavailable, "agent offline")
+ case errors.Is(err, session.ErrTunnelOverLimit):
+ writeTunnelError(w, http.StatusTooManyRequests, "tunnel connection limit exceeded")
+ default:
+ writeTunnelError(w, http.StatusBadGateway, err.Error())
+ }
+}
+
+func writeTunnelError(w http.ResponseWriter, status int, message string) {
+ message = strings.TrimSpace(message)
+ if message == "" {
+ message = http.StatusText(status)
+ }
+ w.Header().Set("Content-Type", "text/plain; charset=utf-8")
+ w.WriteHeader(status)
+ _, _ = w.Write([]byte(message))
+}
+
+func isWebSocketUpgrade(r *http.Request) bool {
+ return strings.EqualFold(r.Header.Get("Upgrade"), "websocket") &&
+ strings.Contains(strings.ToLower(r.Header.Get("Connection")), "upgrade")
+}
+
+func filteredTunnelRequestHeaders(headers http.Header) []*gatewayv1.TunnelHeader {
+ return filteredTunnelHeaders(headers, true)
+}
+
+func filteredTunnelResponseHeaders(headers []*gatewayv1.TunnelHeader) http.Header {
+ out := http.Header{}
+ for _, header := range headers {
+ name := http.CanonicalHeaderKey(strings.TrimSpace(header.GetName()))
+ if name == "" || shouldDropTunnelHeader(name, false) {
+ continue
+ }
+ out.Add(name, header.GetValue())
+ }
+ return out
+}
+
+func filteredTunnelHeaders(headers http.Header, request bool) []*gatewayv1.TunnelHeader {
+ out := make([]*gatewayv1.TunnelHeader, 0, len(headers))
+ for name, values := range headers {
+ canonical := http.CanonicalHeaderKey(strings.TrimSpace(name))
+ if canonical == "" || shouldDropTunnelHeader(canonical, request) {
+ continue
+ }
+ for _, value := range values {
+ out = append(out, &gatewayv1.TunnelHeader{
+ Name: canonical,
+ Value: value,
+ })
+ }
+ }
+ return out
+}
+
+func shouldDropTunnelHeader(name string, request bool) bool {
+ switch strings.ToLower(name) {
+ case "connection",
+ "keep-alive",
+ "proxy-authenticate",
+ "proxy-authorization",
+ "proxy-connection",
+ "te",
+ "trailer",
+ "transfer-encoding",
+ "upgrade":
+ return true
+ case "host":
+ return request
+ default:
+ return false
+ }
+}
+
+func writeTunnelResponseHeaders(
+ w http.ResponseWriter,
+ frame *gatewayv1.TunnelFrame,
+ tunnel *gatewayv1.TunnelSummary,
+) {
+ writeTunnelHTTPHeaders(w, tunnelResponseHeaders(frame, tunnel))
+}
+
+func tunnelResponseHeaders(
+ frame *gatewayv1.TunnelFrame,
+ tunnel *gatewayv1.TunnelSummary,
+) http.Header {
+ headers := filteredTunnelResponseHeaders(frame.GetHeaders())
+ for name, values := range headers {
+ rewritten := make([]string, 0, len(values))
+ for _, value := range values {
+ if strings.EqualFold(name, "Location") {
+ value = rewriteTunnelLocation(value, tunnel)
+ }
+ if strings.EqualFold(name, "Set-Cookie") {
+ value = rewriteTunnelSetCookiePath(value, tunnel)
+ }
+ rewritten = append(rewritten, value)
+ }
+ headers[name] = rewritten
+ }
+ return headers
+}
+
+func writeTunnelHTTPHeaders(w http.ResponseWriter, headers http.Header) {
+ for name, values := range headers {
+ for _, value := range values {
+ w.Header().Add(name, value)
+ }
+ }
+}
+
+func flushTunnelResponse(w http.ResponseWriter) {
+ if flusher, ok := w.(http.Flusher); ok {
+ flusher.Flush()
+ }
+}
+
+func rewriteTunnelLocation(value string, tunnel *gatewayv1.TunnelSummary) string {
+ if tunnel == nil {
+ return value
+ }
+ target, err := url.Parse(tunnel.GetTargetUrl())
+ if err != nil || target.Host == "" {
+ return value
+ }
+ publicPrefix := "/t/" + tunnel.GetSlug()
+ parsed, err := url.Parse(value)
+ if err != nil {
+ return value
+ }
+ if parsed.IsAbs() {
+ if !strings.EqualFold(parsed.Scheme, target.Scheme) || !strings.EqualFold(parsed.Host, target.Host) {
+ return value
+ }
+ path := stripTunnelTargetBasePath(parsed.EscapedPath(), target.EscapedPath())
+ if path == "" {
+ path = "/"
+ }
+ if parsed.RawQuery != "" {
+ path += "?" + parsed.RawQuery
+ }
+ if parsed.Fragment != "" {
+ path += "#" + parsed.EscapedFragment()
+ }
+ return publicPrefix + path
+ }
+ if strings.HasPrefix(value, "/") {
+ path := stripTunnelTargetBasePath(parsed.EscapedPath(), target.EscapedPath())
+ if path == "" {
+ path = "/"
+ }
+ if parsed.RawQuery != "" {
+ path += "?" + parsed.RawQuery
+ }
+ if parsed.Fragment != "" {
+ path += "#" + parsed.EscapedFragment()
+ }
+ return publicPrefix + path
+ }
+ return value
+}
+
+func rewriteTunnelSetCookiePath(value string, tunnel *gatewayv1.TunnelSummary) string {
+ if tunnel == nil || tunnel.GetSlug() == "" {
+ return value
+ }
+ parts := strings.Split(value, ";")
+ targetBasePath := "/"
+ if target, err := url.Parse(tunnel.GetTargetUrl()); err == nil {
+ targetBasePath = target.EscapedPath()
+ }
+ for index, part := range parts {
+ trimmed := strings.TrimSpace(part)
+ if !strings.HasPrefix(strings.ToLower(trimmed), "path=") {
+ continue
+ }
+ cookiePath := strings.TrimSpace(trimmed[len("path="):])
+ if cookiePath == "" {
+ cookiePath = "/"
+ }
+ rest := stripTunnelTargetBasePath(cookiePath, targetBasePath)
+ if rest == "" {
+ rest = "/"
+ }
+ prefix := ""
+ if leading := len(part) - len(strings.TrimLeft(part, " \t")); leading > 0 {
+ prefix = part[:leading]
+ }
+ parts[index] = fmt.Sprintf("%sPath=/t/%s%s", prefix, tunnel.GetSlug(), rest)
+ }
+ return strings.Join(parts, ";")
+}
+
+func stripTunnelTargetBasePath(pathValue string, basePath string) string {
+ pathValue = normalizeTunnelPath(pathValue)
+ basePath = normalizeTunnelPath(basePath)
+ if basePath == "/" {
+ return pathValue
+ }
+ if pathValue == basePath {
+ return "/"
+ }
+ if strings.HasPrefix(pathValue, strings.TrimRight(basePath, "/")+"/") {
+ return strings.TrimPrefix(pathValue, strings.TrimRight(basePath, "/"))
+ }
+ return pathValue
+}
+
+func normalizeTunnelPath(value string) string {
+ value = strings.TrimSpace(value)
+ if value == "" {
+ return "/"
+ }
+ if !strings.HasPrefix(value, "/") {
+ value = "/" + value
+ }
+ return value
+}
+
+func publicBaseURLFromHTTPRequest(r *http.Request) string {
+ if r == nil {
+ return ""
+ }
+ scheme := forwardedHeaderFirst(r.Header.Get("X-Forwarded-Proto"))
+ if scheme == "" {
+ if r.TLS != nil {
+ scheme = "https"
+ } else {
+ scheme = "http"
+ }
+ }
+ host := forwardedHeaderFirst(r.Header.Get("X-Forwarded-Host"))
+ if host == "" {
+ host = r.Host
+ }
+ if host == "" {
+ return ""
+ }
+ return scheme + "://" + host
+}
+
+func forwardedHeaderFirst(value string) string {
+ first := strings.TrimSpace(strings.Split(value, ",")[0])
+ return strings.TrimSpace(first)
+}
diff --git a/crates/agent-gateway/internal/server/tunnel_rewrite.go b/crates/agent-gateway/internal/server/tunnel_rewrite.go
new file mode 100644
index 000000000..b6b743be5
--- /dev/null
+++ b/crates/agent-gateway/internal/server/tunnel_rewrite.go
@@ -0,0 +1,229 @@
+package server
+
+import (
+ "mime"
+ "net/http"
+ "net/url"
+ "regexp"
+ "strings"
+ "unicode/utf8"
+
+ gatewayv1 "github.com/liveagent/agent-gateway/internal/proto/v1"
+)
+
+const tunnelRewriteBodyMaxBytes = 4 * 1024 * 1024
+
+type tunnelResponseRewriteKind int
+
+const (
+ tunnelResponseRewriteNone tunnelResponseRewriteKind = iota
+ tunnelResponseRewriteHTML
+ tunnelResponseRewriteCSS
+ tunnelResponseRewriteJavaScript
+)
+
+var (
+ tunnelHTMLQuotedAttrURLPattern = regexp.MustCompile(`(?i)(\b(?:href|src|action|poster|data|formaction)\s*=\s*)(["'])([^"']+)(["'])`)
+ tunnelHTMLBareAttrURLPattern = regexp.MustCompile(`(?i)(\b(?:href|src|action|poster|data|formaction)\s*=\s*)([^\s"'<>]+)`)
+ tunnelJSQuotedURLPattern = regexp.MustCompile(`(["'])(/[^"'\\]*)(["'])`)
+ tunnelCSSURLPattern = regexp.MustCompile(`(?i)(url\(\s*)(["']?)([^"')]+)(["']?\s*\))`)
+)
+
+func tunnelResponseRewriteKindFor(
+ method string,
+ status int,
+ headers http.Header,
+) tunnelResponseRewriteKind {
+ if strings.EqualFold(strings.TrimSpace(method), http.MethodHead) {
+ return tunnelResponseRewriteNone
+ }
+ if status < http.StatusOK ||
+ status == http.StatusNoContent ||
+ status == http.StatusNotModified {
+ return tunnelResponseRewriteNone
+ }
+ if strings.TrimSpace(headers.Get("Content-Encoding")) != "" {
+ return tunnelResponseRewriteNone
+ }
+
+ contentType := strings.TrimSpace(headers.Get("Content-Type"))
+ if contentType == "" {
+ return tunnelResponseRewriteNone
+ }
+ mediaType, _, err := mime.ParseMediaType(contentType)
+ if err != nil {
+ mediaType = contentType
+ }
+ mediaType = strings.ToLower(strings.TrimSpace(mediaType))
+
+ switch {
+ case mediaType == "text/html" || mediaType == "application/xhtml+xml":
+ return tunnelResponseRewriteHTML
+ case mediaType == "text/css":
+ return tunnelResponseRewriteCSS
+ case mediaType == "text/javascript",
+ mediaType == "application/javascript",
+ mediaType == "application/x-javascript",
+ mediaType == "text/ecmascript",
+ mediaType == "application/ecmascript",
+ strings.HasSuffix(mediaType, "+javascript"):
+ return tunnelResponseRewriteJavaScript
+ default:
+ return tunnelResponseRewriteNone
+ }
+}
+
+func rewriteTunnelResponseBody(
+ body []byte,
+ tunnel *gatewayv1.TunnelSummary,
+ kind tunnelResponseRewriteKind,
+) ([]byte, bool) {
+ if len(body) == 0 || kind == tunnelResponseRewriteNone || tunnelPublicPathPrefix(tunnel) == "" {
+ return body, false
+ }
+ if !utf8.Valid(body) {
+ return body, false
+ }
+
+ original := string(body)
+ rewritten := original
+ switch kind {
+ case tunnelResponseRewriteHTML:
+ rewritten = rewriteTunnelHTMLBody(rewritten, tunnel)
+ case tunnelResponseRewriteCSS:
+ rewritten = rewriteTunnelCSSBody(rewritten, tunnel)
+ case tunnelResponseRewriteJavaScript:
+ rewritten = rewriteTunnelJavaScriptBody(rewritten, tunnel)
+ }
+ if rewritten == original {
+ return body, false
+ }
+ return []byte(rewritten), true
+}
+
+func rewriteTunnelHTMLBody(input string, tunnel *gatewayv1.TunnelSummary) string {
+ input = tunnelHTMLQuotedAttrURLPattern.ReplaceAllStringFunc(input, func(match string) string {
+ parts := tunnelHTMLQuotedAttrURLPattern.FindStringSubmatch(match)
+ if len(parts) != 5 {
+ return match
+ }
+ rewritten := rewriteTunnelBodyURL(parts[3], tunnel)
+ if rewritten == parts[3] {
+ return match
+ }
+ return parts[1] + parts[2] + rewritten + parts[4]
+ })
+
+ return tunnelHTMLBareAttrURLPattern.ReplaceAllStringFunc(input, func(match string) string {
+ parts := tunnelHTMLBareAttrURLPattern.FindStringSubmatch(match)
+ if len(parts) != 3 {
+ return match
+ }
+ rewritten := rewriteTunnelBodyURL(parts[2], tunnel)
+ if rewritten == parts[2] {
+ return match
+ }
+ return parts[1] + rewritten
+ })
+}
+
+func rewriteTunnelCSSBody(input string, tunnel *gatewayv1.TunnelSummary) string {
+ return tunnelCSSURLPattern.ReplaceAllStringFunc(input, func(match string) string {
+ parts := tunnelCSSURLPattern.FindStringSubmatch(match)
+ if len(parts) != 5 {
+ return match
+ }
+ rewritten := rewriteTunnelBodyURL(strings.TrimSpace(parts[3]), tunnel)
+ if rewritten == strings.TrimSpace(parts[3]) {
+ return match
+ }
+ return parts[1] + parts[2] + rewritten + parts[4]
+ })
+}
+
+func rewriteTunnelJavaScriptBody(input string, tunnel *gatewayv1.TunnelSummary) string {
+ return tunnelJSQuotedURLPattern.ReplaceAllStringFunc(input, func(match string) string {
+ parts := tunnelJSQuotedURLPattern.FindStringSubmatch(match)
+ if len(parts) != 4 || parts[1] != parts[3] {
+ return match
+ }
+ rewritten := rewriteTunnelBodyURL(parts[2], tunnel)
+ if rewritten == parts[2] {
+ return match
+ }
+ return parts[1] + rewritten + parts[1]
+ })
+}
+
+func rewriteTunnelBodyURL(value string, tunnel *gatewayv1.TunnelSummary) string {
+ prefix := tunnelPublicPathPrefix(tunnel)
+ if prefix == "" {
+ return value
+ }
+ trimmed := strings.TrimSpace(value)
+ if trimmed == "" ||
+ strings.HasPrefix(trimmed, "#") ||
+ strings.HasPrefix(trimmed, "//") {
+ return value
+ }
+
+ parsed, err := url.Parse(trimmed)
+ if err != nil {
+ return value
+ }
+ target, targetErr := url.Parse(tunnel.GetTargetUrl())
+ if parsed.IsAbs() {
+ if targetErr != nil || target.Host == "" {
+ return value
+ }
+ if !strings.EqualFold(parsed.Scheme, target.Scheme) ||
+ !strings.EqualFold(parsed.Host, target.Host) {
+ return value
+ }
+ path := stripTunnelTargetBasePath(parsed.EscapedPath(), target.EscapedPath())
+ return appendTunnelURLQueryAndFragment(prefix+pathOrRoot(path), parsed)
+ }
+ if !strings.HasPrefix(trimmed, "/") {
+ return value
+ }
+ if trimmed == prefix || strings.HasPrefix(trimmed, prefix+"/") {
+ return value
+ }
+
+ path := parsed.EscapedPath()
+ if targetErr == nil && target.Host != "" {
+ path = stripTunnelTargetBasePath(path, target.EscapedPath())
+ }
+ return appendTunnelURLQueryAndFragment(prefix+pathOrRoot(path), parsed)
+}
+
+func tunnelPublicPathPrefix(tunnel *gatewayv1.TunnelSummary) string {
+ if tunnel == nil {
+ return ""
+ }
+ slug := strings.TrimSpace(tunnel.GetSlug())
+ if slug == "" {
+ return ""
+ }
+ return "/t/" + slug
+}
+
+func pathOrRoot(path string) string {
+ if strings.TrimSpace(path) == "" {
+ return "/"
+ }
+ return path
+}
+
+func appendTunnelURLQueryAndFragment(path string, parsed *url.URL) string {
+ if parsed == nil {
+ return path
+ }
+ if parsed.RawQuery != "" {
+ path += "?" + parsed.RawQuery
+ }
+ if parsed.Fragment != "" {
+ path += "#" + parsed.EscapedFragment()
+ }
+ return path
+}
diff --git a/crates/agent-gateway/internal/server/tunnel_rewrite_test.go b/crates/agent-gateway/internal/server/tunnel_rewrite_test.go
new file mode 100644
index 000000000..3d9d45136
--- /dev/null
+++ b/crates/agent-gateway/internal/server/tunnel_rewrite_test.go
@@ -0,0 +1,269 @@
+package server
+
+import (
+ "net/http"
+ "strings"
+ "testing"
+
+ gatewayv1 "github.com/liveagent/agent-gateway/internal/proto/v1"
+)
+
+func TestTunnelResponseRewriteKindFor(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ name string
+ method string
+ status int
+ headers http.Header
+ want tunnelResponseRewriteKind
+ }{
+ {
+ name: "html",
+ method: http.MethodGet,
+ status: http.StatusOK,
+ headers: http.Header{
+ "Content-Type": []string{"text/html; charset=utf-8"},
+ },
+ want: tunnelResponseRewriteHTML,
+ },
+ {
+ name: "javascript",
+ method: http.MethodGet,
+ status: http.StatusOK,
+ headers: http.Header{
+ "Content-Type": []string{"application/javascript"},
+ },
+ want: tunnelResponseRewriteJavaScript,
+ },
+ {
+ name: "css",
+ method: http.MethodGet,
+ status: http.StatusOK,
+ headers: http.Header{
+ "Content-Type": []string{"text/css"},
+ },
+ want: tunnelResponseRewriteCSS,
+ },
+ {
+ name: "compressed response",
+ method: http.MethodGet,
+ status: http.StatusOK,
+ headers: http.Header{
+ "Content-Type": []string{"text/html; charset=utf-8"},
+ "Content-Encoding": []string{"gzip"},
+ },
+ want: tunnelResponseRewriteNone,
+ },
+ {
+ name: "head request",
+ method: http.MethodHead,
+ status: http.StatusOK,
+ headers: http.Header{
+ "Content-Type": []string{"text/html; charset=utf-8"},
+ },
+ want: tunnelResponseRewriteNone,
+ },
+ {
+ name: "json",
+ method: http.MethodGet,
+ status: http.StatusOK,
+ headers: http.Header{
+ "Content-Type": []string{"application/json"},
+ },
+ want: tunnelResponseRewriteNone,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+
+ if got := tunnelResponseRewriteKindFor(tt.method, tt.status, tt.headers); got != tt.want {
+ t.Fatalf("tunnelResponseRewriteKindFor() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestRewriteTunnelHTMLBodyPrefixesRootRelativeAttributes(t *testing.T) {
+ t.Parallel()
+
+ tunnel := tunnelRewriteTestSummary()
+ input := strings.Join([]string{
+ ``,
+ ``,
+ `
`,
+ `health`,
+ `cdn`,
+ `external`,
+ `already`,
+ `absolute target`,
+ }, "\n")
+
+ body, changed := rewriteTunnelResponseBody([]byte(input), tunnel, tunnelResponseRewriteHTML)
+ if !changed {
+ t.Fatal("rewriteTunnelResponseBody() did not report a change")
+ }
+ output := string(body)
+
+ assertContains(t, output, `href="/t/test-slug/styles.css"`)
+ assertContains(t, output, `src='/t/test-slug/app.js'`)
+ assertContains(t, output, `action=/t/test-slug/api/messages`)
+ assertContains(t, output, `href="/t/test-slug/api/health?check=1#ready"`)
+ assertContains(t, output, `href="//cdn.example.com/lib.js"`)
+ assertContains(t, output, `href="https://example.com/page"`)
+ assertContains(t, output, `href="/t/test-slug/already"`)
+ assertContains(t, output, `href="/t/test-slug/api/showcase"`)
+ assertNotContains(t, output, `/t/test-slug/t/test-slug`)
+}
+
+func TestRewriteTunnelBodyStripsTargetBasePath(t *testing.T) {
+ t.Parallel()
+
+ tunnel := &gatewayv1.TunnelSummary{
+ Slug: "base-slug",
+ TargetUrl: "http://127.0.0.1:3100/app",
+ }
+ input := strings.Join([]string{
+ ``,
+ ``,
+ `root api`,
+ }, "\n")
+
+ body, changed := rewriteTunnelResponseBody([]byte(input), tunnel, tunnelResponseRewriteHTML)
+ if !changed {
+ t.Fatal("rewriteTunnelResponseBody() did not report a change")
+ }
+ output := string(body)
+
+ assertContains(t, output, `src="/t/base-slug/assets/main.js"`)
+ assertContains(t, output, `href="/t/base-slug/styles.css"`)
+ assertContains(t, output, `href="/t/base-slug/api/health"`)
+ assertNotContains(t, output, `/t/base-slug/app/`)
+
+ jsBody, changed := rewriteTunnelResponseBody(
+ []byte(`fetch("/app/api/health?check=1#ready")`),
+ tunnel,
+ tunnelResponseRewriteJavaScript,
+ )
+ if !changed {
+ t.Fatal("rewriteTunnelResponseBody() did not report a JavaScript change")
+ }
+ assertContains(t, string(jsBody), `fetch("/t/base-slug/api/health?check=1#ready")`)
+
+ cssBody, changed := rewriteTunnelResponseBody(
+ []byte(`body { background: url(/app/images/bg.png); }`),
+ tunnel,
+ tunnelResponseRewriteCSS,
+ )
+ if !changed {
+ t.Fatal("rewriteTunnelResponseBody() did not report a CSS change")
+ }
+ assertContains(t, string(cssBody), `url(/t/base-slug/images/bg.png)`)
+}
+
+func TestRewriteTunnelJavaScriptBodyPrefixesRootRelativeStrings(t *testing.T) {
+ t.Parallel()
+
+ tunnel := tunnelRewriteTestSummary()
+ input := strings.Join([]string{
+ `requestJson('/api/showcase')`,
+ `fetch("/api/health?check=1")`,
+ `const root = "/"`,
+ `const external = "https://example.com/api"`,
+ `const cdn = "//cdn.example.com/app.js"`,
+ `const already = "/t/test-slug/api/health"`,
+ }, "\n")
+
+ body, changed := rewriteTunnelResponseBody([]byte(input), tunnel, tunnelResponseRewriteJavaScript)
+ if !changed {
+ t.Fatal("rewriteTunnelResponseBody() did not report a change")
+ }
+ output := string(body)
+
+ assertContains(t, output, `requestJson('/t/test-slug/api/showcase')`)
+ assertContains(t, output, `fetch("/t/test-slug/api/health?check=1")`)
+ assertContains(t, output, `const root = "/t/test-slug/"`)
+ assertContains(t, output, `const external = "https://example.com/api"`)
+ assertContains(t, output, `const cdn = "//cdn.example.com/app.js"`)
+ assertContains(t, output, `const already = "/t/test-slug/api/health"`)
+ assertNotContains(t, output, `/t/test-slug/t/test-slug`)
+}
+
+func TestRewriteTunnelCSSBodyPrefixesRootRelativeURLs(t *testing.T) {
+ t.Parallel()
+
+ tunnel := tunnelRewriteTestSummary()
+ input := strings.Join([]string{
+ `body { background: url(/images/bg.png); }`,
+ `.icon { mask-image: url('/icons/check.svg'); }`,
+ `.remote { background: url("https://example.com/bg.png"); }`,
+ `.cdn { background: url("//cdn.example.com/bg.png"); }`,
+ `.already { background: url(/t/test-slug/images/bg.png); }`,
+ }, "\n")
+
+ body, changed := rewriteTunnelResponseBody([]byte(input), tunnel, tunnelResponseRewriteCSS)
+ if !changed {
+ t.Fatal("rewriteTunnelResponseBody() did not report a change")
+ }
+ output := string(body)
+
+ assertContains(t, output, `url(/t/test-slug/images/bg.png)`)
+ assertContains(t, output, `url('/t/test-slug/icons/check.svg')`)
+ assertContains(t, output, `url("https://example.com/bg.png")`)
+ assertContains(t, output, `url("//cdn.example.com/bg.png")`)
+ assertContains(t, output, `url(/t/test-slug/images/bg.png)`)
+ assertNotContains(t, output, `/t/test-slug/t/test-slug`)
+}
+
+func TestParseTunnelPublicPathWithoutTrailingSlash(t *testing.T) {
+ t.Parallel()
+
+ slug, ok := parseTunnelPublicPathWithoutTrailingSlash("/t/test-slug")
+ if !ok || slug != "test-slug" {
+ t.Fatalf("parseTunnelPublicPathWithoutTrailingSlash() = %q, %v", slug, ok)
+ }
+
+ for _, path := range []string{"/t/test-slug/", "/t/test-slug/api", "/t/", "/api/test-slug"} {
+ if slug, ok := parseTunnelPublicPathWithoutTrailingSlash(path); ok {
+ t.Fatalf("parseTunnelPublicPathWithoutTrailingSlash(%q) = %q, true; want false", path, slug)
+ }
+ }
+}
+
+func TestRewriteTunnelLocationPreservesQueryAndFragment(t *testing.T) {
+ t.Parallel()
+
+ tunnel := tunnelRewriteTestSummary()
+ if got := rewriteTunnelLocation("/api/health?check=1#ready", tunnel); got != "/t/test-slug/api/health?check=1#ready" {
+ t.Fatalf("rewriteTunnelLocation root path = %q", got)
+ }
+ if got := rewriteTunnelLocation("http://127.0.0.1:3100/api/showcase#item", tunnel); got != "/t/test-slug/api/showcase#item" {
+ t.Fatalf("rewriteTunnelLocation absolute target = %q", got)
+ }
+ if got := rewriteTunnelLocation("https://example.com/api#item", tunnel); got != "https://example.com/api#item" {
+ t.Fatalf("rewriteTunnelLocation external = %q", got)
+ }
+}
+
+func tunnelRewriteTestSummary() *gatewayv1.TunnelSummary {
+ return &gatewayv1.TunnelSummary{
+ Slug: "test-slug",
+ TargetUrl: "http://127.0.0.1:3100",
+ }
+}
+
+func assertContains(t *testing.T, value string, needle string) {
+ t.Helper()
+ if !strings.Contains(value, needle) {
+ t.Fatalf("expected output to contain %q, got:\n%s", needle, value)
+ }
+}
+
+func assertNotContains(t *testing.T, value string, needle string) {
+ t.Helper()
+ if strings.Contains(value, needle) {
+ t.Fatalf("expected output not to contain %q, got:\n%s", needle, value)
+ }
+}
diff --git a/crates/agent-gateway/internal/server/websocket.go b/crates/agent-gateway/internal/server/websocket.go
index 56f87c440..0a7e43d30 100644
--- a/crates/agent-gateway/internal/server/websocket.go
+++ b/crates/agent-gateway/internal/server/websocket.go
@@ -431,6 +431,25 @@ func (c *websocketConnection) awaitAgentResponse(
return awaitAgentUnaryResponse(ctx, c.sm, requestID, envelope)
}
+func (c *websocketConnection) sendToAgent(envelope *gatewayv1.GatewayEnvelope) error {
+ timeout := c.cfg.WebSocketWriteTimeout
+ if timeout <= 0 {
+ timeout = 10 * time.Second
+ }
+ ctx, cancel := context.WithTimeout(context.Background(), timeout)
+ defer cancel()
+
+ go func() {
+ select {
+ case <-c.done:
+ cancel()
+ case <-ctx.Done():
+ }
+ }()
+
+ return c.sm.SendToAgentContext(ctx, envelope)
+}
+
func (c *websocketConnection) writeResponse(requestID string, payload any) error {
return c.writeEnvelope(websocketEnvelope{
ID: requestID,
diff --git a/crates/agent-gateway/internal/server/websocket_chat_handlers.go b/crates/agent-gateway/internal/server/websocket_chat_handlers.go
index b050b3623..f647b32e6 100644
--- a/crates/agent-gateway/internal/server/websocket_chat_handlers.go
+++ b/crates/agent-gateway/internal/server/websocket_chat_handlers.go
@@ -76,7 +76,7 @@ func (c *websocketConnection) handleChatStart(req websocketRequest) {
defer c.releaseActiveChat(responseID)
if created {
- if err := c.sm.SendToAgent(&gatewayv1.GatewayEnvelope{
+ if err := c.sendToAgent(&gatewayv1.GatewayEnvelope{
RequestId: sourceRequestID,
Timestamp: time.Now().Unix(),
Payload: &gatewayv1.GatewayEnvelope_ChatRequest{
@@ -328,7 +328,7 @@ func (c *websocketConnection) handleChatCancel(req websocketRequest) {
return
}
- if err := c.sm.SendToAgent(&gatewayv1.GatewayEnvelope{
+ if err := c.sendToAgent(&gatewayv1.GatewayEnvelope{
RequestId: req.ID,
Timestamp: time.Now().Unix(),
Payload: &gatewayv1.GatewayEnvelope_CancelChat{
diff --git a/crates/agent-gateway/internal/server/websocket_roundtrip.go b/crates/agent-gateway/internal/server/websocket_roundtrip.go
index 3d7836aa6..c2723c57f 100644
--- a/crates/agent-gateway/internal/server/websocket_roundtrip.go
+++ b/crates/agent-gateway/internal/server/websocket_roundtrip.go
@@ -53,7 +53,7 @@ func awaitAgentUnaryResponse(
}
defer cleanup()
- if err := sm.SendToAgent(envelope); err != nil {
+ if err := sm.SendToAgentContext(ctx, envelope); err != nil {
return nil, err
}
diff --git a/crates/agent-gateway/internal/server/websocket_routes.go b/crates/agent-gateway/internal/server/websocket_routes.go
index 823244479..60ad324dd 100644
--- a/crates/agent-gateway/internal/server/websocket_routes.go
+++ b/crates/agent-gateway/internal/server/websocket_routes.go
@@ -51,6 +51,10 @@ var websocketRequestHandlers = map[string]websocketRequestHandler{
"terminal.close": (*websocketConnection).handleTerminalRequest,
"terminal.close_project": (*websocketConnection).handleTerminalRequest,
"terminal.detach": (*websocketConnection).handleTerminalDetach,
+ "tunnel.list": (*websocketConnection).handleTunnelList,
+ "tunnel.create": (*websocketConnection).handleTunnelCreate,
+ "tunnel.update": (*websocketConnection).handleTunnelUpdate,
+ "tunnel.close": (*websocketConnection).handleTunnelClose,
"git.status": (*websocketConnection).handleGitRequest,
"git.branches": (*websocketConnection).handleGitRequest,
"git.init": (*websocketConnection).handleGitRequest,
diff --git a/crates/agent-gateway/internal/server/websocket_routes_test.go b/crates/agent-gateway/internal/server/websocket_routes_test.go
index e7e8a2d8e..3b275b626 100644
--- a/crates/agent-gateway/internal/server/websocket_routes_test.go
+++ b/crates/agent-gateway/internal/server/websocket_routes_test.go
@@ -1,6 +1,9 @@
package server
-import "testing"
+import (
+ "encoding/json"
+ "testing"
+)
func TestWebsocketRequestHandlersCoverKnownProtocolTypes(t *testing.T) {
t.Parallel()
@@ -52,6 +55,10 @@ func TestWebsocketRequestHandlersCoverKnownProtocolTypes(t *testing.T) {
"terminal.close",
"terminal.close_project",
"terminal.detach",
+ "tunnel.list",
+ "tunnel.create",
+ "tunnel.update",
+ "tunnel.close",
"git.status",
"git.branches",
"git.init",
@@ -103,3 +110,50 @@ func TestDecodeWebSocketPayloadRejectsUnknownFields(t *testing.T) {
t.Fatal("expected unknown payload field to be rejected")
}
}
+
+func TestTunnelTTLFromPayloadDefaultsOnlyWhenOmitted(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ name string
+ raw json.RawMessage
+ camelValue uint32
+ snakeValue uint32
+ want uint32
+ }{
+ {
+ name: "omitted",
+ raw: json.RawMessage(`{"targetUrl":"http://localhost:3000"}`),
+ camelValue: 0,
+ snakeValue: 0,
+ want: websocketDefaultTunnelTTLSeconds,
+ },
+ {
+ name: "camel explicit infinite",
+ raw: json.RawMessage(`{"targetUrl":"http://localhost:3000","ttlSeconds":0}`),
+ camelValue: 0,
+ snakeValue: 0,
+ want: 0,
+ },
+ {
+ name: "snake explicit infinite",
+ raw: json.RawMessage(`{"target_url":"http://localhost:3000","ttl_seconds":0}`),
+ camelValue: 0,
+ snakeValue: 0,
+ want: 0,
+ },
+ {
+ name: "camel finite",
+ raw: json.RawMessage(`{"targetUrl":"http://localhost:3000","ttlSeconds":900}`),
+ camelValue: 900,
+ snakeValue: 0,
+ want: 900,
+ },
+ }
+
+ for _, tt := range tests {
+ if got := tunnelTTLFromPayload(tt.raw, tt.camelValue, tt.snakeValue); got != tt.want {
+ t.Fatalf("%s: tunnelTTLFromPayload = %d, want %d", tt.name, got, tt.want)
+ }
+ }
+}
diff --git a/crates/agent-gateway/internal/server/websocket_tunnel_handlers.go b/crates/agent-gateway/internal/server/websocket_tunnel_handlers.go
new file mode 100644
index 000000000..f01e3867e
--- /dev/null
+++ b/crates/agent-gateway/internal/server/websocket_tunnel_handlers.go
@@ -0,0 +1,322 @@
+package server
+
+import (
+ "encoding/json"
+ "strings"
+ "time"
+
+ gatewayv1 "github.com/liveagent/agent-gateway/internal/proto/v1"
+)
+
+const websocketDefaultTunnelTTLSeconds = 3600
+
+type websocketTunnelCreatePayload struct {
+ TargetURL string `json:"targetUrl"`
+ TargetUrl string `json:"target_url"`
+ Name string `json:"name"`
+ TTLSeconds uint32 `json:"ttlSeconds"`
+ TtlSeconds uint32 `json:"ttl_seconds"`
+ ProjectPathKey string `json:"projectPathKey"`
+ ProjectPathKeySnake string `json:"project_path_key"`
+}
+
+func tunnelTTLFromPayload(raw json.RawMessage, camelValue uint32, snakeValue uint32) uint32 {
+ var fields map[string]json.RawMessage
+ if err := json.Unmarshal(raw, &fields); err != nil {
+ return websocketDefaultTunnelTTLSeconds
+ }
+ if _, ok := fields["ttlSeconds"]; ok {
+ return camelValue
+ }
+ if _, ok := fields["ttl_seconds"]; ok {
+ return snakeValue
+ }
+ return websocketDefaultTunnelTTLSeconds
+}
+
+type websocketTunnelUpdatePayload struct {
+ ID string `json:"id"`
+ TunnelID string `json:"tunnelId"`
+ TunnelId string `json:"tunnel_id"`
+ Slug string `json:"slug"`
+ TargetURL string `json:"targetUrl"`
+ TargetUrl string `json:"target_url"`
+ Name string `json:"name"`
+ TTLSeconds uint32 `json:"ttlSeconds"`
+ TtlSeconds uint32 `json:"ttl_seconds"`
+ ProjectPathKey string `json:"projectPathKey"`
+ ProjectPathKeySnake string `json:"project_path_key"`
+}
+
+type websocketTunnelClosePayload struct {
+ ID string `json:"id"`
+ TunnelID string `json:"tunnelId"`
+ TunnelId string `json:"tunnel_id"`
+ Slug string `json:"slug"`
+}
+
+func (c *websocketConnection) handleTunnelList(req websocketRequest) {
+ _ = c.writeResponse(req.ID, map[string]any{
+ "tunnels": websocketTunnelSummariesPayload(c.sm.ListTunnels(), c.publicBaseURL()),
+ })
+}
+
+func (c *websocketConnection) handleTunnelCreate(req websocketRequest) {
+ if !c.sm.WebTunnelsEnabled() {
+ _ = c.writeError(req.ID, "web tunnels are disabled in desktop Remote settings")
+ return
+ }
+
+ var body websocketTunnelCreatePayload
+ if err := decodeWebSocketPayload(req.Payload, &body); err != nil {
+ _ = c.writeError(req.ID, "invalid tunnel.create payload")
+ return
+ }
+ targetURL := strings.TrimSpace(body.TargetURL)
+ if targetURL == "" {
+ targetURL = strings.TrimSpace(body.TargetUrl)
+ }
+ ttlSeconds := tunnelTTLFromPayload(req.Payload, body.TTLSeconds, body.TtlSeconds)
+ projectPathKey := strings.TrimSpace(body.ProjectPathKey)
+ if projectPathKey == "" {
+ projectPathKey = strings.TrimSpace(body.ProjectPathKeySnake)
+ }
+ prepared, err := c.sm.PrepareTunnelCreate(&gatewayv1.TunnelControlRequest{
+ Action: "create",
+ TargetUrl: targetURL,
+ Name: strings.TrimSpace(body.Name),
+ TtlSeconds: ttlSeconds,
+ ProjectPathKey: projectPathKey,
+ }, c.publicBaseURL())
+ if err != nil {
+ _ = c.writeError(req.ID, websocketErrorMessage(err))
+ return
+ }
+
+ response, err := c.awaitAgentResponse(req.ID, &gatewayv1.GatewayEnvelope{
+ RequestId: req.ID,
+ Timestamp: time.Now().Unix(),
+ Payload: &gatewayv1.GatewayEnvelope_TunnelControl{
+ TunnelControl: prepared,
+ },
+ })
+ if err != nil {
+ _ = c.writeError(req.ID, websocketErrorMessage(err))
+ return
+ }
+ if errResp := response.GetError(); errResp != nil {
+ _ = c.writeError(req.ID, errResp.GetMessage())
+ return
+ }
+ controlResp := response.GetTunnelControlResp()
+ if controlResp == nil {
+ _ = c.writeError(req.ID, "unexpected agent response")
+ return
+ }
+ if controlResp.GetErrorMessage() != "" {
+ _ = c.writeError(req.ID, controlResp.GetErrorMessage())
+ return
+ }
+ targetOverride := ""
+ if tunnel := controlResp.GetTunnel(); tunnel != nil {
+ targetOverride = tunnel.GetTargetUrl()
+ }
+ tunnel, err := c.sm.StorePreparedTunnel(prepared, targetOverride)
+ if err != nil {
+ _ = c.writeError(req.ID, websocketErrorMessage(err))
+ return
+ }
+ _ = c.writeResponse(req.ID, map[string]any{
+ "tunnel": websocketTunnelSummaryPayload(tunnel, c.publicBaseURL()),
+ "tunnels": websocketTunnelSummariesPayload(c.sm.ListTunnels(), c.publicBaseURL()),
+ })
+}
+
+func (c *websocketConnection) handleTunnelUpdate(req websocketRequest) {
+ if !c.sm.WebTunnelsEnabled() {
+ _ = c.writeError(req.ID, "web tunnels are disabled in desktop Remote settings")
+ return
+ }
+
+ var body websocketTunnelUpdatePayload
+ if err := decodeWebSocketPayload(req.Payload, &body); err != nil {
+ _ = c.writeError(req.ID, "invalid tunnel.update payload")
+ return
+ }
+ identifier := strings.TrimSpace(body.ID)
+ if identifier == "" {
+ identifier = strings.TrimSpace(body.TunnelID)
+ }
+ if identifier == "" {
+ identifier = strings.TrimSpace(body.TunnelId)
+ }
+ if identifier == "" {
+ identifier = strings.TrimSpace(body.Slug)
+ }
+ if identifier == "" {
+ _ = c.writeError(req.ID, "tunnel id is required")
+ return
+ }
+ targetURL := strings.TrimSpace(body.TargetURL)
+ if targetURL == "" {
+ targetURL = strings.TrimSpace(body.TargetUrl)
+ }
+ ttlSeconds := tunnelTTLFromPayload(req.Payload, body.TTLSeconds, body.TtlSeconds)
+ projectPathKey := strings.TrimSpace(body.ProjectPathKey)
+ if projectPathKey == "" {
+ projectPathKey = strings.TrimSpace(body.ProjectPathKeySnake)
+ }
+ prepared, err := c.sm.PrepareTunnelUpdate(&gatewayv1.TunnelControlRequest{
+ Action: "update",
+ TunnelId: identifier,
+ TargetUrl: targetURL,
+ Name: strings.TrimSpace(body.Name),
+ TtlSeconds: ttlSeconds,
+ ProjectPathKey: projectPathKey,
+ })
+ if err != nil {
+ _ = c.writeError(req.ID, websocketErrorMessage(err))
+ return
+ }
+
+ response, err := c.awaitAgentResponse(req.ID, &gatewayv1.GatewayEnvelope{
+ RequestId: req.ID,
+ Timestamp: time.Now().Unix(),
+ Payload: &gatewayv1.GatewayEnvelope_TunnelControl{
+ TunnelControl: prepared,
+ },
+ })
+ if err != nil {
+ _ = c.writeError(req.ID, websocketErrorMessage(err))
+ return
+ }
+ if errResp := response.GetError(); errResp != nil {
+ _ = c.writeError(req.ID, errResp.GetMessage())
+ return
+ }
+ controlResp := response.GetTunnelControlResp()
+ if controlResp == nil {
+ _ = c.writeError(req.ID, "unexpected agent response")
+ return
+ }
+ if controlResp.GetErrorMessage() != "" {
+ _ = c.writeError(req.ID, controlResp.GetErrorMessage())
+ return
+ }
+ tunnel, err := c.sm.ApplyTunnelUpdate(controlResp.GetTunnel())
+ if err != nil {
+ _ = c.writeError(req.ID, websocketErrorMessage(err))
+ return
+ }
+ _ = c.writeResponse(req.ID, map[string]any{
+ "tunnel": websocketTunnelSummaryPayload(tunnel, c.publicBaseURL()),
+ "tunnels": websocketTunnelSummariesPayload(c.sm.ListTunnels(), c.publicBaseURL()),
+ })
+}
+
+func (c *websocketConnection) handleTunnelClose(req websocketRequest) {
+ if !c.sm.WebTunnelsEnabled() {
+ _ = c.writeError(req.ID, "web tunnels are disabled in desktop Remote settings")
+ return
+ }
+
+ var body websocketTunnelClosePayload
+ if err := decodeWebSocketPayload(req.Payload, &body); err != nil {
+ _ = c.writeError(req.ID, "invalid tunnel.close payload")
+ return
+ }
+ identifier := strings.TrimSpace(body.ID)
+ if identifier == "" {
+ identifier = strings.TrimSpace(body.TunnelID)
+ }
+ if identifier == "" {
+ identifier = strings.TrimSpace(body.TunnelId)
+ }
+ if identifier == "" {
+ identifier = strings.TrimSpace(body.Slug)
+ }
+ if identifier == "" {
+ _ = c.writeError(req.ID, "tunnel id is required")
+ return
+ }
+
+ tunnel, err := c.sm.CloseTunnel(identifier)
+ if err != nil {
+ _ = c.writeError(req.ID, websocketErrorMessage(err))
+ return
+ }
+
+ _ = c.sendToAgent(&gatewayv1.GatewayEnvelope{
+ RequestId: "tunnel-close-" + tunnel.GetId(),
+ Timestamp: time.Now().Unix(),
+ Payload: &gatewayv1.GatewayEnvelope_TunnelControl{
+ TunnelControl: &gatewayv1.TunnelControlRequest{
+ Action: "close",
+ TunnelId: tunnel.GetId(),
+ Slug: tunnel.GetSlug(),
+ },
+ },
+ })
+
+ _ = c.writeResponse(req.ID, map[string]any{
+ "tunnel": websocketTunnelSummaryPayload(tunnel, c.publicBaseURL()),
+ "tunnels": websocketTunnelSummariesPayload(c.sm.ListTunnels(), c.publicBaseURL()),
+ })
+}
+
+func (c *websocketConnection) publicBaseURL() string {
+ return publicBaseURLFromHTTPRequest(c.conn.Request())
+}
+
+func websocketTunnelSummariesPayload(
+ summaries []*gatewayv1.TunnelSummary,
+ publicBaseURL string,
+) []map[string]any {
+ payload := make([]map[string]any, 0, len(summaries))
+ for _, summary := range summaries {
+ if item := websocketTunnelSummaryPayload(summary, publicBaseURL); item != nil {
+ payload = append(payload, item)
+ }
+ }
+ return payload
+}
+
+func websocketTunnelSummaryPayload(
+ summary *gatewayv1.TunnelSummary,
+ publicBaseURL string,
+) map[string]any {
+ if summary == nil {
+ return nil
+ }
+ publicURL := strings.TrimSpace(summary.GetPublicUrl())
+ if publicURL == "" {
+ publicURL = buildPublicTunnelURL(publicBaseURL, summary.GetSlug())
+ }
+ return map[string]any{
+ "id": strings.TrimSpace(summary.GetId()),
+ "slug": strings.TrimSpace(summary.GetSlug()),
+ "name": strings.TrimSpace(summary.GetName()),
+ "targetUrl": strings.TrimSpace(summary.GetTargetUrl()),
+ "target_url": strings.TrimSpace(summary.GetTargetUrl()),
+ "publicUrl": publicURL,
+ "public_url": publicURL,
+ "createdAt": summary.GetCreatedAt(),
+ "created_at": summary.GetCreatedAt(),
+ "expiresAt": summary.GetExpiresAt(),
+ "expires_at": summary.GetExpiresAt(),
+ "activeConnections": summary.GetActiveConnections(),
+ "active_connections": summary.GetActiveConnections(),
+ "status": strings.TrimSpace(summary.GetStatus()),
+ "projectPathKey": strings.TrimSpace(summary.GetProjectPathKey()),
+ "project_path_key": strings.TrimSpace(summary.GetProjectPathKey()),
+ }
+}
+
+func buildPublicTunnelURL(publicBaseURL string, slug string) string {
+ publicBaseURL = strings.TrimRight(strings.TrimSpace(publicBaseURL), "/")
+ slug = strings.TrimSpace(slug)
+ if publicBaseURL == "" || slug == "" {
+ return ""
+ }
+ return publicBaseURL + "/t/" + slug + "/"
+}
diff --git a/crates/agent-gateway/internal/session/agent_session.go b/crates/agent-gateway/internal/session/agent_session.go
index 790e6ae90..5bee6705c 100644
--- a/crates/agent-gateway/internal/session/agent_session.go
+++ b/crates/agent-gateway/internal/session/agent_session.go
@@ -1,6 +1,7 @@
package session
import (
+ "context"
"time"
gatewayv1 "github.com/liveagent/agent-gateway/internal/proto/v1"
@@ -13,13 +14,37 @@ func NewAgentSession(auth AuthSnapshot) *AgentSession {
SessionID: auth.SessionID,
ConnectedAt: time.Now(),
LastPing: time.Now(),
- toAgent: make(chan *gatewayv1.GatewayEnvelope, 64),
+ toAgent: make(chan *OutboundEnvelope, 64),
done: make(chan struct{}),
streams: make(map[string]*agentStream),
}
}
-func (s *AgentSession) Outbound() <-chan *gatewayv1.GatewayEnvelope {
+type OutboundEnvelope struct {
+ *gatewayv1.GatewayEnvelope
+
+ ctx context.Context
+ result chan error
+}
+
+func (e *OutboundEnvelope) Context() context.Context {
+ if e == nil || e.ctx == nil {
+ return context.Background()
+ }
+ return e.ctx
+}
+
+func (e *OutboundEnvelope) Ack(err error) {
+ if e == nil || e.result == nil {
+ return
+ }
+ select {
+ case e.result <- err:
+ default:
+ }
+}
+
+func (s *AgentSession) Outbound() <-chan *OutboundEnvelope {
return s.toAgent
}
@@ -41,6 +66,34 @@ func (s *AgentSession) Close() {
}
func (s *AgentSession) SendToAgent(env *gatewayv1.GatewayEnvelope) error {
+ return s.enqueueToAgent(context.Background(), env, nil)
+}
+
+func (s *AgentSession) SendToAgentContext(ctx context.Context, env *gatewayv1.GatewayEnvelope) error {
+ if ctx == nil {
+ ctx = context.Background()
+ }
+ result := make(chan error, 1)
+ if err := s.enqueueToAgent(ctx, env, result); err != nil {
+ return err
+ }
+
+ select {
+ case err := <-result:
+ return err
+ case <-ctx.Done():
+ s.Close()
+ return ctx.Err()
+ case <-s.done:
+ return ErrAgentOffline
+ }
+}
+
+func (s *AgentSession) enqueueToAgent(
+ ctx context.Context,
+ env *gatewayv1.GatewayEnvelope,
+ result chan error,
+) error {
s.streamsMu.Lock()
closed := s.closed
s.streamsMu.Unlock()
@@ -49,9 +102,15 @@ func (s *AgentSession) SendToAgent(env *gatewayv1.GatewayEnvelope) error {
}
select {
+ case <-ctx.Done():
+ return ctx.Err()
case <-s.done:
return ErrAgentOffline
- case s.toAgent <- env:
+ case s.toAgent <- &OutboundEnvelope{
+ GatewayEnvelope: env,
+ ctx: ctx,
+ result: result,
+ }:
return nil
}
}
@@ -73,7 +132,7 @@ func (s *AgentSession) TrySendToAgent(env *gatewayv1.GatewayEnvelope) (bool, err
select {
case <-s.done:
return false, ErrAgentOffline
- case s.toAgent <- env:
+ case s.toAgent <- &OutboundEnvelope{GatewayEnvelope: env}:
return true, nil
default:
return false, nil
diff --git a/crates/agent-gateway/internal/session/manager.go b/crates/agent-gateway/internal/session/manager.go
index c46bf6b1a..b40db5b98 100644
--- a/crates/agent-gateway/internal/session/manager.go
+++ b/crates/agent-gateway/internal/session/manager.go
@@ -10,6 +10,10 @@ import (
var ErrAgentOffline = errors.New("agent offline")
var ErrChatRunNotFound = errors.New("chat run not found")
+var ErrTunnelNotFound = errors.New("tunnel not found")
+var ErrTunnelExpired = errors.New("tunnel expired")
+var ErrTunnelOverLimit = errors.New("tunnel connection limit exceeded")
+var ErrTunnelLimitExceeded = errors.New("tunnel limit exceeded")
const (
maxBufferedChatRunEvents = 50000
@@ -29,6 +33,7 @@ type Manager struct {
registry *sessionRegistry
syncHub *syncHub
chatStore *chatRunStore
+ tunnels *tunnelStore
}
type AgentSession struct {
@@ -38,7 +43,7 @@ type AgentSession struct {
ConnectedAt time.Time
LastPing time.Time
- toAgent chan *gatewayv1.GatewayEnvelope
+ toAgent chan *OutboundEnvelope
done chan struct{}
closeOnce sync.Once
@@ -117,5 +122,6 @@ func NewManager() *Manager {
registry: newSessionRegistry(),
syncHub: newSyncHub(),
chatStore: newChatRunStore(),
+ tunnels: newTunnelStore(),
}
}
diff --git a/crates/agent-gateway/internal/session/manager_chat_runs.go b/crates/agent-gateway/internal/session/manager_chat_runs.go
index ef8eb3e35..53cf22653 100644
--- a/crates/agent-gateway/internal/session/manager_chat_runs.go
+++ b/crates/agent-gateway/internal/session/manager_chat_runs.go
@@ -491,6 +491,16 @@ func (m *Manager) dispatchFromAgent(expected *AgentSession, env *gatewayv1.Agent
return
}
+ if tunnelFrame := env.GetTunnelFrame(); tunnelFrame != nil {
+ m.dispatchTunnelFrame(tunnelFrame)
+ return
+ }
+
+ if tunnelControl := env.GetTunnelControl(); tunnelControl != nil {
+ m.handleAgentTunnelControl(session, env.GetRequestId(), tunnelControl)
+ return
+ }
+
session.dispatch(env)
}
diff --git a/crates/agent-gateway/internal/session/manager_registry.go b/crates/agent-gateway/internal/session/manager_registry.go
index 09609a19c..925bc2838 100644
--- a/crates/agent-gateway/internal/session/manager_registry.go
+++ b/crates/agent-gateway/internal/session/manager_registry.go
@@ -1,6 +1,7 @@
package session
import (
+ "context"
"time"
gatewayv1 "github.com/liveagent/agent-gateway/internal/proto/v1"
@@ -114,6 +115,17 @@ func (m *Manager) SendToAgent(env *gatewayv1.GatewayEnvelope) error {
return session.SendToAgent(env)
}
+func (m *Manager) SendToAgentContext(ctx context.Context, env *gatewayv1.GatewayEnvelope) error {
+ m.registry.mu.RLock()
+ session := m.registry.session
+ m.registry.mu.RUnlock()
+ if session == nil {
+ return ErrAgentOffline
+ }
+
+ return session.SendToAgentContext(ctx, env)
+}
+
func (m *Manager) currentSessionEpoch() uint64 {
m.registry.mu.RLock()
defer m.registry.mu.RUnlock()
diff --git a/crates/agent-gateway/internal/session/manager_tunnel.go b/crates/agent-gateway/internal/session/manager_tunnel.go
new file mode 100644
index 000000000..aaf2c1712
--- /dev/null
+++ b/crates/agent-gateway/internal/session/manager_tunnel.go
@@ -0,0 +1,843 @@
+package session
+
+import (
+ "context"
+ "crypto/rand"
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "net/url"
+ "strings"
+ "sync"
+ "time"
+
+ gatewayv1 "github.com/liveagent/agent-gateway/internal/proto/v1"
+)
+
+const (
+ MaxTunnelsPerAgent = 5
+ MaxTunnelConnections = 20
+ defaultTunnelTTLSeconds = 3600
+ tunnelSlugEntropyBytes = 24
+ tunnelStreamChannelDepth = 256
+ tunnelAgentSendTimeout = 10 * time.Second
+)
+
+type tunnelStore struct {
+ mu sync.Mutex
+ tunnelsByID map[string]*tunnelRecord
+ tunnelIDBySlug map[string]string
+ streams map[string]*tunnelStream
+}
+
+type tunnelRecord struct {
+ id string
+ slug string
+ name string
+ targetURL string
+ publicURL string
+ projectPathKey string
+ createdAt time.Time
+ expiresAt time.Time
+ activeConnections int
+ closed bool
+}
+
+type tunnelStream struct {
+ streamID string
+ tunnelID string
+ ch chan *gatewayv1.TunnelFrame
+ done chan struct{}
+ once sync.Once
+}
+
+type TunnelStreamLease struct {
+ manager *Manager
+ stream *tunnelStream
+ tunnel *gatewayv1.TunnelSummary
+ once sync.Once
+}
+
+func newTunnelStore() *tunnelStore {
+ return &tunnelStore{
+ tunnelsByID: make(map[string]*tunnelRecord),
+ tunnelIDBySlug: make(map[string]string),
+ streams: make(map[string]*tunnelStream),
+ }
+}
+
+func (l *TunnelStreamLease) Tunnel() *gatewayv1.TunnelSummary {
+ if l == nil || l.tunnel == nil {
+ return nil
+ }
+ return cloneTunnelSummary(l.tunnel)
+}
+
+func (l *TunnelStreamLease) TunnelID() string {
+ if l == nil || l.stream == nil {
+ return ""
+ }
+ return l.stream.tunnelID
+}
+
+func (l *TunnelStreamLease) StreamID() string {
+ if l == nil || l.stream == nil {
+ return ""
+ }
+ return l.stream.streamID
+}
+
+func (l *TunnelStreamLease) Frames() <-chan *gatewayv1.TunnelFrame {
+ if l == nil || l.stream == nil {
+ return nil
+ }
+ return l.stream.ch
+}
+
+func (l *TunnelStreamLease) Done() <-chan struct{} {
+ if l == nil || l.stream == nil {
+ return nil
+ }
+ return l.stream.done
+}
+
+func (l *TunnelStreamLease) Release() {
+ if l == nil {
+ return
+ }
+ l.once.Do(func() {
+ l.manager.releaseTunnelStream(l.stream)
+ })
+}
+
+func (s *tunnelStream) close() {
+ if s == nil {
+ return
+ }
+ s.once.Do(func() {
+ close(s.done)
+ })
+}
+
+func (s *tunnelStream) send(frame *gatewayv1.TunnelFrame) bool {
+ select {
+ case <-s.done:
+ return false
+ case s.ch <- frame:
+ return true
+ }
+}
+
+func (m *Manager) WebTunnelsEnabled() bool {
+ m.syncHub.settingsSnapshotMu.RLock()
+ defer m.syncHub.settingsSnapshotMu.RUnlock()
+
+ remote, ok := m.syncHub.settingsSnapshot["remote"].(map[string]any)
+ if !ok {
+ return false
+ }
+ enabled, ok := remote["enableWebTunnels"].(bool)
+ return ok && enabled
+}
+
+func (m *Manager) ListTunnels() []*gatewayv1.TunnelSummary {
+ now := time.Now()
+ online := m.IsOnline()
+ m.tunnels.mu.Lock()
+ defer m.tunnels.mu.Unlock()
+
+ summaries := make([]*gatewayv1.TunnelSummary, 0, len(m.tunnels.tunnelsByID))
+ for _, record := range m.tunnels.tunnelsByID {
+ if record == nil || record.closed {
+ continue
+ }
+ summaries = append(summaries, tunnelSummaryLocked(record, now, online))
+ }
+ sortTunnelSummaries(summaries)
+ return summaries
+}
+
+func (m *Manager) PrepareTunnelCreate(
+ input *gatewayv1.TunnelControlRequest,
+ publicBaseURL string,
+) (*gatewayv1.TunnelControlRequest, error) {
+ if input == nil {
+ return nil, errors.New("tunnel create input is required")
+ }
+ ttlSeconds, err := normalizeTunnelTTL(input.GetTtlSeconds())
+ if err != nil {
+ return nil, err
+ }
+ now := time.Now()
+ var expiresAt time.Time
+ if ttlSeconds > 0 {
+ expiresAt = now.Add(time.Duration(ttlSeconds) * time.Second)
+ }
+
+ m.tunnels.mu.Lock()
+ defer m.tunnels.mu.Unlock()
+
+ activeCount := 0
+ for _, record := range m.tunnels.tunnelsByID {
+ if record == nil || record.closed || isTunnelExpired(record, now) {
+ continue
+ }
+ activeCount += 1
+ }
+ if activeCount >= MaxTunnelsPerAgent {
+ return nil, ErrTunnelLimitExceeded
+ }
+
+ id := strings.TrimSpace(input.GetTunnelId())
+ if id == "" {
+ id = generateTunnelID()
+ }
+ if _, exists := m.tunnels.tunnelsByID[id]; exists {
+ return nil, fmt.Errorf("tunnel id already exists")
+ }
+ slug := strings.TrimSpace(input.GetSlug())
+ if slug == "" {
+ for {
+ generated, err := generateTunnelSlug()
+ if err != nil {
+ return nil, err
+ }
+ if _, exists := m.tunnels.tunnelIDBySlug[generated]; !exists {
+ slug = generated
+ break
+ }
+ }
+ } else if _, exists := m.tunnels.tunnelIDBySlug[slug]; exists {
+ return nil, fmt.Errorf("tunnel slug already exists")
+ }
+
+ publicURL := normalizeTunnelPublicURL(input.GetPublicUrl())
+ if publicURL == "" {
+ publicURL = buildTunnelPublicURL(publicBaseURL, slug)
+ }
+
+ return &gatewayv1.TunnelControlRequest{
+ Action: strings.TrimSpace(input.GetAction()),
+ TunnelId: id,
+ Slug: slug,
+ TargetUrl: strings.TrimSpace(input.GetTargetUrl()),
+ Name: strings.TrimSpace(input.GetName()),
+ TtlSeconds: ttlSeconds,
+ ExpiresAt: tunnelUnix(expiresAt),
+ PublicUrl: publicURL,
+ PublicBaseUrl: strings.TrimSpace(publicBaseURL),
+ ProjectPathKey: strings.TrimSpace(input.GetProjectPathKey()),
+ }, nil
+}
+
+func (m *Manager) PrepareTunnelUpdate(
+ input *gatewayv1.TunnelControlRequest,
+) (*gatewayv1.TunnelControlRequest, error) {
+ if input == nil {
+ return nil, errors.New("tunnel update input is required")
+ }
+ ttlSeconds, err := normalizeTunnelTTL(input.GetTtlSeconds())
+ if err != nil {
+ return nil, err
+ }
+ now := time.Now()
+ var expiresAt time.Time
+ if input.GetExpiresAt() > 0 {
+ expiresAt = time.Unix(input.GetExpiresAt(), 0)
+ } else if ttlSeconds > 0 {
+ expiresAt = now.Add(time.Duration(ttlSeconds) * time.Second)
+ }
+ targetURL := strings.TrimSpace(input.GetTargetUrl())
+ if targetURL == "" {
+ return nil, errors.New("target_url is required")
+ }
+
+ m.tunnels.mu.Lock()
+ defer m.tunnels.mu.Unlock()
+
+ identifier := strings.TrimSpace(input.GetTunnelId())
+ if identifier == "" {
+ identifier = strings.TrimSpace(input.GetSlug())
+ }
+ if identifier == "" {
+ return nil, ErrTunnelNotFound
+ }
+ tunnelID := identifier
+ if bySlug := m.tunnels.tunnelIDBySlug[identifier]; bySlug != "" {
+ tunnelID = bySlug
+ }
+ record := m.tunnels.tunnelsByID[tunnelID]
+ if record == nil || record.closed {
+ return nil, ErrTunnelNotFound
+ }
+ if isTunnelExpired(record, now) {
+ return nil, ErrTunnelExpired
+ }
+ projectPathKey := strings.TrimSpace(input.GetProjectPathKey())
+ if projectPathKey == "" {
+ projectPathKey = record.projectPathKey
+ }
+
+ return &gatewayv1.TunnelControlRequest{
+ Action: strings.TrimSpace(input.GetAction()),
+ TunnelId: record.id,
+ Slug: record.slug,
+ TargetUrl: targetURL,
+ Name: strings.TrimSpace(input.GetName()),
+ TtlSeconds: ttlSeconds,
+ ExpiresAt: tunnelUnix(expiresAt),
+ PublicUrl: record.publicURL,
+ ProjectPathKey: projectPathKey,
+ }, nil
+}
+
+func (m *Manager) StorePreparedTunnel(
+ prepared *gatewayv1.TunnelControlRequest,
+ targetURLOverride string,
+) (*gatewayv1.TunnelSummary, error) {
+ if prepared == nil {
+ return nil, errors.New("prepared tunnel is required")
+ }
+ now := time.Now()
+ targetURL := strings.TrimSpace(targetURLOverride)
+ if targetURL == "" {
+ targetURL = strings.TrimSpace(prepared.GetTargetUrl())
+ }
+ if targetURL == "" {
+ return nil, errors.New("target_url is required")
+ }
+ var expiresAt time.Time
+ if prepared.GetExpiresAt() > 0 {
+ expiresAt = time.Unix(prepared.GetExpiresAt(), 0)
+ } else if prepared.GetTtlSeconds() > 0 {
+ ttlSeconds, err := normalizeTunnelTTL(prepared.GetTtlSeconds())
+ if err != nil {
+ return nil, err
+ }
+ expiresAt = now.Add(time.Duration(ttlSeconds) * time.Second)
+ }
+ record := &tunnelRecord{
+ id: strings.TrimSpace(prepared.GetTunnelId()),
+ slug: strings.TrimSpace(prepared.GetSlug()),
+ name: strings.TrimSpace(prepared.GetName()),
+ targetURL: targetURL,
+ publicURL: normalizeTunnelPublicURL(prepared.GetPublicUrl()),
+ projectPathKey: strings.TrimSpace(prepared.GetProjectPathKey()),
+ createdAt: now,
+ expiresAt: expiresAt,
+ }
+ if record.id == "" || record.slug == "" {
+ return nil, errors.New("prepared tunnel is missing id or slug")
+ }
+ if record.publicURL == "" {
+ record.publicURL = buildTunnelPublicURL(prepared.GetPublicBaseUrl(), record.slug)
+ }
+
+ online := m.IsOnline()
+ m.tunnels.mu.Lock()
+ defer m.tunnels.mu.Unlock()
+ if _, exists := m.tunnels.tunnelsByID[record.id]; exists {
+ return nil, fmt.Errorf("tunnel id already exists")
+ }
+ if _, exists := m.tunnels.tunnelIDBySlug[record.slug]; exists {
+ return nil, fmt.Errorf("tunnel slug already exists")
+ }
+ m.tunnels.tunnelsByID[record.id] = record
+ m.tunnels.tunnelIDBySlug[record.slug] = record.id
+ return tunnelSummaryLocked(record, now, online), nil
+}
+
+func (m *Manager) CreateTunnelFromAgent(
+ input *gatewayv1.TunnelControlRequest,
+) (*gatewayv1.TunnelSummary, error) {
+ prepared, err := m.PrepareTunnelCreate(input, input.GetPublicBaseUrl())
+ if err != nil {
+ return nil, err
+ }
+ return m.StorePreparedTunnel(prepared, input.GetTargetUrl())
+}
+
+func (m *Manager) UpdateTunnelFromAgent(
+ input *gatewayv1.TunnelControlRequest,
+) (*gatewayv1.TunnelSummary, error) {
+ prepared, err := m.PrepareTunnelUpdate(input)
+ if err != nil {
+ return nil, err
+ }
+ return m.ApplyTunnelUpdate(&gatewayv1.TunnelSummary{
+ Id: prepared.GetTunnelId(),
+ Slug: prepared.GetSlug(),
+ Name: prepared.GetName(),
+ TargetUrl: prepared.GetTargetUrl(),
+ PublicUrl: prepared.GetPublicUrl(),
+ ExpiresAt: prepared.GetExpiresAt(),
+ ProjectPathKey: prepared.GetProjectPathKey(),
+ })
+}
+
+func (m *Manager) ApplyTunnelUpdate(summary *gatewayv1.TunnelSummary) (*gatewayv1.TunnelSummary, error) {
+ if summary == nil {
+ return nil, errors.New("tunnel update summary is required")
+ }
+ identifier := strings.TrimSpace(summary.GetId())
+ if identifier == "" {
+ identifier = strings.TrimSpace(summary.GetSlug())
+ }
+ if identifier == "" {
+ return nil, ErrTunnelNotFound
+ }
+ targetURL := strings.TrimSpace(summary.GetTargetUrl())
+ if targetURL == "" {
+ return nil, errors.New("target_url is required")
+ }
+ now := time.Now()
+ online := m.IsOnline()
+ var expiresAt time.Time
+ if summary.GetExpiresAt() > 0 {
+ expiresAt = time.Unix(summary.GetExpiresAt(), 0)
+ }
+
+ m.tunnels.mu.Lock()
+ defer m.tunnels.mu.Unlock()
+
+ tunnelID := identifier
+ if bySlug := m.tunnels.tunnelIDBySlug[identifier]; bySlug != "" {
+ tunnelID = bySlug
+ }
+ record := m.tunnels.tunnelsByID[tunnelID]
+ if record == nil || record.closed {
+ return nil, ErrTunnelNotFound
+ }
+ if isTunnelExpired(record, now) {
+ return nil, ErrTunnelExpired
+ }
+ record.name = strings.TrimSpace(summary.GetName())
+ record.targetURL = targetURL
+ if publicURL := normalizeTunnelPublicURL(summary.GetPublicUrl()); publicURL != "" {
+ record.publicURL = publicURL
+ }
+ record.projectPathKey = strings.TrimSpace(summary.GetProjectPathKey())
+ record.expiresAt = expiresAt
+ return tunnelSummaryLocked(record, now, online), nil
+}
+
+func (m *Manager) AcquireTunnel(slug string, streamID string) (*TunnelStreamLease, error) {
+ slug = strings.TrimSpace(slug)
+ streamID = strings.TrimSpace(streamID)
+ if slug == "" || streamID == "" {
+ return nil, ErrTunnelNotFound
+ }
+ if !m.IsOnline() {
+ return nil, ErrAgentOffline
+ }
+ now := time.Now()
+ online := true
+
+ m.tunnels.mu.Lock()
+ defer m.tunnels.mu.Unlock()
+
+ tunnelID := m.tunnels.tunnelIDBySlug[slug]
+ record := m.tunnels.tunnelsByID[tunnelID]
+ if record == nil || record.closed {
+ return nil, ErrTunnelNotFound
+ }
+ if isTunnelExpired(record, now) {
+ return nil, ErrTunnelExpired
+ }
+ if record.activeConnections >= MaxTunnelConnections {
+ return nil, ErrTunnelOverLimit
+ }
+ stream := &tunnelStream{
+ streamID: streamID,
+ tunnelID: record.id,
+ ch: make(chan *gatewayv1.TunnelFrame, tunnelStreamChannelDepth),
+ done: make(chan struct{}),
+ }
+ if existing := m.tunnels.streams[streamID]; existing != nil {
+ existing.close()
+ }
+ m.tunnels.streams[streamID] = stream
+ record.activeConnections += 1
+
+ return &TunnelStreamLease{
+ manager: m,
+ stream: stream,
+ tunnel: tunnelSummaryLocked(record, now, online),
+ }, nil
+}
+
+func (m *Manager) CloseTunnel(identifier string) (*gatewayv1.TunnelSummary, error) {
+ identifier = strings.TrimSpace(identifier)
+ if identifier == "" {
+ return nil, ErrTunnelNotFound
+ }
+ now := time.Now()
+ online := m.IsOnline()
+
+ var summary *gatewayv1.TunnelSummary
+ var cancelFrames []*gatewayv1.TunnelFrame
+ m.tunnels.mu.Lock()
+ tunnelID := identifier
+ if bySlug := m.tunnels.tunnelIDBySlug[identifier]; bySlug != "" {
+ tunnelID = bySlug
+ }
+ record := m.tunnels.tunnelsByID[tunnelID]
+ if record == nil || record.closed {
+ m.tunnels.mu.Unlock()
+ return nil, ErrTunnelNotFound
+ }
+ record.closed = true
+ summary = tunnelSummaryLocked(record, now, online)
+ delete(m.tunnels.tunnelsByID, record.id)
+ delete(m.tunnels.tunnelIDBySlug, record.slug)
+ for streamID, stream := range m.tunnels.streams {
+ if stream == nil || stream.tunnelID != record.id {
+ continue
+ }
+ delete(m.tunnels.streams, streamID)
+ stream.close()
+ cancelFrames = append(cancelFrames, &gatewayv1.TunnelFrame{
+ StreamId: stream.streamID,
+ TunnelId: record.id,
+ Slug: record.slug,
+ Kind: gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_CANCEL,
+ })
+ }
+ m.tunnels.mu.Unlock()
+
+ for _, frame := range cancelFrames {
+ _ = m.SendTunnelFrameToAgent(frame)
+ }
+ return summary, nil
+}
+
+func (m *Manager) ResumeTunnel(input *gatewayv1.TunnelControlRequest) (*gatewayv1.TunnelSummary, error) {
+ if input == nil {
+ return nil, errors.New("resume tunnel input is required")
+ }
+ now := time.Now()
+ online := m.IsOnline()
+ id := strings.TrimSpace(input.GetTunnelId())
+ slug := strings.TrimSpace(input.GetSlug())
+ if id == "" && slug == "" {
+ return nil, ErrTunnelNotFound
+ }
+
+ m.tunnels.mu.Lock()
+ defer m.tunnels.mu.Unlock()
+ if id == "" {
+ id = m.tunnels.tunnelIDBySlug[slug]
+ }
+ record := m.tunnels.tunnelsByID[id]
+ if record == nil || record.closed {
+ return nil, ErrTunnelNotFound
+ }
+ if slug != "" && record.slug != slug {
+ return nil, ErrTunnelNotFound
+ }
+ if isTunnelExpired(record, now) {
+ return nil, ErrTunnelExpired
+ }
+ if targetURL := strings.TrimSpace(input.GetTargetUrl()); targetURL != "" {
+ record.targetURL = targetURL
+ }
+ if name := strings.TrimSpace(input.GetName()); name != "" {
+ record.name = name
+ }
+ if projectPathKey := strings.TrimSpace(input.GetProjectPathKey()); projectPathKey != "" {
+ record.projectPathKey = projectPathKey
+ }
+ return tunnelSummaryLocked(record, now, online), nil
+}
+
+func (m *Manager) SendTunnelFrameToAgent(frame *gatewayv1.TunnelFrame) error {
+ if frame == nil {
+ return errors.New("tunnel frame is required")
+ }
+ ctx, cancel := context.WithTimeout(context.Background(), tunnelAgentSendTimeout)
+ defer cancel()
+ return m.SendToAgentContext(ctx, &gatewayv1.GatewayEnvelope{
+ RequestId: fmt.Sprintf("tunnel-frame-%s", strings.TrimSpace(frame.GetStreamId())),
+ Timestamp: time.Now().Unix(),
+ Payload: &gatewayv1.GatewayEnvelope_TunnelFrame{
+ TunnelFrame: frame,
+ },
+ })
+}
+
+func (m *Manager) dispatchTunnelFrame(frame *gatewayv1.TunnelFrame) {
+ if frame == nil {
+ return
+ }
+ streamID := strings.TrimSpace(frame.GetStreamId())
+ if streamID == "" {
+ return
+ }
+ m.tunnels.mu.Lock()
+ stream := m.tunnels.streams[streamID]
+ m.tunnels.mu.Unlock()
+ if stream == nil {
+ return
+ }
+ stream.send(frame)
+}
+
+func (m *Manager) handleAgentTunnelControl(
+ session *AgentSession,
+ requestID string,
+ request *gatewayv1.TunnelControlRequest,
+) {
+ if session == nil || request == nil {
+ return
+ }
+ response := m.handleAgentTunnelControlInner(request)
+ ctx, cancel := context.WithTimeout(context.Background(), tunnelAgentSendTimeout)
+ defer cancel()
+ _ = session.SendToAgentContext(ctx, &gatewayv1.GatewayEnvelope{
+ RequestId: requestID,
+ Timestamp: time.Now().Unix(),
+ Payload: &gatewayv1.GatewayEnvelope_TunnelControlResp{
+ TunnelControlResp: response,
+ },
+ })
+}
+
+func (m *Manager) handleAgentTunnelControlInner(
+ request *gatewayv1.TunnelControlRequest,
+) *gatewayv1.TunnelControlResponse {
+ action := strings.ToLower(strings.TrimSpace(request.GetAction()))
+ if action == "" {
+ return tunnelControlError("invalid_action", "tunnel action is required")
+ }
+ switch action {
+ case "list":
+ return &gatewayv1.TunnelControlResponse{
+ Action: action,
+ Tunnels: m.ListTunnels(),
+ }
+ case "create":
+ tunnel, err := m.CreateTunnelFromAgent(request)
+ if err != nil {
+ return tunnelControlErrorFor(action, err)
+ }
+ return &gatewayv1.TunnelControlResponse{
+ Action: action,
+ Tunnel: tunnel,
+ Tunnels: m.ListTunnels(),
+ }
+ case "update":
+ tunnel, err := m.UpdateTunnelFromAgent(request)
+ if err != nil {
+ return tunnelControlErrorFor(action, err)
+ }
+ return &gatewayv1.TunnelControlResponse{
+ Action: action,
+ Tunnel: tunnel,
+ Tunnels: m.ListTunnels(),
+ }
+ case "close":
+ identifier := request.GetTunnelId()
+ if strings.TrimSpace(identifier) == "" {
+ identifier = request.GetSlug()
+ }
+ tunnel, err := m.CloseTunnel(identifier)
+ if err != nil {
+ return tunnelControlErrorFor(action, err)
+ }
+ return &gatewayv1.TunnelControlResponse{
+ Action: action,
+ Tunnel: tunnel,
+ Tunnels: m.ListTunnels(),
+ }
+ case "resume":
+ tunnel, err := m.ResumeTunnel(request)
+ if err != nil {
+ return tunnelControlErrorFor(action, err)
+ }
+ return &gatewayv1.TunnelControlResponse{
+ Action: action,
+ Tunnel: tunnel,
+ Tunnels: m.ListTunnels(),
+ }
+ default:
+ return tunnelControlError("invalid_action", "unsupported tunnel action")
+ }
+}
+
+func (m *Manager) releaseTunnelStream(stream *tunnelStream) {
+ if stream == nil {
+ return
+ }
+ m.tunnels.mu.Lock()
+ if existing := m.tunnels.streams[stream.streamID]; existing == stream {
+ delete(m.tunnels.streams, stream.streamID)
+ }
+ if record := m.tunnels.tunnelsByID[stream.tunnelID]; record != nil && record.activeConnections > 0 {
+ record.activeConnections -= 1
+ }
+ stream.close()
+ m.tunnels.mu.Unlock()
+}
+
+func normalizeTunnelTTL(input uint32) (uint32, error) {
+ switch input {
+ case 0:
+ return 0, nil
+ case 900, 3600, 14400:
+ return input, nil
+ default:
+ return 0, errors.New("ttl_seconds must be one of 0, 900, 3600, or 14400")
+ }
+}
+
+func tunnelUnix(value time.Time) int64 {
+ if value.IsZero() {
+ return 0
+ }
+ return value.Unix()
+}
+
+func generateTunnelID() string {
+ return "tun_" + strings.ReplaceAll(time.Now().UTC().Format("20060102150405.000000000"), ".", "") + "_" + randomURLToken(8)
+}
+
+func generateTunnelSlug() (string, error) {
+ token := randomURLToken(tunnelSlugEntropyBytes)
+ if token == "" {
+ return "", errors.New("generate tunnel slug failed")
+ }
+ return token, nil
+}
+
+func randomURLToken(byteCount int) string {
+ if byteCount <= 0 {
+ return ""
+ }
+ buf := make([]byte, byteCount)
+ if _, err := rand.Read(buf); err != nil {
+ return ""
+ }
+ return base64.RawURLEncoding.EncodeToString(buf)
+}
+
+func normalizeTunnelPublicURL(input string) string {
+ trimmed := strings.TrimSpace(input)
+ if trimmed == "" {
+ return ""
+ }
+ parsed, err := url.Parse(trimmed)
+ if err != nil || parsed.Scheme == "" || parsed.Host == "" {
+ return ""
+ }
+ parsed.RawQuery = ""
+ parsed.Fragment = ""
+ if !strings.HasSuffix(parsed.Path, "/") {
+ parsed.Path += "/"
+ }
+ return parsed.String()
+}
+
+func buildTunnelPublicURL(publicBaseURL string, slug string) string {
+ base := strings.TrimSpace(publicBaseURL)
+ if base == "" || strings.TrimSpace(slug) == "" {
+ return ""
+ }
+ parsed, err := url.Parse(base)
+ if err != nil || parsed.Scheme == "" || parsed.Host == "" {
+ return ""
+ }
+ parsed.RawQuery = ""
+ parsed.Fragment = ""
+ parsed.Path = strings.TrimRight(parsed.Path, "/") + "/t/" + strings.TrimSpace(slug) + "/"
+ return parsed.String()
+}
+
+func isTunnelExpired(record *tunnelRecord, now time.Time) bool {
+ return record == nil || (!record.expiresAt.IsZero() && !record.expiresAt.After(now))
+}
+
+func tunnelSummaryLocked(record *tunnelRecord, now time.Time, online bool) *gatewayv1.TunnelSummary {
+ if record == nil {
+ return &gatewayv1.TunnelSummary{Status: "expired"}
+ }
+ status := "active"
+ if record.closed || isTunnelExpired(record, now) {
+ status = "expired"
+ } else if !online {
+ status = "offline"
+ }
+ activeConnections := uint32(0)
+ if record.activeConnections > 0 {
+ activeConnections = uint32(record.activeConnections)
+ }
+ return &gatewayv1.TunnelSummary{
+ Id: record.id,
+ Slug: record.slug,
+ Name: record.name,
+ TargetUrl: record.targetURL,
+ PublicUrl: record.publicURL,
+ CreatedAt: record.createdAt.Unix(),
+ ExpiresAt: tunnelUnix(record.expiresAt),
+ ActiveConnections: activeConnections,
+ Status: status,
+ ProjectPathKey: record.projectPathKey,
+ }
+}
+
+func cloneTunnelSummary(summary *gatewayv1.TunnelSummary) *gatewayv1.TunnelSummary {
+ if summary == nil {
+ return nil
+ }
+ return &gatewayv1.TunnelSummary{
+ Id: summary.GetId(),
+ Slug: summary.GetSlug(),
+ Name: summary.GetName(),
+ TargetUrl: summary.GetTargetUrl(),
+ PublicUrl: summary.GetPublicUrl(),
+ CreatedAt: summary.GetCreatedAt(),
+ ExpiresAt: summary.GetExpiresAt(),
+ ActiveConnections: summary.GetActiveConnections(),
+ Status: summary.GetStatus(),
+ ProjectPathKey: strings.TrimSpace(summary.GetProjectPathKey()),
+ }
+}
+
+func sortTunnelSummaries(summaries []*gatewayv1.TunnelSummary) {
+ for i := 1; i < len(summaries); i++ {
+ current := summaries[i]
+ j := i - 1
+ for j >= 0 && summaries[j].GetCreatedAt() > current.GetCreatedAt() {
+ summaries[j+1] = summaries[j]
+ j--
+ }
+ summaries[j+1] = current
+ }
+}
+
+func tunnelControlError(code string, message string) *gatewayv1.TunnelControlResponse {
+ return &gatewayv1.TunnelControlResponse{
+ ErrorCode: strings.TrimSpace(code),
+ ErrorMessage: strings.TrimSpace(message),
+ }
+}
+
+func tunnelControlErrorFor(action string, err error) *gatewayv1.TunnelControlResponse {
+ code := "failed"
+ switch {
+ case errors.Is(err, ErrTunnelNotFound):
+ code = "not_found"
+ case errors.Is(err, ErrTunnelExpired):
+ code = "expired"
+ case errors.Is(err, ErrTunnelLimitExceeded):
+ code = "limit_exceeded"
+ case errors.Is(err, ErrTunnelOverLimit):
+ code = "over_limit"
+ case errors.Is(err, ErrAgentOffline):
+ code = "agent_offline"
+ }
+ return &gatewayv1.TunnelControlResponse{
+ Action: strings.TrimSpace(action),
+ ErrorCode: code,
+ ErrorMessage: err.Error(),
+ }
+}
diff --git a/crates/agent-gateway/internal/session/manager_tunnel_test.go b/crates/agent-gateway/internal/session/manager_tunnel_test.go
new file mode 100644
index 000000000..1e147a0b3
--- /dev/null
+++ b/crates/agent-gateway/internal/session/manager_tunnel_test.go
@@ -0,0 +1,265 @@
+package session
+
+import (
+ "errors"
+ "testing"
+ "time"
+
+ gatewayv1 "github.com/liveagent/agent-gateway/internal/proto/v1"
+)
+
+func onlineTunnelTestManager() *Manager {
+ manager := NewManager()
+ manager.SetSession(NewAgentSession(AuthSnapshot{
+ AgentID: "agent-a",
+ AgentVersion: "test",
+ SessionID: "session-a",
+ }))
+ return manager
+}
+
+func createTestTunnel(t *testing.T, manager *Manager, name string) *gatewayv1.TunnelSummary {
+ t.Helper()
+ tunnel, err := manager.CreateTunnelFromAgent(&gatewayv1.TunnelControlRequest{
+ Action: "create",
+ TargetUrl: "http://localhost:3000/app",
+ Name: name,
+ TtlSeconds: 3600,
+ PublicBaseUrl: "https://gateway.example",
+ })
+ if err != nil {
+ t.Fatalf("CreateTunnelFromAgent: %v", err)
+ }
+ if tunnel.GetSlug() == "" || tunnel.GetPublicUrl() == "" {
+ t.Fatalf("created tunnel missing slug/public URL: %+v", tunnel)
+ }
+ return tunnel
+}
+
+func TestTunnelRegistryCreateLimitListAndClose(t *testing.T) {
+ manager := onlineTunnelTestManager()
+
+ var first *gatewayv1.TunnelSummary
+ for i := 0; i < MaxTunnelsPerAgent; i++ {
+ tunnel := createTestTunnel(t, manager, "app")
+ if i == 0 {
+ first = tunnel
+ }
+ }
+
+ if _, err := manager.CreateTunnelFromAgent(&gatewayv1.TunnelControlRequest{
+ Action: "create",
+ TargetUrl: "http://localhost:3001",
+ TtlSeconds: 3600,
+ PublicBaseUrl: "https://gateway.example",
+ }); !errors.Is(err, ErrTunnelLimitExceeded) {
+ t.Fatalf("expected ErrTunnelLimitExceeded, got %v", err)
+ }
+
+ if got := len(manager.ListTunnels()); got != MaxTunnelsPerAgent {
+ t.Fatalf("ListTunnels returned %d tunnels, want %d", got, MaxTunnelsPerAgent)
+ }
+
+ closed, err := manager.CloseTunnel(first.GetId())
+ if err != nil {
+ t.Fatalf("CloseTunnel: %v", err)
+ }
+ if closed.GetStatus() != "expired" {
+ t.Fatalf("closed tunnel summary status = %q, want expired", closed.GetStatus())
+ }
+ if got := len(manager.ListTunnels()); got != MaxTunnelsPerAgent-1 {
+ t.Fatalf("ListTunnels after close returned %d tunnels, want %d", got, MaxTunnelsPerAgent-1)
+ }
+}
+
+func TestTunnelAcquireConnectionLimitAndRelease(t *testing.T) {
+ manager := onlineTunnelTestManager()
+ tunnel := createTestTunnel(t, manager, "app")
+
+ leases := make([]*TunnelStreamLease, 0, MaxTunnelConnections)
+ for i := 0; i < MaxTunnelConnections; i++ {
+ lease, err := manager.AcquireTunnel(tunnel.GetSlug(), "stream-"+string(rune('a'+i)))
+ if err != nil {
+ t.Fatalf("AcquireTunnel %d: %v", i, err)
+ }
+ leases = append(leases, lease)
+ }
+ if _, err := manager.AcquireTunnel(tunnel.GetSlug(), "stream-over-limit"); !errors.Is(err, ErrTunnelOverLimit) {
+ t.Fatalf("expected ErrTunnelOverLimit, got %v", err)
+ }
+
+ leases[0].Release()
+ lease, err := manager.AcquireTunnel(tunnel.GetSlug(), "stream-after-release")
+ if err != nil {
+ t.Fatalf("AcquireTunnel after release: %v", err)
+ }
+ lease.Release()
+ for _, item := range leases[1:] {
+ item.Release()
+ }
+
+ summaries := manager.ListTunnels()
+ if len(summaries) != 1 {
+ t.Fatalf("ListTunnels returned %d tunnels, want 1", len(summaries))
+ }
+ if got := summaries[0].GetActiveConnections(); got != 0 {
+ t.Fatalf("active connections after release = %d, want 0", got)
+ }
+}
+
+func TestTunnelExpiredCannotBeAcquired(t *testing.T) {
+ manager := onlineTunnelTestManager()
+ tunnel := createTestTunnel(t, manager, "app")
+
+ manager.tunnels.mu.Lock()
+ manager.tunnels.tunnelsByID[tunnel.GetId()].expiresAt = time.Now().Add(-time.Second)
+ manager.tunnels.mu.Unlock()
+
+ if _, err := manager.AcquireTunnel(tunnel.GetSlug(), "stream-expired"); !errors.Is(err, ErrTunnelExpired) {
+ t.Fatalf("expected ErrTunnelExpired, got %v", err)
+ }
+ summaries := manager.ListTunnels()
+ if len(summaries) != 1 {
+ t.Fatalf("ListTunnels returned %d tunnels, want 1", len(summaries))
+ }
+ if summaries[0].GetStatus() != "expired" {
+ t.Fatalf("expired tunnel status = %q, want expired", summaries[0].GetStatus())
+ }
+}
+
+func TestTunnelInfiniteTTLCreatesNonExpiringTunnel(t *testing.T) {
+ manager := onlineTunnelTestManager()
+ tunnel, err := manager.CreateTunnelFromAgent(&gatewayv1.TunnelControlRequest{
+ Action: "create",
+ TargetUrl: "http://localhost:3000/app",
+ Name: "app",
+ TtlSeconds: 0,
+ PublicBaseUrl: "https://gateway.example",
+ })
+ if err != nil {
+ t.Fatalf("CreateTunnelFromAgent with infinite TTL: %v", err)
+ }
+ if tunnel.GetExpiresAt() != 0 {
+ t.Fatalf("infinite tunnel expiresAt = %d, want 0", tunnel.GetExpiresAt())
+ }
+ if tunnel.GetStatus() != "active" {
+ t.Fatalf("infinite tunnel status = %q, want active", tunnel.GetStatus())
+ }
+
+ manager.tunnels.mu.Lock()
+ manager.tunnels.tunnelsByID[tunnel.GetId()].expiresAt = time.Time{}
+ manager.tunnels.mu.Unlock()
+
+ lease, err := manager.AcquireTunnel(tunnel.GetSlug(), "stream-infinite")
+ if err != nil {
+ t.Fatalf("AcquireTunnel for infinite tunnel: %v", err)
+ }
+ lease.Release()
+}
+
+func TestTunnelUpdateChangesTargetNameScopeAndTTL(t *testing.T) {
+ manager := onlineTunnelTestManager()
+ tunnel := createTestTunnel(t, manager, "app")
+
+ updated, err := manager.UpdateTunnelFromAgent(&gatewayv1.TunnelControlRequest{
+ Action: "update",
+ TunnelId: tunnel.GetId(),
+ TargetUrl: "http://127.0.0.1:4000/dashboard",
+ Name: "dashboard",
+ TtlSeconds: 0,
+ ProjectPathKey: "project:/tmp/liveagent",
+ })
+ if err != nil {
+ t.Fatalf("UpdateTunnelFromAgent: %v", err)
+ }
+ if updated.GetName() != "dashboard" {
+ t.Fatalf("updated name = %q, want dashboard", updated.GetName())
+ }
+ if updated.GetTargetUrl() != "http://127.0.0.1:4000/dashboard" {
+ t.Fatalf("updated target = %q", updated.GetTargetUrl())
+ }
+ if updated.GetExpiresAt() != 0 {
+ t.Fatalf("updated expiresAt = %d, want 0", updated.GetExpiresAt())
+ }
+ if updated.GetProjectPathKey() != "project:/tmp/liveagent" {
+ t.Fatalf("updated projectPathKey = %q", updated.GetProjectPathKey())
+ }
+
+ listed := manager.ListTunnels()
+ if len(listed) != 1 {
+ t.Fatalf("ListTunnels returned %d tunnels, want 1", len(listed))
+ }
+ if listed[0].GetId() != tunnel.GetId() || listed[0].GetTargetUrl() != updated.GetTargetUrl() {
+ t.Fatalf("ListTunnels did not include updated tunnel: %+v", listed[0])
+ }
+}
+
+func TestTunnelInfiniteTTLStaysActiveAndVisible(t *testing.T) {
+ manager := onlineTunnelTestManager()
+
+ tunnel, err := manager.CreateTunnelFromAgent(&gatewayv1.TunnelControlRequest{
+ Action: "create",
+ TargetUrl: "http://localhost:3000/app",
+ Name: "app",
+ TtlSeconds: 0,
+ PublicBaseUrl: "https://gateway.example",
+ ProjectPathKey: "/workspace/app",
+ })
+ if err != nil {
+ t.Fatalf("CreateTunnelFromAgent: %v", err)
+ }
+ if tunnel.GetExpiresAt() != 0 {
+ t.Fatalf("infinite tunnel expires_at = %d, want 0", tunnel.GetExpiresAt())
+ }
+ if tunnel.GetProjectPathKey() != "/workspace/app" {
+ t.Fatalf("project_path_key = %q, want /workspace/app", tunnel.GetProjectPathKey())
+ }
+
+ summaries := manager.ListTunnels()
+ if len(summaries) != 1 {
+ t.Fatalf("ListTunnels returned %d tunnels, want 1", len(summaries))
+ }
+ if summaries[0].GetStatus() != "active" {
+ t.Fatalf("infinite tunnel status = %q, want active", summaries[0].GetStatus())
+ }
+ if summaries[0].GetExpiresAt() != 0 {
+ t.Fatalf("listed infinite tunnel expires_at = %d, want 0", summaries[0].GetExpiresAt())
+ }
+}
+
+func TestTunnelUpdateChangesTargetNameTTLAndKeepsProjectScope(t *testing.T) {
+ manager := onlineTunnelTestManager()
+ tunnel := createTestTunnel(t, manager, "app")
+
+ updated, err := manager.UpdateTunnelFromAgent(&gatewayv1.TunnelControlRequest{
+ Action: "update",
+ TunnelId: tunnel.GetId(),
+ TargetUrl: "http://localhost:3000/next",
+ Name: "next",
+ TtlSeconds: 0,
+ ProjectPathKey: "/workspace/app",
+ })
+ if err != nil {
+ t.Fatalf("UpdateTunnelFromAgent: %v", err)
+ }
+ if updated.GetTargetUrl() != "http://localhost:3000/next" {
+ t.Fatalf("target_url = %q, want http://localhost:3000/next", updated.GetTargetUrl())
+ }
+ if updated.GetName() != "next" {
+ t.Fatalf("name = %q, want next", updated.GetName())
+ }
+ if updated.GetExpiresAt() != 0 {
+ t.Fatalf("updated expires_at = %d, want 0", updated.GetExpiresAt())
+ }
+ if updated.GetProjectPathKey() != "/workspace/app" {
+ t.Fatalf("project_path_key = %q, want /workspace/app", updated.GetProjectPathKey())
+ }
+
+ listed := manager.ListTunnels()
+ if len(listed) != 1 {
+ t.Fatalf("ListTunnels returned %d tunnels, want 1", len(listed))
+ }
+ if listed[0].GetTargetUrl() != "http://localhost:3000/next" {
+ t.Fatalf("listed target_url = %q, want http://localhost:3000/next", listed[0].GetTargetUrl())
+ }
+}
diff --git a/crates/agent-gateway/proto/v1/gateway.proto b/crates/agent-gateway/proto/v1/gateway.proto
index 94a0df3c6..e187c39be 100644
--- a/crates/agent-gateway/proto/v1/gateway.proto
+++ b/crates/agent-gateway/proto/v1/gateway.proto
@@ -63,6 +63,9 @@ message GatewayEnvelope {
GitRequest git_request = 61;
FsReadEditableTextRequest fs_read_editable_text = 62;
FsReadWorkspaceImageRequest fs_read_workspace_image = 63;
+ TunnelControlRequest tunnel_control = 67;
+ TunnelControlResponse tunnel_control_resp = 68;
+ TunnelFrame tunnel_frame = 69;
}
}
@@ -110,6 +113,9 @@ message AgentEnvelope {
GitResponse git_response = 64;
FsReadEditableTextResponse fs_read_editable_text_resp = 65;
FsReadWorkspaceImageResponse fs_read_workspace_image_resp = 66;
+ TunnelControlRequest tunnel_control = 67;
+ TunnelControlResponse tunnel_control_resp = 68;
+ TunnelFrame tunnel_frame = 69;
ErrorResponse error = 99;
}
}
@@ -160,6 +166,75 @@ message UploadedImagePreviewResponse {
string data = 2;
}
+message TunnelControlRequest {
+ string action = 1;
+ string tunnel_id = 2;
+ string slug = 3;
+ string target_url = 4;
+ string name = 5;
+ uint32 ttl_seconds = 6;
+ int64 expires_at = 7;
+ string public_url = 8;
+ string public_base_url = 9;
+ string project_path_key = 10;
+}
+
+message TunnelControlResponse {
+ string action = 1;
+ repeated TunnelSummary tunnels = 2;
+ TunnelSummary tunnel = 3;
+ string error_code = 4;
+ string error_message = 5;
+}
+
+message TunnelSummary {
+ string id = 1;
+ string slug = 2;
+ string name = 3;
+ string target_url = 4;
+ string public_url = 5;
+ int64 created_at = 6;
+ int64 expires_at = 7;
+ uint32 active_connections = 8;
+ string status = 9;
+ string project_path_key = 10;
+}
+
+message TunnelHeader {
+ string name = 1;
+ string value = 2;
+}
+
+enum TunnelFrameKind {
+ TUNNEL_FRAME_KIND_UNSPECIFIED = 0;
+ TUNNEL_FRAME_KIND_HTTP_REQUEST_START = 1;
+ TUNNEL_FRAME_KIND_HTTP_REQUEST_BODY = 2;
+ TUNNEL_FRAME_KIND_HTTP_REQUEST_END = 3;
+ TUNNEL_FRAME_KIND_HTTP_RESPONSE_START = 4;
+ TUNNEL_FRAME_KIND_HTTP_RESPONSE_BODY = 5;
+ TUNNEL_FRAME_KIND_HTTP_RESPONSE_END = 6;
+ TUNNEL_FRAME_KIND_WS_OPEN = 7;
+ TUNNEL_FRAME_KIND_WS_FRAME = 8;
+ TUNNEL_FRAME_KIND_WS_CLOSE = 9;
+ TUNNEL_FRAME_KIND_ERROR = 10;
+ TUNNEL_FRAME_KIND_CANCEL = 11;
+}
+
+message TunnelFrame {
+ string stream_id = 1;
+ string tunnel_id = 2;
+ string slug = 3;
+ TunnelFrameKind kind = 4;
+ string method = 5;
+ string path = 6;
+ repeated TunnelHeader headers = 7;
+ uint32 status_code = 8;
+ bytes body = 9;
+ bool end_stream = 10;
+ string error = 11;
+ string ws_message_type = 12;
+}
+
message MemoryManageRequest {
string command = 1;
string args_json = 2;
diff --git a/crates/agent-gateway/test/session/manager_test.go b/crates/agent-gateway/test/session/manager_test.go
index 53d5e645c..4def205b9 100644
--- a/crates/agent-gateway/test/session/manager_test.go
+++ b/crates/agent-gateway/test/session/manager_test.go
@@ -1,6 +1,7 @@
package session_test
import (
+ "context"
"errors"
"fmt"
"strings"
@@ -158,6 +159,28 @@ func TestSendToAgentUnblocksWhenSessionCloses(t *testing.T) {
}
}
+func TestSendToAgentContextReturnsWhenOutboundQueueIsFull(t *testing.T) {
+ t.Parallel()
+
+ sm := newTestSessionManager()
+ sess := session.NewAgentSession(sm.LatestAuthSnapshot())
+ sm.SetSession(sess)
+
+ for i := 0; i < 64; i += 1 {
+ if err := sm.SendToAgent(&gatewayv1.GatewayEnvelope{RequestId: fmt.Sprintf("queued-%d", i)}); err != nil {
+ t.Fatalf("prime outbound queue: %v", err)
+ }
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
+ defer cancel()
+
+ err := sm.SendToAgentContext(ctx, &gatewayv1.GatewayEnvelope{RequestId: "blocked"})
+ if !errors.Is(err, context.DeadlineExceeded) {
+ t.Fatalf("SendToAgentContext with full queue = %v, want context deadline exceeded", err)
+ }
+}
+
func TestRemoveChatRunByConversationReleasesBufferedRun(t *testing.T) {
t.Parallel()
diff --git a/crates/agent-gateway/test/upload/import_readable_files_test.go b/crates/agent-gateway/test/upload/import_readable_files_test.go
index 7b26d803a..d25c08480 100644
--- a/crates/agent-gateway/test/upload/import_readable_files_test.go
+++ b/crates/agent-gateway/test/upload/import_readable_files_test.go
@@ -65,7 +65,8 @@ func TestImportReadableFilesForwardsMultipartToAgent(t *testing.T) {
var outbound *gatewayv1.GatewayEnvelope
select {
- case outbound = <-agentSession.Outbound():
+ case delivered := <-agentSession.Outbound():
+ outbound = delivered.GatewayEnvelope
case <-time.After(time.Second):
t.Fatalf("timed out waiting for upload request to reach agent")
}
diff --git a/crates/agent-gateway/test/websocket/chat_bridge_test.go b/crates/agent-gateway/test/websocket/chat_bridge_test.go
index 194c9c0bc..f20742cac 100644
--- a/crates/agent-gateway/test/websocket/chat_bridge_test.go
+++ b/crates/agent-gateway/test/websocket/chat_bridge_test.go
@@ -90,7 +90,8 @@ func readOutboundEnvelope(t *testing.T, agentSession *session.AgentSession) *gat
t.Helper()
select {
case outbound := <-agentSession.Outbound():
- return outbound
+ outbound.Ack(nil)
+ return outbound.GatewayEnvelope
case <-time.After(time.Second):
t.Fatalf("timed out waiting for gateway request to reach agent")
return nil
@@ -184,7 +185,9 @@ func TestWebSocketChatStartForwardsNormalizedRequestAndStreamsEvents(t *testing.
var outbound *gatewayv1.GatewayEnvelope
select {
- case outbound = <-agentSession.Outbound():
+ case delivered := <-agentSession.Outbound():
+ delivered.Ack(nil)
+ outbound = delivered.GatewayEnvelope
case <-time.After(time.Second):
t.Fatalf("timed out waiting for chat request to reach agent")
}
@@ -278,6 +281,46 @@ func TestWebSocketChatStartForwardsNormalizedRequestAndStreamsEvents(t *testing.
}
}
+func TestWebSocketChatStartClearsRunWhenAgentDeliveryStalls(t *testing.T) {
+ t.Parallel()
+
+ sm := session.NewManager()
+ sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1")
+ agentSession := session.NewAgentSession(sm.LatestAuthSnapshot())
+ sm.SetSession(agentSession)
+
+ handler := server.NewWebSocketServer(&config.Config{
+ Token: "ws-token",
+ RequestTimeout: time.Second,
+ WebSocketWriteTimeout: 50 * time.Millisecond,
+ }, sm)
+ conn, cleanup := dialGatewayWebSocket(t, handler)
+ defer cleanup()
+
+ authWebSocket(t, conn, "ws-token")
+ sendEnvelope(t, conn, "chat-stalled", "chat.start", map[string]any{
+ "conversation_id": "conversation-stalled",
+ "message": "hello gateway",
+ })
+
+ select {
+ case outbound := <-agentSession.Outbound():
+ if outbound.GetChatRequest() == nil {
+ t.Fatalf("outbound payload = %T, want ChatRequest", outbound.GetPayload())
+ }
+ case <-time.After(time.Second):
+ t.Fatalf("timed out waiting for chat request to be enqueued")
+ }
+
+ env := receiveEnvelope(t, conn)
+ if env.ID != "chat-stalled" || env.Type != "error" || env.Error != "request timed out" {
+ t.Fatalf("stalled delivery response = %#v, want timeout error", env)
+ }
+ if got := sm.ActiveChatRunConversationIDs(); len(got) != 0 {
+ t.Fatalf("active chat runs after stalled delivery = %#v, want empty", got)
+ }
+}
+
func TestWebSocketChatStartDedupesClientRequestID(t *testing.T) {
t.Parallel()
diff --git a/crates/agent-gateway/test/webui/gateway-socket-client.test.mjs b/crates/agent-gateway/test/webui/gateway-socket-client.test.mjs
index 43e511971..53f864fa9 100644
--- a/crates/agent-gateway/test/webui/gateway-socket-client.test.mjs
+++ b/crates/agent-gateway/test/webui/gateway-socket-client.test.mjs
@@ -1618,6 +1618,208 @@ test("Gateway SharedWorker forwards history share requests", async () => {
globalThis.onconnect = previousOnConnect;
});
+test("Gateway SharedWorker forwards tunnel requests", async () => {
+ installBrowser();
+ const loader = createWebModuleLoader();
+ const gatewaySocketPath = loader.resolveLocal("src/lib/gatewaySocket.ts");
+ const clientInstances = [];
+
+ class MockGatewayWebSocketClient {
+ calls = [];
+
+ constructor(token) {
+ this.token = token;
+ clientInstances.push(this);
+ }
+
+ subscribeStatus() {
+ return () => {};
+ }
+
+ subscribeHistory() {
+ return () => {};
+ }
+
+ subscribeConversation() {
+ return () => {};
+ }
+
+ subscribeSettings() {
+ return () => {};
+ }
+
+ subscribeTerminal() {
+ return () => {};
+ }
+
+ listTunnels() {
+ this.calls.push(["listTunnels"]);
+ return [
+ {
+ id: "tun-1",
+ slug: "slug-1",
+ name: "App",
+ targetUrl: "http://localhost:3000",
+ publicUrl: "https://gateway.example/t/slug-1/",
+ createdAt: 10,
+ expiresAt: 3700,
+ activeConnections: 0,
+ status: "active",
+ },
+ ];
+ }
+
+ createTunnel(input) {
+ this.calls.push(["createTunnel", input]);
+ return {
+ id: "tun-2",
+ slug: "slug-2",
+ name: input.name ?? "",
+ targetUrl: input.targetUrl,
+ publicUrl: "https://gateway.example/t/slug-2/",
+ createdAt: 20,
+ expiresAt: 920,
+ activeConnections: 0,
+ status: "active",
+ };
+ }
+
+ updateTunnel(input) {
+ this.calls.push(["updateTunnel", input]);
+ return {
+ id: input.id,
+ slug: "slug-2",
+ name: input.name ?? "",
+ targetUrl: input.targetUrl,
+ publicUrl: "https://gateway.example/t/slug-2/",
+ createdAt: 20,
+ expiresAt: input.ttlSeconds === 0 ? 0 : 920,
+ activeConnections: 0,
+ status: "active",
+ projectPathKey: input.projectPathKey ?? "",
+ };
+ }
+
+ closeTunnel(id) {
+ this.calls.push(["closeTunnel", id]);
+ return {
+ id,
+ slug: "slug-2",
+ name: "Closed",
+ targetUrl: "http://localhost:3000",
+ publicUrl: "https://gateway.example/t/slug-2/",
+ createdAt: 20,
+ expiresAt: 920,
+ activeConnections: 0,
+ status: "expired",
+ };
+ }
+
+ dispose() {}
+ }
+
+ const workerLoader = createWebModuleLoader({
+ mocks: {
+ [gatewaySocketPath]: {
+ GatewayWebSocketClient: MockGatewayWebSocketClient,
+ },
+ },
+ });
+
+ const previousOnConnect = globalThis.onconnect;
+ workerLoader.loadModule("src/lib/gatewaySocket.worker.ts");
+
+ const port = new FakeMessagePort();
+ globalThis.onconnect({ ports: [port] });
+ port.emit({ type: "connect", connection_id: "connection-1", token: " token " });
+ assert.equal(clientInstances.length, 1);
+
+ port.emit({
+ type: "request",
+ connection_id: "connection-1",
+ request_id: "tunnel-list",
+ method: "tunnel.list",
+ payload: {},
+ });
+ await waitFor(
+ () => port.messages.some((message) => message.request_id === "tunnel-list"),
+ "shared worker tunnel list response",
+ );
+ assert.deepEqual(clientInstances[0].calls.at(-1), ["listTunnels"]);
+ assert.equal(port.messages.at(-1).payload.tunnels[0].id, "tun-1");
+
+ port.emit({
+ type: "request",
+ connection_id: "connection-1",
+ request_id: "tunnel-create",
+ method: "tunnel.create",
+ payload: {
+ targetUrl: "http://localhost:3000/app",
+ ttlSeconds: 900,
+ name: "App",
+ },
+ });
+ await waitFor(
+ () => port.messages.some((message) => message.request_id === "tunnel-create"),
+ "shared worker tunnel create response",
+ );
+ assert.deepEqual(clientInstances[0].calls.at(-1), [
+ "createTunnel",
+ {
+ targetUrl: "http://localhost:3000/app",
+ ttlSeconds: 900,
+ name: "App",
+ },
+ ]);
+ assert.equal(port.messages.at(-1).payload.tunnel.id, "tun-2");
+
+ port.emit({
+ type: "request",
+ connection_id: "connection-1",
+ request_id: "tunnel-update-infinite",
+ method: "tunnel.update",
+ payload: {
+ id: "tun-2",
+ targetUrl: "http://localhost:4000/dashboard",
+ ttlSeconds: 0,
+ name: "Dashboard",
+ projectPathKey: "project:/tmp/liveagent",
+ },
+ });
+ await waitFor(
+ () => port.messages.some((message) => message.request_id === "tunnel-update-infinite"),
+ "shared worker tunnel update response",
+ );
+ assert.deepEqual(clientInstances[0].calls.at(-1), [
+ "updateTunnel",
+ {
+ id: "tun-2",
+ targetUrl: "http://localhost:4000/dashboard",
+ ttlSeconds: 0,
+ name: "Dashboard",
+ projectPathKey: "project:/tmp/liveagent",
+ },
+ ]);
+ assert.equal(port.messages.at(-1).payload.tunnel.expiresAt, 0);
+ assert.equal(port.messages.at(-1).payload.tunnel.projectPathKey, "project:/tmp/liveagent");
+
+ port.emit({
+ type: "request",
+ connection_id: "connection-1",
+ request_id: "tunnel-close",
+ method: "tunnel.close",
+ payload: { id: "tun-2" },
+ });
+ await waitFor(
+ () => port.messages.some((message) => message.request_id === "tunnel-close"),
+ "shared worker tunnel close response",
+ );
+ assert.deepEqual(clientInstances[0].calls.at(-1), ["closeTunnel", "tun-2"]);
+ assert.equal(port.messages.at(-1).payload.tunnel.status, "expired");
+
+ globalThis.onconnect = previousOnConnect;
+});
+
test("Gateway SharedWorker forwards chat.attach streams to the requesting port", async () => {
installBrowser();
const loader = createWebModuleLoader();
diff --git a/crates/agent-gateway/test/webui/web-settings.test.mjs b/crates/agent-gateway/test/webui/web-settings.test.mjs
index 5f28d7086..0532fa4bd 100644
--- a/crates/agent-gateway/test/webui/web-settings.test.mjs
+++ b/crates/agent-gateway/test/webui/web-settings.test.mjs
@@ -198,6 +198,10 @@ test("loadWebSettings forces current gateway URL/token over stale persisted remo
openProjectPathKeys: ["/stale/project"],
openVersion: 1,
};
+ stale.customSettings.projectToolsTunnel = {
+ openProjectPathKeys: ["/stale/project"],
+ openVersion: 1,
+ };
store.set("liveagent.gateway.webui.settings.v1", JSON.stringify(stale));
const loaded = webSettings.loadWebSettings(" new-token ");
@@ -207,6 +211,7 @@ test("loadWebSettings forces current gateway URL/token over stale persisted remo
assert.equal(loaded.remote.enabled, true);
assert.deepEqual(loaded.customSettings.projectToolsFileTree.openProjectPathKeys, []);
assert.deepEqual(loaded.customSettings.projectToolsGitReview.openProjectPathKeys, []);
+ assert.deepEqual(loaded.customSettings.projectToolsTunnel.openProjectPathKeys, []);
});
test("gateway settings sync keeps remote connection local and syncs web terminal setting", () => {
@@ -249,6 +254,7 @@ test("gateway settings sync keeps remote connection local and syncs web terminal
assert.deepEqual(payload.remote, {
enableWebTerminal: synced.remote.enableWebTerminal,
enableWebGit: synced.remote.enableWebGit,
+ enableWebTunnels: synced.remote.enableWebTunnels,
});
assert.deepEqual(payload.chatRuntimeControls, synced.chatRuntimeControls);
});
@@ -338,48 +344,108 @@ test("gateway settings sync preserves active workspace project by path when ids
assert.equal(synced.system.activeWorkspaceProjectId, "desktop-project-a");
});
-test("gateway settings sync keeps newer git review tab open state", () => {
+test("gateway settings sync keeps newer project tool tab open state", () => {
installWindow();
const current = settings.normalizeSettings({
customSettings: {
+ projectToolsPanel: {
+ width: 612,
+ activeTab: "gitReview",
+ tabOrders: {
+ "/web/project": ["__git_review__", "__file_tree__"],
+ },
+ },
projectToolsGitReview: {
openProjectPathKeys: ["/web/project"],
openVersion: 2,
},
+ projectToolsTunnel: {
+ openProjectPathKeys: ["/web/project"],
+ openVersion: 2,
+ },
},
});
const staleSynced = settingsSync.applyGatewaySettingsSyncPayload(current, {
customSettings: {
+ projectToolsPanel: {
+ width: 360,
+ activeTab: "terminal",
+ tabOrders: {
+ "/desktop/project": ["terminal-1", "__file_tree__"],
+ },
+ },
projectToolsGitReview: {
openProjectPathKeys: [],
openVersion: 1,
},
+ projectToolsTunnel: {
+ openProjectPathKeys: [],
+ openVersion: 1,
+ },
},
});
assert.deepEqual(staleSynced.customSettings.projectToolsGitReview.openProjectPathKeys, [
"/web/project",
]);
assert.equal(staleSynced.customSettings.projectToolsGitReview.openVersion, 2);
+ assert.deepEqual(staleSynced.customSettings.projectToolsTunnel.openProjectPathKeys, [
+ "/web/project",
+ ]);
+ assert.equal(staleSynced.customSettings.projectToolsTunnel.openVersion, 2);
+ assert.equal(staleSynced.customSettings.projectToolsPanel.width, 612);
+ assert.equal(staleSynced.customSettings.projectToolsPanel.activeTab, "terminal");
+ assert.deepEqual(staleSynced.customSettings.projectToolsPanel.tabOrders, {
+ "/web/project": ["__git_review__", "__file_tree__"],
+ });
const newerSynced = settingsSync.applyGatewaySettingsSyncPayload(staleSynced, {
customSettings: {
+ projectToolsPanel: {
+ width: 360,
+ activeTab: "tunnel",
+ tabOrders: {
+ "/desktop/project": ["terminal-1", "__tunnel__"],
+ },
+ },
projectToolsGitReview: {
openProjectPathKeys: ["/desktop/project"],
openVersion: 3,
},
+ projectToolsTunnel: {
+ openProjectPathKeys: ["/desktop/project"],
+ openVersion: 3,
+ },
},
});
assert.deepEqual(newerSynced.customSettings.projectToolsGitReview.openProjectPathKeys, [
"/desktop/project",
]);
assert.equal(newerSynced.customSettings.projectToolsGitReview.openVersion, 3);
+ assert.deepEqual(newerSynced.customSettings.projectToolsTunnel.openProjectPathKeys, [
+ "/desktop/project",
+ ]);
+ assert.equal(newerSynced.customSettings.projectToolsTunnel.openVersion, 3);
+ assert.equal(newerSynced.customSettings.projectToolsPanel.width, 612);
+ assert.equal(newerSynced.customSettings.projectToolsPanel.activeTab, "tunnel");
+ assert.deepEqual(newerSynced.customSettings.projectToolsPanel.tabOrders, {
+ "/web/project": ["__git_review__", "__file_tree__"],
+ });
const payload = settingsSync.buildGatewaySettingsSyncPayload(newerSynced);
+ assert.deepEqual(payload.customSettings.projectToolsPanel, {
+ activeTab: "tunnel",
+ });
+ assert.equal(Object.hasOwn(payload.customSettings.projectToolsPanel, "width"), false);
+ assert.equal(Object.hasOwn(payload.customSettings.projectToolsPanel, "tabOrders"), false);
assert.deepEqual(payload.customSettings.projectToolsGitReview, {
openProjectPathKeys: ["/desktop/project"],
openVersion: 3,
});
+ assert.deepEqual(payload.customSettings.projectToolsTunnel, {
+ openProjectPathKeys: ["/desktop/project"],
+ openVersion: 3,
+ });
});
test("gateway settings sync keeps newer project conversation activity", () => {
diff --git a/crates/agent-gateway/web/src/App.tsx b/crates/agent-gateway/web/src/App.tsx
index ba683e931..1c9e2ae2e 100644
--- a/crates/agent-gateway/web/src/App.tsx
+++ b/crates/agent-gateway/web/src/App.tsx
@@ -69,6 +69,7 @@ import {
isAgentDevMode,
isProjectToolsFileTreeOpen,
isProjectToolsGitReviewOpen,
+ isProjectToolsTunnelOpen,
normalizeChatRuntimeControlsForProvider,
normalizeSettings,
removeProjectToolsProjectState,
@@ -79,6 +80,7 @@ import {
updateProjectToolsFileTreeProjectState,
updateProjectToolsFileTreeOpen,
updateProjectToolsGitReviewOpen,
+ updateProjectToolsTunnelOpen,
updateProjectToolsPanelTabOrder,
type AppSettings,
type ChatRuntimeControls,
@@ -477,6 +479,38 @@ function isTerminalChatEvent(event: ChatEvent) {
return event.type === "done" || event.type === "error";
}
+type TunnelManagerToolChange = {
+ action: "create" | "close";
+ projectPathKey: string;
+};
+
+function asRecord(value: unknown): Record {
+ return value && typeof value === "object" && !Array.isArray(value)
+ ? (value as Record)
+ : {};
+}
+
+function readTunnelManagerToolChange(event: ChatEvent): TunnelManagerToolChange | null {
+ if (event.type !== "tool_result" || event.isError === true) {
+ return null;
+ }
+ const details = asRecord(event.details);
+ if (details.kind !== "tunnel_manager") {
+ return null;
+ }
+ const action = typeof details.action === "string" ? details.action.trim() : "";
+ if (action !== "create" && action !== "close") {
+ return null;
+ }
+ const tunnel = asRecord(details.tunnel);
+ const projectPathKey =
+ (typeof tunnel.projectPathKey === "string" ? tunnel.projectPathKey.trim() : "") ||
+ (typeof tunnel.project_path_key === "string" ? tunnel.project_path_key.trim() : "") ||
+ event.workdir?.trim() ||
+ "";
+ return { action, projectPathKey };
+}
+
function buildGatewaySelectedModel(
selectedModel: SelectedModel | undefined,
providers: ModelProviderSource[],
@@ -829,6 +863,7 @@ export default function App() {
const [isFileDropActive, setIsFileDropActive] = useState(false);
const [activeView, setActiveView] = useState<"chat" | "skills-hub" | "mcp-hub">("chat");
const [projectToolsPanelOpen, setProjectToolsPanelOpen] = useState(false);
+ const [tunnelRefreshToken, setTunnelRefreshToken] = useState(0);
const previousProjectToolsFileTreeOpenRef = useRef(false);
const [workspaceEditorMounted, setWorkspaceEditorMounted] = useState(false);
const [workspaceEditorOpen, setWorkspaceEditorOpen] = useState(false);
@@ -2035,6 +2070,42 @@ export default function App() {
[queueSettingsSave],
);
+ const openTunnelToolPanel = useCallback(
+ (projectPathKey?: string) => {
+ const targetProjectPathKey =
+ workspaceProjectPathKey(projectPathKey) ||
+ workspaceProjectPathKey(activeWorkspaceProjectPath);
+ if (!targetProjectPathKey) return;
+ setActiveView("chat");
+ setProjectToolsPanelOpen(true);
+ setSettings((prev) =>
+ updateProjectToolsTunnelOpen(
+ updateCustomSettings(prev, {
+ projectToolsPanel: {
+ ...prev.customSettings.projectToolsPanel,
+ activeTab: "tunnel",
+ },
+ }),
+ targetProjectPathKey,
+ true,
+ ),
+ );
+ },
+ [activeWorkspaceProjectPath, setSettings],
+ );
+
+ const handleTunnelManagerChatEvent = useCallback(
+ (event: ChatEvent) => {
+ const change = readTunnelManagerToolChange(event);
+ if (!change) return;
+ setTunnelRefreshToken((current) => current + 1);
+ if (change.action === "create") {
+ openTunnelToolPanel(change.projectPathKey);
+ }
+ },
+ [openTunnelToolPanel],
+ );
+
const persistProjectConversationActivity = useCallback(
(activity: ReadonlyMap) => {
if (activity.size === 0) {
@@ -2828,6 +2899,7 @@ export default function App() {
liveStore.appendEvent(event, {
flush: event.type === "done" || event.type === "error",
});
+ handleTunnelManagerChatEvent(event);
if (event.type === "done" || event.type === "error") {
terminalEventSeen = true;
@@ -2884,6 +2956,7 @@ export default function App() {
commitTerminalConversationLiveStream,
getConversationAbortController,
getConversationLiveStreamStore,
+ handleTunnelManagerChatEvent,
markCompletedLiveStream,
markLiveConversationStreamActive,
recoverUnavailableConversationStream,
@@ -3090,6 +3163,7 @@ export default function App() {
clearConversationLiveStream,
getConversationAbortController,
getHistoryPositionLockedConversationIds,
+ handleTunnelManagerChatEvent,
hasRecentlyCompletedLiveStream,
hasRetainedConversationLiveStream,
isAgentMode,
@@ -3122,14 +3196,17 @@ export default function App() {
return;
}
+ const visibleBroadcastConversationId = resolveVisibleConversationId(
+ selectedHistoryIdRef.current,
+ conversationIdRef.current,
+ );
const isTerminalEvent = isTerminalChatEvent(event);
if (!isTerminalEvent && !isChatStreamNotAvailableEvent(event)) {
setRemoteConversationRunningState(targetConversationId, true, {
workdir: event.workdir,
});
if (
- resolveVisibleConversationId(selectedHistoryIdRef.current, conversationIdRef.current) ===
- targetConversationId &&
+ visibleBroadcastConversationId === targetConversationId &&
!isConversationLiveStreamAttached(targetConversationId)
) {
attachVisibleConversationLiveStream(targetConversationId, api);
@@ -3177,6 +3254,9 @@ export default function App() {
liveStore.appendEvent(event, {
flush: isTerminalEvent,
});
+ if (visibleBroadcastConversationId === targetConversationId) {
+ handleTunnelManagerChatEvent(event);
+ }
if (isTerminalEvent) {
markCompletedLiveStream(targetConversationId);
commitTerminalConversationLiveStream(targetConversationId);
@@ -3197,6 +3277,7 @@ export default function App() {
commitTerminalConversationLiveStream,
getConversationAbortController,
getConversationLiveStreamStore,
+ handleTunnelManagerChatEvent,
isConversationLiveStreamAttached,
markCompletedLiveStream,
markLiveConversationStreamActive,
@@ -4143,6 +4224,7 @@ export default function App() {
getConversationLiveStreamStore(activeConversationId)?.appendEvent(event, {
flush: event.type === "done" || event.type === "error",
});
+ handleTunnelManagerChatEvent(event);
if (event.type === "done" || event.type === "error") {
terminalEventSeen = true;
markCompletedLiveStream(activeConversationId);
@@ -5530,6 +5612,10 @@ export default function App() {
settings.customSettings,
terminalProjectPathKey,
);
+ const projectToolsTunnelOpen = isProjectToolsTunnelOpen(
+ settings.customSettings,
+ terminalProjectPathKey,
+ );
const projectToolsDisabledMessage = !settingsSyncReady
? "Syncing desktop settings..."
: !isAgentMode
@@ -5545,6 +5631,29 @@ export default function App() {
const gitDisabledMessage = !settings.remote.enableWebGit
? "WebUI Git is disabled in desktop Remote settings."
: undefined;
+ const tunnelEnabled =
+ settingsSyncReady && settings.remote.enableWebTunnels === true && status?.online === true;
+ const tunnelDisabledMessage = !settingsSyncReady
+ ? translate("chat.runtime.tunnelSettingsSyncing", settings.locale)
+ : !settings.remote.enableWebTunnels
+ ? translate("projectTools.tunnelWebDisabled", settings.locale)
+ : status?.online !== true
+ ? translate("projectTools.tunnelRemoteOffline", settings.locale)
+ : undefined;
+ const tunnelManagerToolAvailable =
+ settingsSyncReady &&
+ isAgentMode &&
+ settings.remote.enableWebTunnels === true &&
+ status?.online === true;
+ const tunnelManagerToolDisabledMessage = !settingsSyncReady
+ ? translate("chat.runtime.tunnelSettingsSyncing", settings.locale)
+ : !isAgentMode
+ ? translate("chat.runtime.tunnelAgentModeRequired", settings.locale)
+ : !settings.remote.enableWebTunnels
+ ? translate("chat.runtime.tunnelWebDisabled", settings.locale)
+ : status?.online !== true
+ ? translate("chat.runtime.tunnelRemoteOffline", settings.locale)
+ : undefined;
const handleOpenWorkspaceFile = useCallback(
(path: string) => {
if (!terminalProjectPath || !terminalProjectPathKey) return;
@@ -5574,6 +5683,11 @@ export default function App() {
},
[terminalProjectPath, terminalProjectPathKey],
);
+
+ const handleOpenTunnelToolPanel = useCallback(() => {
+ if (!tunnelManagerToolAvailable) return;
+ openTunnelToolPanel();
+ }, [openTunnelToolPanel, tunnelManagerToolAvailable]);
const requestWorkspaceEditorClose = useCallback(() => {
setWorkspaceEditorCloseRequestId((current) => current + 1);
}, []);
@@ -6433,6 +6547,8 @@ export default function App() {
gitClient={gitClient}
gitWriteEnabled={settings.remote.enableWebGit}
gitDisabledMessage={gitDisabledMessage}
+ tunnelToolAvailable={tunnelManagerToolAvailable}
+ tunnelToolDisabledMessage={tunnelManagerToolDisabledMessage}
onGitChanged={(gitWorkdir) =>
window.dispatchEvent(
new CustomEvent("liveagent:git-changed", {
@@ -6440,6 +6556,7 @@ export default function App() {
}),
)
}
+ onOpenTunnelToolPanel={handleOpenTunnelToolPanel}
onSend={() => {
if (
submitInFlightRef.current ||
@@ -6657,10 +6774,15 @@ export default function App() {
settings.customSettings,
terminalProjectPathKey,
)}
+ tunnelOpen={projectToolsTunnelOpen}
client={terminalClient}
gitClient={gitClient}
gitWriteEnabled={settings.remote.enableWebGit}
gitDisabledMessage={gitDisabledMessage}
+ tunnelClient={isAgentMode ? api : null}
+ tunnelEnabled={tunnelEnabled}
+ tunnelDisabledMessage={tunnelDisabledMessage}
+ tunnelRefreshToken={tunnelRefreshToken}
onWidthChange={(nextWidth) =>
setSettings((prev) =>
updateCustomSettings(prev, {
@@ -6701,6 +6823,9 @@ export default function App() {
updateProjectToolsGitReviewOpen(prev, terminalProjectPathKey, open),
)
}
+ onTunnelOpenChange={(open) =>
+ setSettings((prev) => updateProjectToolsTunnelOpen(prev, terminalProjectPathKey, open))
+ }
onSessionsChange={handleProjectTerminalSessionsChange}
onInsertFileMention={(path, kind) => {
composerRef.current?.insertFileMention(path, kind);
diff --git a/crates/agent-gateway/web/src/components/project-tools/LocalTunnelPanel.tsx b/crates/agent-gateway/web/src/components/project-tools/LocalTunnelPanel.tsx
new file mode 100644
index 000000000..7bb03695b
--- /dev/null
+++ b/crates/agent-gateway/web/src/components/project-tools/LocalTunnelPanel.tsx
@@ -0,0 +1,783 @@
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import { useLocale } from "@/i18n";
+import { cn } from "@/lib/shared/utils";
+import type {
+ TunnelCreateInput,
+ TunnelSummary as GatewayTunnelSummary,
+ TunnelUpdateInput,
+} from "@/lib/gatewaySocket";
+import {
+ Check,
+ Clock3,
+ Copy,
+ Edit3,
+ ExternalLink,
+ Folder,
+ Globe,
+ Link2,
+ Loader2,
+ Save,
+ Trash2,
+ X,
+} from "../icons";
+import { Button } from "../ui/button";
+import { Input } from "../ui/input";
+import { Label } from "../ui/label";
+
+export type { TunnelCreateInput };
+export type TunnelSummary = Omit;
+
+export type TunnelTtlSeconds = 0 | 900 | 3600 | 14400;
+
+export type LocalTunnelClient = {
+ listTunnels(): Promise;
+ createTunnel(input: TunnelCreateInput): Promise;
+ updateTunnel(input: TunnelUpdateInput): Promise;
+ closeTunnel(id: string): Promise;
+};
+
+type LocalTunnelPanelProps = {
+ client: LocalTunnelClient;
+ enabled?: boolean;
+ disabledMessage?: string;
+ projectPathKey?: string;
+ refreshToken?: number;
+};
+
+type TunnelScope = "project" | "global";
+
+const TUNNEL_MANAGER_CHANGED_EVENT = "liveagent:tunnel-manager-changed";
+
+const TUNNEL_SCOPE_OPTIONS: Array<{
+ scope: TunnelScope;
+ labelKey: string;
+ titleKey: string;
+}> = [
+ {
+ scope: "project",
+ labelKey: "projectTools.tunnelScopeProject",
+ titleKey: "projectTools.tunnelScopeProjectTitle",
+ },
+ {
+ scope: "global",
+ labelKey: "projectTools.tunnelScopeGlobal",
+ titleKey: "projectTools.tunnelScopeGlobalTitle",
+ },
+];
+
+const TTL_OPTIONS: Array<{ value: TunnelTtlSeconds; labelKey: string }> = [
+ { value: 900, labelKey: "projectTools.tunnelTtl15m" },
+ { value: 3600, labelKey: "projectTools.tunnelTtl1h" },
+ { value: 14400, labelKey: "projectTools.tunnelTtl4h" },
+ { value: 0, labelKey: "projectTools.tunnelTtlInfinite" },
+];
+
+function validateLocalHttpTarget(input: string) {
+ const value = input.trim();
+ if (!value) return "projectTools.tunnelTargetRequired";
+ try {
+ const url = new URL(value);
+ if (url.protocol !== "http:") {
+ return "projectTools.tunnelInvalidUrl";
+ }
+ const hostname = url.hostname.toLowerCase();
+ if (!["localhost", "127.0.0.1", "::1", "[::1]"].includes(hostname)) {
+ return "projectTools.tunnelLocalhostOnly";
+ }
+ if (url.username || url.password || url.hash) {
+ return "projectTools.tunnelInvalidUrl";
+ }
+ } catch {
+ return "projectTools.tunnelInvalidUrl";
+ }
+ return null;
+}
+
+function asErrorMessage(error: unknown) {
+ return error instanceof Error ? error.message : String(error);
+}
+
+function formatRemaining(seconds: number) {
+ if (seconds <= 0) return "0m";
+ const hours = Math.floor(seconds / 3600);
+ const minutes = Math.ceil((seconds % 3600) / 60);
+ if (hours <= 0) return `${minutes}m`;
+ if (minutes >= 60) return `${hours + 1}h`;
+ return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
+}
+
+function formatDateTime(seconds: number) {
+ if (!seconds) return "";
+ return new Date(seconds * 1000).toLocaleString();
+}
+
+function writeTextToClipboard(text: string) {
+ if (navigator.clipboard?.writeText) {
+ return navigator.clipboard.writeText(text).then(
+ () => true,
+ () => fallbackWriteTextToClipboard(text),
+ );
+ }
+ return Promise.resolve(fallbackWriteTextToClipboard(text));
+}
+
+function fallbackWriteTextToClipboard(text: string) {
+ const textarea = document.createElement("textarea");
+ textarea.value = text;
+ textarea.setAttribute("readonly", "");
+ textarea.style.position = "fixed";
+ textarea.style.top = "-9999px";
+ textarea.style.opacity = "0";
+ document.body.appendChild(textarea);
+ textarea.select();
+ try {
+ return document.execCommand("copy");
+ } finally {
+ document.body.removeChild(textarea);
+ }
+}
+
+function displayTunnelName(tunnel: TunnelSummary) {
+ return tunnel.name.trim() || tunnel.targetUrl;
+}
+
+function tunnelStatusKey(status: TunnelSummary["status"]) {
+ if (status === "expired") return "projectTools.tunnelStatusExpired";
+ if (status === "offline") return "projectTools.tunnelStatusOffline";
+ return "projectTools.tunnelStatusActive";
+}
+
+function normalizeProjectPathKey(value: string | undefined) {
+ return value?.trim() ?? "";
+}
+
+function ttlFromTunnel(tunnel: TunnelSummary, nowSeconds: number): TunnelTtlSeconds {
+ if (!tunnel.expiresAt) return 0;
+ const remaining = Math.max(0, tunnel.expiresAt - nowSeconds);
+ if (remaining <= 900) return 900;
+ if (remaining <= 3600) return 3600;
+ return 14400;
+}
+
+export function LocalTunnelPanel({
+ client,
+ enabled = true,
+ disabledMessage,
+ projectPathKey,
+ refreshToken,
+}: LocalTunnelPanelProps) {
+ const { t } = useLocale();
+ const normalizedProjectPathKey = useMemo(
+ () => normalizeProjectPathKey(projectPathKey),
+ [projectPathKey],
+ );
+ const [scope, setScope] = useState(() =>
+ normalizeProjectPathKey(projectPathKey) ? "project" : "global",
+ );
+ const [targetUrl, setTargetUrl] = useState("http://localhost:3000");
+ const [name, setName] = useState("");
+ const [ttlSeconds, setTtlSeconds] = useState(3600);
+ const [editingId, setEditingId] = useState("");
+ const [editTargetUrl, setEditTargetUrl] = useState("");
+ const [editName, setEditName] = useState("");
+ const [editTtlSeconds, setEditTtlSeconds] = useState(3600);
+ const [tunnels, setTunnels] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [creating, setCreating] = useState(false);
+ const [savingId, setSavingId] = useState("");
+ const [closingId, setClosingId] = useState("");
+ const [copiedId, setCopiedId] = useState("");
+ const [error, setError] = useState(null);
+ const [nowSeconds, setNowSeconds] = useState(() => Math.floor(Date.now() / 1000));
+ const refreshTokenRef = useRef(refreshToken);
+ const targetValidationKey = useMemo(() => validateLocalHttpTarget(targetUrl), [targetUrl]);
+ const editTargetValidationKey = useMemo(
+ () => (editingId ? validateLocalHttpTarget(editTargetUrl) : null),
+ [editTargetUrl, editingId],
+ );
+
+ const refresh = useCallback(
+ (options?: { showLoading?: boolean }) => {
+ const showLoading = options?.showLoading ?? true;
+ if (showLoading) {
+ setLoading(true);
+ }
+ setError(null);
+ return client
+ .listTunnels()
+ .then((items) => setTunnels(items))
+ .catch((err) => setError(asErrorMessage(err)))
+ .finally(() => {
+ if (showLoading) {
+ setLoading(false);
+ }
+ });
+ },
+ [client],
+ );
+
+ useEffect(() => {
+ void refresh();
+ }, [refresh]);
+
+ useEffect(() => {
+ if (refreshTokenRef.current === refreshToken) return;
+ refreshTokenRef.current = refreshToken;
+ void refresh({ showLoading: false });
+ }, [refresh, refreshToken]);
+
+ useEffect(() => {
+ const handleTunnelManagerChanged = () => {
+ void refresh({ showLoading: false });
+ };
+ window.addEventListener(TUNNEL_MANAGER_CHANGED_EVENT, handleTunnelManagerChanged);
+ return () =>
+ window.removeEventListener(TUNNEL_MANAGER_CHANGED_EVENT, handleTunnelManagerChanged);
+ }, [refresh]);
+
+ useEffect(() => {
+ if (!normalizedProjectPathKey && scope === "project") {
+ setScope("global");
+ setError(null);
+ }
+ }, [normalizedProjectPathKey, scope]);
+
+ useEffect(() => {
+ const timer = window.setInterval(() => {
+ setNowSeconds(Math.floor(Date.now() / 1000));
+ }, 1000);
+ return () => window.clearInterval(timer);
+ }, []);
+
+ useEffect(() => {
+ if (!copiedId) return;
+ const timer = window.setTimeout(() => setCopiedId(""), 1600);
+ return () => window.clearTimeout(timer);
+ }, [copiedId]);
+
+ const createTunnel = useCallback(() => {
+ const validationKey = validateLocalHttpTarget(targetUrl);
+ if (validationKey) {
+ setError(t(validationKey));
+ return;
+ }
+ if (!enabled || creating) return;
+ const input: TunnelCreateInput = {
+ targetUrl: targetUrl.trim(),
+ name: name.trim() || undefined,
+ ttlSeconds,
+ };
+ if (scope === "project" && normalizedProjectPathKey) {
+ input.projectPathKey = normalizedProjectPathKey;
+ }
+ setCreating(true);
+ setError(null);
+ void client
+ .createTunnel(input)
+ .then((created) => {
+ setTunnels((current) => [
+ created,
+ ...current.filter((item) => item.id !== created.id && item.slug !== created.slug),
+ ]);
+ setName("");
+ void refresh({ showLoading: false });
+ })
+ .catch((err) => setError(asErrorMessage(err)))
+ .finally(() => setCreating(false));
+ }, [
+ client,
+ creating,
+ enabled,
+ name,
+ normalizedProjectPathKey,
+ refresh,
+ scope,
+ t,
+ targetUrl,
+ ttlSeconds,
+ ]);
+
+ const beginEdit = useCallback(
+ (tunnel: TunnelSummary) => {
+ setEditingId(tunnel.id);
+ setEditTargetUrl(tunnel.targetUrl);
+ setEditName(tunnel.name);
+ setEditTtlSeconds(ttlFromTunnel(tunnel, nowSeconds));
+ setError(null);
+ },
+ [nowSeconds],
+ );
+
+ const cancelEdit = useCallback(() => {
+ setEditingId("");
+ setEditTargetUrl("");
+ setEditName("");
+ setEditTtlSeconds(3600);
+ setError(null);
+ }, []);
+
+ const updateTunnel = useCallback(
+ (tunnel: TunnelSummary) => {
+ const validationKey = validateLocalHttpTarget(editTargetUrl);
+ if (validationKey) {
+ setError(t(validationKey));
+ return;
+ }
+ if (!enabled || savingId) return;
+ const input: TunnelUpdateInput = {
+ id: tunnel.id,
+ targetUrl: editTargetUrl.trim(),
+ name: editName.trim() || undefined,
+ ttlSeconds: editTtlSeconds,
+ };
+ const tunnelProjectPathKey = normalizeProjectPathKey(tunnel.projectPathKey);
+ if (tunnelProjectPathKey) {
+ input.projectPathKey = tunnelProjectPathKey;
+ }
+ setSavingId(tunnel.id);
+ setError(null);
+ void client
+ .updateTunnel(input)
+ .then((updated) => {
+ setTunnels((current) => current.map((item) => (item.id === updated.id ? updated : item)));
+ cancelEdit();
+ })
+ .catch((err) => setError(asErrorMessage(err)))
+ .finally(() => setSavingId((current) => (current === tunnel.id ? "" : current)));
+ },
+ [cancelEdit, client, editName, editTargetUrl, editTtlSeconds, enabled, savingId, t],
+ );
+
+ const closeTunnel = useCallback(
+ (id: string) => {
+ if (!enabled || closingId) return;
+ setClosingId(id);
+ setError(null);
+ void client
+ .closeTunnel(id)
+ .then((closed) => {
+ setTunnels((current) =>
+ current
+ .filter((item) => item.id !== id)
+ .concat(closed.status === "active" ? [closed] : []),
+ );
+ })
+ .catch((err) => setError(asErrorMessage(err)))
+ .finally(() => setClosingId((current) => (current === id ? "" : current)));
+ },
+ [client, closingId, enabled],
+ );
+
+ const copyLink = useCallback((tunnel: TunnelSummary) => {
+ if (!tunnel.publicUrl) return;
+ void writeTextToClipboard(tunnel.publicUrl)
+ .then((copied) => {
+ if (copied) {
+ setCopiedId(tunnel.id);
+ }
+ })
+ .catch(() => {});
+ }, []);
+
+ const openLink = useCallback((tunnel: TunnelSummary) => {
+ if (!tunnel.publicUrl) return;
+ window.open(tunnel.publicUrl, "_blank", "noopener,noreferrer");
+ }, []);
+
+ const scopedTunnels = useMemo(
+ () =>
+ tunnels.filter((tunnel) => {
+ const tunnelProjectPathKey = normalizeProjectPathKey(tunnel.projectPathKey);
+ if (scope === "project") {
+ return (
+ Boolean(normalizedProjectPathKey) && tunnelProjectPathKey === normalizedProjectPathKey
+ );
+ }
+ return true;
+ }),
+ [normalizedProjectPathKey, scope, tunnels],
+ );
+ const sortedTunnels = useMemo(
+ () => [...scopedTunnels].sort((a, b) => b.createdAt - a.createdAt),
+ [scopedTunnels],
+ );
+ const canCreate =
+ enabled &&
+ !creating &&
+ !targetValidationKey &&
+ (scope !== "project" || Boolean(normalizedProjectPathKey));
+ const showCreateForm = scope === "project" && Boolean(normalizedProjectPathKey);
+
+ return (
+
+
+
+
+
+
+
+
+ {t("projectTools.tunnelTitle")}
+
+
+ {t("projectTools.tunnelDescription")}
+
+
+
+
+ {TUNNEL_SCOPE_OPTIONS.map((option) => {
+ const active = scope === option.scope;
+ const disabled = option.scope === "project" && !normalizedProjectPathKey;
+ const Icon = option.scope === "project" ? Folder : Globe;
+ return (
+
+ );
+ })}
+
+
+
+
+
+ {disabledMessage || showCreateForm ? (
+
+ {disabledMessage ? (
+
+ {disabledMessage}
+
+ ) : null}
+ {showCreateForm ? (
+
+
+
+
setTargetUrl(event.target.value)}
+ placeholder={t("projectTools.tunnelTargetPlaceholder")}
+ disabled={!enabled || creating}
+ className="h-8 min-w-0 text-xs"
+ />
+ {targetValidationKey ? (
+
{t(targetValidationKey)}
+ ) : null}
+
+
+
+
+ setName(event.target.value)}
+ placeholder={t("projectTools.tunnelNamePlaceholder")}
+ disabled={!enabled || creating}
+ className="h-8 min-w-0 text-xs"
+ />
+
+
+
+
+ {TTL_OPTIONS.map((option) => (
+
+ ))}
+
+
+
+
+
+ ) : null}
+
+ ) : null}
+
+
+ {error ? (
+
+ {error}
+
+ ) : null}
+ {loading && sortedTunnels.length === 0 ? (
+
+
+ {t("projectTools.tunnelLoading")}
+
+ ) : sortedTunnels.length === 0 ? (
+
+
+
+
+
{t("projectTools.tunnelEmpty")}
+
+ ) : (
+
+ {sortedTunnels.map((tunnel) => {
+ const hasExpiry = tunnel.expiresAt > 0;
+ const remaining = hasExpiry ? tunnel.expiresAt - nowSeconds : 0;
+ const expired = tunnel.status === "expired" || (hasExpiry && remaining <= 0);
+ const isEditing = editingId === tunnel.id;
+ const updating = savingId === tunnel.id;
+ const tunnelProjectPathKey = normalizeProjectPathKey(tunnel.projectPathKey);
+ return (
+
+
+
+
+
+
+
+
+ {displayTunnelName(tunnel)}
+
+
+ {t(tunnelStatusKey(expired ? "expired" : tunnel.status))}
+
+
+ {isEditing ? (
+
+
setEditTargetUrl(event.target.value)}
+ disabled={!enabled || updating}
+ className="h-8 min-w-0 text-xs"
+ aria-label={t("projectTools.tunnelTargetUrl")}
+ />
+ {editTargetValidationKey ? (
+
+ {t(editTargetValidationKey)}
+
+ ) : null}
+
+
setEditName(event.target.value)}
+ placeholder={t("projectTools.tunnelNamePlaceholder")}
+ disabled={!enabled || updating}
+ className="h-8 min-w-0 text-xs"
+ aria-label={t("projectTools.tunnelName")}
+ />
+
+
+
+ {TTL_OPTIONS.map((option) => (
+
+ ))}
+
+
+
+
+ ) : (
+ <>
+
+ {t("projectTools.tunnelTarget")}: {tunnel.targetUrl}
+
+
+
+
+ {tunnel.publicUrl}
+
+
+
+
+
+
+ {!hasExpiry
+ ? t("projectTools.tunnelTtlInfinite")
+ : expired
+ ? t("projectTools.tunnelExpired")
+ : t("projectTools.tunnelExpiresIn").replace(
+ "{time}",
+ formatRemaining(remaining),
+ )}
+
+
+ {scope === "global" ? (
+
+ {t(
+ tunnelProjectPathKey
+ ? "projectTools.tunnelScopeProjectBadge"
+ : "projectTools.tunnelScopeGlobalBadge",
+ )}
+
+ ) : null}
+ {hasExpiry ? {formatDateTime(tunnel.expiresAt)} : null}
+
+ >
+ )}
+
+
+
+ {isEditing ? (
+ <>
+
+
+ >
+ ) : (
+ <>
+
+
+
+
+ >
+ )}
+
+
+ );
+ })}
+
+ )}
+
+
+ );
+}
diff --git a/crates/agent-gateway/web/src/components/project-tools/ProjectToolsPanel.tsx b/crates/agent-gateway/web/src/components/project-tools/ProjectToolsPanel.tsx
index c01212a2b..baf973971 100644
--- a/crates/agent-gateway/web/src/components/project-tools/ProjectToolsPanel.tsx
+++ b/crates/agent-gateway/web/src/components/project-tools/ProjectToolsPanel.tsx
@@ -34,6 +34,7 @@ import {
ChevronRight,
FolderTree,
GitBranch,
+ Globe,
GripVertical,
Plus,
Terminal,
@@ -54,6 +55,7 @@ import {
type GitCommitContextPayload,
type GitFileContextPayload,
} from "./GitReviewPanel";
+import { LocalTunnelPanel, type LocalTunnelClient } from "./LocalTunnelPanel";
import { ProjectFileTreePanel } from "./ProjectFileTreePanel";
const MIN_PANEL_WIDTH = 320;
@@ -64,6 +66,7 @@ const DEFAULT_TERMINAL_COLS = 80;
const DEFAULT_TERMINAL_ROWS = 24;
const FILE_TREE_TAB_ID = "__file_tree__";
const GIT_REVIEW_TAB_ID = "__git_review__";
+const TUNNEL_TAB_ID = "__tunnel__";
const PROJECT_TOOLS_RESIZE_END_EVENT = "liveagent:project-tools-resize-end";
type ProjectToolsPanelProps = {
@@ -81,16 +84,22 @@ type ProjectToolsPanelProps = {
fileTreeOpen: boolean;
fileTreeState: ProjectToolsFileTreeProjectState;
gitReviewOpen: boolean;
+ tunnelOpen?: boolean;
client: TerminalClient;
gitClient?: GitClient | null;
gitWriteEnabled?: boolean;
gitDisabledMessage?: string;
+ tunnelClient?: LocalTunnelClient | null;
+ tunnelEnabled?: boolean;
+ tunnelDisabledMessage?: string;
+ tunnelRefreshToken?: number;
onWidthChange: (width: number) => void;
onActiveTabChange: (tab: ProjectToolsPanelTab) => void;
onTabOrderChange?: (tabOrder: string[]) => void;
onFileTreeOpenChange: (open: boolean) => void;
onFileTreeStateChange: (patch: ProjectToolsFileTreeStatePatch) => void;
onGitReviewOpenChange: (open: boolean) => void;
+ onTunnelOpenChange?: (open: boolean) => void;
onSessionsChange?: (sessions: TerminalSession[]) => void;
onInsertFileMention?: (path: string, kind: "file" | "dir") => void;
onOpenFile?: (path: string) => void;
@@ -200,6 +209,10 @@ type ProjectToolsTab =
| {
id: typeof GIT_REVIEW_TAB_ID;
kind: "gitReview";
+ }
+ | {
+ id: typeof TUNNEL_TAB_ID;
+ kind: "tunnel";
};
type TabDragState = {
@@ -727,16 +740,22 @@ export function ProjectToolsPanel(props: ProjectToolsPanelProps) {
fileTreeOpen,
fileTreeState,
gitReviewOpen,
+ tunnelOpen = false,
client,
gitClient,
gitWriteEnabled = false,
gitDisabledMessage,
+ tunnelClient,
+ tunnelEnabled = false,
+ tunnelDisabledMessage,
+ tunnelRefreshToken,
onWidthChange,
onActiveTabChange,
onTabOrderChange,
onFileTreeOpenChange,
onFileTreeStateChange,
onGitReviewOpenChange,
+ onTunnelOpenChange,
onSessionsChange,
onInsertFileMention,
onOpenFile,
@@ -790,10 +809,15 @@ export function ProjectToolsPanel(props: ProjectToolsPanelProps) {
const isControlled = externalSessions !== undefined;
const fileTreeInitialized = Boolean(projectPathKey && fileTreeOpen);
const gitReviewInitialized = Boolean(projectPathKey && gitReviewOpen);
+ const tunnelInitialized = Boolean(tunnelOpen && tunnelClient);
+ const tunnelAvailable = Boolean(tunnelClient);
const previousFileTreeInitializedRef = useRef(fileTreeInitialized);
const previousGitReviewInitializedRef = useRef(gitReviewInitialized);
+ const previousTunnelInitializedRef = useRef(tunnelInitialized);
const currentActiveTab: ProjectToolsPanelTab =
- activeTab === "gitReview" && gitReviewInitialized
+ activeTab === "tunnel" && tunnelInitialized
+ ? "tunnel"
+ : activeTab === "gitReview" && gitReviewInitialized
? "gitReview"
: activeTab === "fileTree" && fileTreeInitialized
? "fileTree"
@@ -822,8 +846,11 @@ export function ProjectToolsPanel(props: ProjectToolsPanelProps) {
if (gitReviewInitialized) {
nextTabs.push({ id: GIT_REVIEW_TAB_ID, kind: "gitReview" });
}
+ if (tunnelInitialized) {
+ nextTabs.push({ id: TUNNEL_TAB_ID, kind: "tunnel" });
+ }
return nextTabs;
- }, [fileTreeInitialized, gitReviewInitialized, sessions]);
+ }, [fileTreeInitialized, gitReviewInitialized, sessions, tunnelInitialized]);
const effectiveTabOrder = draftTabOrder ?? tabOrder;
const orderedProjectTabs = useMemo(
() => orderProjectToolsTabs(visibleTabs, effectiveTabOrder),
@@ -914,6 +941,18 @@ export function ProjectToolsPanel(props: ProjectToolsPanelProps) {
}
}, [activeTab, gitReviewInitialized, onActiveTabChange]);
+ useEffect(() => {
+ const previousTunnelInitialized = previousTunnelInitializedRef.current;
+ previousTunnelInitializedRef.current = tunnelInitialized;
+ if (tunnelInitialized && !previousTunnelInitialized) {
+ onActiveTabChange("tunnel");
+ return;
+ }
+ if (!tunnelInitialized && previousTunnelInitialized && activeTab === "tunnel") {
+ onActiveTabChange("terminal");
+ }
+ }, [activeTab, onActiveTabChange, tunnelInitialized]);
+
const publishSessions = useCallback(
(nextSessions: TerminalSession[], options?: { notifyParent?: boolean }) => {
const sorted = sortSessions(nextSessions);
@@ -1047,7 +1086,14 @@ export function ProjectToolsPanel(props: ProjectToolsPanelProps) {
if (!isOpen) return;
const element = tabsScrollRef.current;
if (!element) return;
- const targetTabId = currentActiveTab === "fileTree" ? FILE_TREE_TAB_ID : activeSession?.id;
+ const targetTabId =
+ currentActiveTab === "fileTree"
+ ? FILE_TREE_TAB_ID
+ : currentActiveTab === "gitReview"
+ ? GIT_REVIEW_TAB_ID
+ : currentActiveTab === "tunnel"
+ ? TUNNEL_TAB_ID
+ : activeSession?.id;
if (!targetTabId) return;
const target = Array.from(
element.querySelectorAll("[data-project-tools-tab-id]"),
@@ -1540,8 +1586,12 @@ export function ProjectToolsPanel(props: ProjectToolsPanelProps) {
}
}, []);
+ const showDisabledMessage = Boolean(disabledMessage && !tunnelAvailable && !tunnelInitialized);
const showProjectToolsChooser =
- projectReady && currentActiveTab === "terminal" && !activeSession;
+ !showDisabledMessage &&
+ (projectReady || tunnelAvailable) &&
+ currentActiveTab === "terminal" &&
+ !activeSession;
const startFileTree = useCallback(() => {
setFileTreeInitialized(true);
@@ -1598,6 +1648,19 @@ export function ProjectToolsPanel(props: ProjectToolsPanelProps) {
}
}, [activeTab, onActiveTabChange, onGitReviewOpenChange]);
+ const startTunnel = useCallback(() => {
+ if (!tunnelClient) return;
+ onTunnelOpenChange?.(true);
+ onActiveTabChange("tunnel");
+ }, [onActiveTabChange, onTunnelOpenChange, tunnelClient]);
+
+ const closeTunnelTab = useCallback(() => {
+ onTunnelOpenChange?.(false);
+ if (activeTab === "tunnel") {
+ onActiveTabChange("terminal");
+ }
+ }, [activeTab, onActiveTabChange, onTunnelOpenChange]);
+
const renderCreateTerminalMenuItem = () => {
if (shellOptions.length > 1) {
return (
@@ -1807,6 +1870,63 @@ export function ProjectToolsPanel(props: ProjectToolsPanelProps) {
);
}
+ if (tab.kind === "tunnel") {
+ return (
+
+
+ );
+ }
+
const session = tab.session;
const isPendingClose = pendingCloseSessionId === session.id;
const isClosing = closingSessionId === session.id;
@@ -1914,7 +2034,7 @@ export function ProjectToolsPanel(props: ProjectToolsPanelProps) {
@@ -1939,6 +2059,14 @@ export function ProjectToolsPanel(props: ProjectToolsPanelProps) {
{t("projectTools.newGitReview")}
+
+
+ {t("projectTools.newTunnel")}
+
{onClose ? (
@@ -1987,7 +2115,7 @@ export function ProjectToolsPanel(props: ProjectToolsPanelProps) {
) : null}
- {disabledMessage ? (
+ {showDisabledMessage ? (
{disabledMessage}
@@ -2024,7 +2152,9 @@ export function ProjectToolsPanel(props: ProjectToolsPanelProps) {
@@ -2041,7 +2171,9 @@ export function ProjectToolsPanel(props: ProjectToolsPanelProps) {
@@ -2055,6 +2187,24 @@ export function ProjectToolsPanel(props: ProjectToolsPanelProps) {
+
+
+
+
+
+
+ {t("projectTools.newTunnel")}
+
+
+ {t("projectTools.tunnelDescription")}
+
+
+
{loading ? (
@@ -2104,6 +2254,22 @@ export function ProjectToolsPanel(props: ProjectToolsPanelProps) {
/>
) : null}
+ {tunnelInitialized && tunnelClient ? (
+
+
+
+ ) : null}
{sessions.length > 0 ? (
> = {
"chat.runtime.webSearchOn": "联网搜索已开启",
"chat.runtime.webSearchOff": "联网搜索已关闭",
"chat.runtime.webSearchTooltip": "联网搜索",
+ "chat.runtime.tunnelToolAvailable": "内网穿透工具已可用",
+ "chat.runtime.tunnelToolUnavailable": "内网穿透工具不可用",
+ "chat.runtime.tunnelAgentModeRequired": "Agent 模式下可使用内网穿透工具",
+ "chat.runtime.tunnelWebDisabled": "Remote 设置未允许 WebUI 内网穿透",
+ "chat.runtime.tunnelRemoteOffline": "Remote Gateway 未连接",
+ "chat.runtime.tunnelSettingsSyncing": "正在同步桌面端设置",
"chat.runtime.reasoning": "思考程度",
"chat.emptyRound": "(无回复)",
"chat.inputHint": "输入消息,@ 引用文件,Enter 发送,Shift+Enter 换行",
@@ -220,6 +226,7 @@ export const translations: Record
> = {
"projectTools.terminalTitle": "终端",
"projectTools.fileTreeTitle": "文件树",
"projectTools.gitReviewTitle": "Git 审查",
+ "projectTools.tunnelTitle": "内网穿透",
"projectTools.resizePanel": "调整项目工具栏宽度",
"projectTools.getStarted": "开始使用",
"projectTools.getStartedHint": "选择一个工具开始",
@@ -229,6 +236,13 @@ export const translations: Record> = {
"projectTools.fileTreeDescription": "浏览和管理项目文件",
"projectTools.newGitReview": "新建审查",
"projectTools.gitReviewDescription": "查看代码变更和提交历史",
+ "projectTools.newTunnel": "新建内网穿透",
+ "projectTools.tunnelDescription": "通过 Gateway 暴露本机 HTTP 服务",
+ "projectTools.tunnelScopeGroup": "切换内网穿透视角",
+ "projectTools.tunnelScopeProject": "当前项目",
+ "projectTools.tunnelScopeGlobal": "全局",
+ "projectTools.tunnelScopeProjectTitle": "管理当前项目内网穿透",
+ "projectTools.tunnelScopeGlobalTitle": "管理全局内网穿透",
"projectTools.newProjectTool": "新建项目工具",
"projectTools.closePanel": "关闭项目工具栏",
"projectTools.close": "关闭",
@@ -238,6 +252,41 @@ export const translations: Record> = {
"projectTools.closeRunningTerminal": "关闭正在运行的终端「{title}」?",
"projectTools.closeFileTree": "关闭文件树",
"projectTools.closeGitReview": "关闭 Git 审查",
+ "projectTools.closeTunnelTab": "关闭内网穿透",
+ "projectTools.tunnelTargetUrl": "本地服务地址",
+ "projectTools.tunnelTargetPlaceholder": "http://localhost:3000",
+ "projectTools.tunnelName": "名称",
+ "projectTools.tunnelNamePlaceholder": "可选",
+ "projectTools.tunnelTtl": "有效期",
+ "projectTools.tunnelTtl15m": "15m",
+ "projectTools.tunnelTtl1h": "1h",
+ "projectTools.tunnelTtl4h": "4h",
+ "projectTools.tunnelTtlInfinite": "无限",
+ "projectTools.tunnelCreate": "创建临时链接",
+ "projectTools.tunnelCreating": "创建中...",
+ "projectTools.tunnelEdit": "编辑链接",
+ "projectTools.tunnelSave": "保存修改",
+ "projectTools.tunnelUpdating": "保存中...",
+ "projectTools.tunnelCancelEdit": "取消编辑",
+ "projectTools.tunnelLoading": "正在加载内网穿透...",
+ "projectTools.tunnelEmpty": "还没有内网穿透链接",
+ "projectTools.tunnelTargetRequired": "请输入本地 HTTP 服务地址。",
+ "projectTools.tunnelInvalidUrl": "请输入有效的 http://localhost 地址,不能包含账号、密码或片段。",
+ "projectTools.tunnelLocalhostOnly": "仅支持 localhost、127.0.0.1 或 [::1]。",
+ "projectTools.tunnelRemoteOffline": "Remote Gateway 未连接,连接后才能创建或关闭 tunnel。",
+ "projectTools.tunnelWebDisabled": "桌面端 Remote 设置未允许 WebUI 创建或关闭内网穿透。",
+ "projectTools.tunnelStatusActive": "运行中",
+ "projectTools.tunnelStatusExpired": "已过期",
+ "projectTools.tunnelStatusOffline": "离线",
+ "projectTools.tunnelTarget": "目标",
+ "projectTools.tunnelExpiresIn": "剩余 {time}",
+ "projectTools.tunnelExpired": "已过期",
+ "projectTools.tunnelScopeProjectBadge": "项目",
+ "projectTools.tunnelScopeGlobalBadge": "全局",
+ "projectTools.tunnelCopyLink": "复制链接",
+ "projectTools.tunnelCopied": "已复制",
+ "projectTools.tunnelOpenLink": "打开链接",
+ "projectTools.tunnelClose": "删除链接",
"projectTools.gitReview.viewChanges": "查看更改",
"projectTools.gitReview.discardChanges": "放弃更改",
"projectTools.gitReview.stageChanges": "暂存更改",
@@ -909,6 +958,9 @@ export const translations: Record> = {
"settings.remoteWebGit": "允许 WebUI Git",
"settings.remoteWebGitHint":
"由桌面端 Remote 设置控制。开启后,已登录 WebUI 可对本机项目执行分支、暂存、提交和同步操作。",
+ "settings.remoteWebTunnels": "允许 WebUI 内网穿透",
+ "settings.remoteWebTunnelsHint":
+ "由桌面端 Remote 设置控制。开启后,已登录 WebUI 可为本机 localhost HTTP 服务创建和关闭临时访问链接。",
"settings.remoteHeartbeat": "心跳间隔",
"settings.remoteHeartbeatUnit": "秒",
"settings.remoteHeartbeatHint": "与 Gateway 之间的心跳检测间隔,用于维持连接和检测在线状态",
@@ -1243,6 +1295,12 @@ export const translations: Record> = {
"chat.runtime.webSearchOn": "Web search enabled",
"chat.runtime.webSearchOff": "Web search disabled",
"chat.runtime.webSearchTooltip": "Toggle web search",
+ "chat.runtime.tunnelToolAvailable": "Tunnel tool available",
+ "chat.runtime.tunnelToolUnavailable": "Tunnel tool unavailable",
+ "chat.runtime.tunnelAgentModeRequired": "Tunnel tool requires Agent mode",
+ "chat.runtime.tunnelWebDisabled": "Remote WebUI tunnels are not allowed",
+ "chat.runtime.tunnelRemoteOffline": "Remote Gateway is offline",
+ "chat.runtime.tunnelSettingsSyncing": "Syncing desktop settings",
"chat.runtime.reasoning": "Thinking effort",
"chat.emptyRound": "(No reply)",
"chat.inputHint": "Type a message, @ to mention files, Enter to send, Shift+Enter for newline",
@@ -1389,6 +1447,7 @@ export const translations: Record> = {
"projectTools.terminalTitle": "Terminal",
"projectTools.fileTreeTitle": "File Tree",
"projectTools.gitReviewTitle": "Git Review",
+ "projectTools.tunnelTitle": "Tunnel",
"projectTools.resizePanel": "Resize project tools panel",
"projectTools.getStarted": "Get Started",
"projectTools.getStartedHint": "Choose a tool to begin",
@@ -1398,6 +1457,13 @@ export const translations: Record> = {
"projectTools.fileTreeDescription": "Browse and manage project files",
"projectTools.newGitReview": "New Review",
"projectTools.gitReviewDescription": "Review code changes and commit history",
+ "projectTools.newTunnel": "New Tunnel",
+ "projectTools.tunnelDescription": "Expose local HTTP services through Gateway",
+ "projectTools.tunnelScopeGroup": "Switch tunnel scope",
+ "projectTools.tunnelScopeProject": "Current Project",
+ "projectTools.tunnelScopeGlobal": "Global",
+ "projectTools.tunnelScopeProjectTitle": "Manage current project tunnels",
+ "projectTools.tunnelScopeGlobalTitle": "Manage global tunnels",
"projectTools.newProjectTool": "New project tool",
"projectTools.closePanel": "Close project tools panel",
"projectTools.close": "Close",
@@ -1407,6 +1473,44 @@ export const translations: Record> = {
"projectTools.closeRunningTerminal": 'Close running terminal "{title}"?',
"projectTools.closeFileTree": "Close File Tree",
"projectTools.closeGitReview": "Close Git Review",
+ "projectTools.closeTunnelTab": "Close Tunnel",
+ "projectTools.tunnelTargetUrl": "Local service URL",
+ "projectTools.tunnelTargetPlaceholder": "http://localhost:3000",
+ "projectTools.tunnelName": "Name",
+ "projectTools.tunnelNamePlaceholder": "Optional",
+ "projectTools.tunnelTtl": "TTL",
+ "projectTools.tunnelTtl15m": "15m",
+ "projectTools.tunnelTtl1h": "1h",
+ "projectTools.tunnelTtl4h": "4h",
+ "projectTools.tunnelTtlInfinite": "Unlimited",
+ "projectTools.tunnelCreate": "Create temporary link",
+ "projectTools.tunnelCreating": "Creating...",
+ "projectTools.tunnelEdit": "Edit link",
+ "projectTools.tunnelSave": "Save changes",
+ "projectTools.tunnelUpdating": "Saving...",
+ "projectTools.tunnelCancelEdit": "Cancel edit",
+ "projectTools.tunnelLoading": "Loading tunnels...",
+ "projectTools.tunnelEmpty": "No tunnel links yet",
+ "projectTools.tunnelTargetRequired": "Enter a local HTTP service URL.",
+ "projectTools.tunnelInvalidUrl":
+ "Enter a valid http://localhost URL without credentials or fragments.",
+ "projectTools.tunnelLocalhostOnly": "Only localhost, 127.0.0.1, or [::1] are supported.",
+ "projectTools.tunnelRemoteOffline":
+ "Remote Gateway is not connected. Connect it before creating or closing tunnels.",
+ "projectTools.tunnelWebDisabled":
+ "Desktop Remote settings do not allow WebUI tunnel create or close.",
+ "projectTools.tunnelStatusActive": "Active",
+ "projectTools.tunnelStatusExpired": "Expired",
+ "projectTools.tunnelStatusOffline": "Offline",
+ "projectTools.tunnelTarget": "Target",
+ "projectTools.tunnelExpiresIn": "{time} left",
+ "projectTools.tunnelExpired": "Expired",
+ "projectTools.tunnelScopeProjectBadge": "Project",
+ "projectTools.tunnelScopeGlobalBadge": "Global",
+ "projectTools.tunnelCopyLink": "Copy link",
+ "projectTools.tunnelCopied": "Copied",
+ "projectTools.tunnelOpenLink": "Open link",
+ "projectTools.tunnelClose": "Delete link",
"projectTools.gitReview.viewChanges": "View Changes",
"projectTools.gitReview.discardChanges": "Discard Changes",
"projectTools.gitReview.stageChanges": "Stage Changes",
@@ -2107,6 +2211,9 @@ export const translations: Record> = {
"settings.remoteWebGit": "Allow WebUI Git",
"settings.remoteWebGitHint":
"Controlled by the desktop Remote settings. When enabled, authenticated WebUI clients can run branch, stage, commit, and sync operations on local projects.",
+ "settings.remoteWebTunnels": "Allow WebUI Tunnels",
+ "settings.remoteWebTunnelsHint":
+ "Controlled by the desktop Remote settings. When enabled, authenticated WebUI clients can create and close temporary links for local localhost HTTP services.",
"settings.remoteHeartbeat": "Heartbeat Interval",
"settings.remoteHeartbeatUnit": "seconds",
"settings.remoteHeartbeatHint":
diff --git a/crates/agent-gateway/web/src/lib/gatewaySocket.ts b/crates/agent-gateway/web/src/lib/gatewaySocket.ts
index d81da0402..304cfac4b 100644
--- a/crates/agent-gateway/web/src/lib/gatewaySocket.ts
+++ b/crates/agent-gateway/web/src/lib/gatewaySocket.ts
@@ -154,6 +154,58 @@ export type UploadedImagePreviewResponse = {
data: string;
};
+export type TunnelCreateInput = {
+ targetUrl: string;
+ name?: string;
+ ttlSeconds: 0 | 900 | 3600 | 14400;
+ projectPathKey?: string;
+};
+
+export type TunnelUpdateInput = {
+ id: string;
+ targetUrl: string;
+ name?: string;
+ ttlSeconds: 0 | 900 | 3600 | 14400;
+ projectPathKey?: string;
+};
+
+export type TunnelSummary = {
+ id: string;
+ slug: string;
+ name: string;
+ targetUrl: string;
+ publicUrl: string;
+ createdAt: number;
+ expiresAt: number;
+ activeConnections: number;
+ status: "active" | "expired" | "offline";
+ projectPathKey: string;
+};
+
+type RawTunnelSummary = {
+ id?: string;
+ slug?: string;
+ name?: string;
+ targetUrl?: string;
+ target_url?: string;
+ publicUrl?: string;
+ public_url?: string;
+ createdAt?: number;
+ created_at?: number;
+ expiresAt?: number;
+ expires_at?: number;
+ activeConnections?: number;
+ active_connections?: number;
+ status?: string;
+ projectPathKey?: string;
+ project_path_key?: string;
+};
+
+type RawTunnelResponse = {
+ tunnel?: RawTunnelSummary;
+ tunnels?: RawTunnelSummary[];
+};
+
type HistoryGetOptions = {
maxMessages?: number;
};
@@ -443,6 +495,42 @@ function normalizeTerminalEvent(input: RawTerminalEvent): TerminalEvent | null {
};
}
+function normalizeTunnelStatus(input: unknown): TunnelSummary["status"] {
+ return input === "expired" || input === "offline" ? input : "active";
+}
+
+function fallbackTunnelPublicUrl(slug: string) {
+ const origin = getRuntimeOrigin().replace(/\/$/, "");
+ return origin && slug ? `${origin}/t/${slug}/` : "";
+}
+
+function normalizeTunnelSummary(input: RawTunnelSummary): TunnelSummary {
+ const slug = input.slug?.trim() ?? "";
+ return {
+ id: input.id?.trim() ?? "",
+ slug,
+ name: input.name?.trim() ?? "",
+ targetUrl: input.targetUrl ?? input.target_url ?? "",
+ publicUrl: input.publicUrl ?? input.public_url ?? fallbackTunnelPublicUrl(slug),
+ createdAt: Number(input.createdAt ?? input.created_at ?? 0),
+ expiresAt: Number(input.expiresAt ?? input.expires_at ?? 0),
+ activeConnections: Number(input.activeConnections ?? input.active_connections ?? 0),
+ status: normalizeTunnelStatus(input.status),
+ projectPathKey: (input.projectPathKey ?? input.project_path_key ?? "").trim(),
+ };
+}
+
+function normalizeTunnelListResponse(input: RawTunnelResponse): TunnelSummary[] {
+ return (input.tunnels ?? []).map(normalizeTunnelSummary);
+}
+
+function normalizeTunnelResponse(input: RawTunnelResponse): TunnelSummary {
+ if (!input.tunnel) {
+ throw new Error("Tunnel response did not include a tunnel");
+ }
+ return normalizeTunnelSummary(input.tunnel);
+}
+
function normalizeOptionalOffset(value: unknown) {
return typeof value === "number" && Number.isFinite(value) && value >= 0
? Math.floor(value)
@@ -883,6 +971,49 @@ export class GatewayWebSocketClient {
});
}
+ async listTunnels(): Promise {
+ return normalizeTunnelListResponse(
+ await this.requestWithRecovery("tunnel.list", {}),
+ );
+ }
+
+ async createTunnel(input: TunnelCreateInput): Promise {
+ const payload: Record = {
+ targetUrl: input.targetUrl,
+ ttlSeconds: input.ttlSeconds,
+ name: input.name,
+ };
+ if (input.projectPathKey?.trim()) {
+ payload.projectPathKey = input.projectPathKey.trim();
+ }
+ return normalizeTunnelResponse(
+ await this.request("tunnel.create", payload),
+ );
+ }
+
+ async updateTunnel(input: TunnelUpdateInput): Promise {
+ const payload: Record = {
+ id: input.id,
+ targetUrl: input.targetUrl,
+ ttlSeconds: input.ttlSeconds,
+ name: input.name,
+ };
+ if (input.projectPathKey?.trim()) {
+ payload.projectPathKey = input.projectPathKey.trim();
+ }
+ return normalizeTunnelResponse(
+ await this.request("tunnel.update", payload),
+ );
+ }
+
+ async closeTunnel(id: string): Promise {
+ return normalizeTunnelResponse(
+ await this.request("tunnel.close", {
+ id,
+ }),
+ );
+ }
+
async listHistory(
page: number,
pageSize: number,
@@ -1869,6 +2000,10 @@ export type GatewayWebSocketClientLike = {
closeTerminal(sessionId: string, projectPathKey?: string): Promise;
closeProjectTerminals(projectPathKey: string): Promise;
detachTerminal(sessionId: string, projectPathKey?: string): Promise;
+ listTunnels(): Promise;
+ createTunnel(input: TunnelCreateInput): Promise;
+ updateTunnel(input: TunnelUpdateInput): Promise;
+ closeTunnel(id: string): Promise;
listHistory(page: number, pageSize: number, filter?: HistoryListFilter): Promise;
listHistoryWorkdirs(): Promise;
listSharedHistory(page: number, pageSize: number): Promise;
@@ -2429,6 +2564,47 @@ class SharedWorkerGatewayWebSocketClient implements GatewayWebSocketClientLike {
});
}
+ async listTunnels(): Promise {
+ return normalizeTunnelListResponse(await this.request("tunnel.list", {}));
+ }
+
+ async createTunnel(input: TunnelCreateInput): Promise {
+ const payload: Record = {
+ targetUrl: input.targetUrl,
+ ttlSeconds: input.ttlSeconds,
+ name: input.name,
+ };
+ if (input.projectPathKey?.trim()) {
+ payload.projectPathKey = input.projectPathKey.trim();
+ }
+ return normalizeTunnelResponse(
+ await this.request("tunnel.create", payload),
+ );
+ }
+
+ async updateTunnel(input: TunnelUpdateInput): Promise {
+ const payload: Record = {
+ id: input.id,
+ targetUrl: input.targetUrl,
+ ttlSeconds: input.ttlSeconds,
+ name: input.name,
+ };
+ if (input.projectPathKey?.trim()) {
+ payload.projectPathKey = input.projectPathKey.trim();
+ }
+ return normalizeTunnelResponse(
+ await this.request("tunnel.update", payload),
+ );
+ }
+
+ async closeTunnel(id: string): Promise {
+ return normalizeTunnelResponse(
+ await this.request("tunnel.close", {
+ id,
+ }),
+ );
+ }
+
async listHistory(
page: number,
pageSize: number,
diff --git a/crates/agent-gateway/web/src/lib/gatewaySocket.worker.ts b/crates/agent-gateway/web/src/lib/gatewaySocket.worker.ts
index ad95bd461..6b64cfc83 100644
--- a/crates/agent-gateway/web/src/lib/gatewaySocket.worker.ts
+++ b/crates/agent-gateway/web/src/lib/gatewaySocket.worker.ts
@@ -574,6 +574,71 @@ async function resolveRequest(client: GatewayWebSocketClient, method: string, pa
String(body.project_path_key ?? ""),
);
return undefined;
+ case "tunnel.list":
+ return {
+ tunnels: await client.listTunnels(),
+ };
+ case "tunnel.create": {
+ const projectPathKey =
+ typeof body.projectPathKey === "string"
+ ? body.projectPathKey.trim()
+ : typeof body.project_path_key === "string"
+ ? body.project_path_key.trim()
+ : "";
+ return {
+ tunnel: await client.createTunnel({
+ targetUrl: String(body.targetUrl ?? body.target_url ?? ""),
+ ttlSeconds:
+ body.ttlSeconds === 0 ||
+ body.ttlSeconds === 900 ||
+ body.ttlSeconds === 3600 ||
+ body.ttlSeconds === 14400
+ ? body.ttlSeconds
+ : body.ttl_seconds === 0 ||
+ body.ttl_seconds === 900 ||
+ body.ttl_seconds === 3600 ||
+ body.ttl_seconds === 14400
+ ? body.ttl_seconds
+ : 3600,
+ name: typeof body.name === "string" ? body.name : undefined,
+ ...(projectPathKey ? { projectPathKey } : {}),
+ }),
+ };
+ }
+ case "tunnel.update": {
+ const projectPathKey =
+ typeof body.projectPathKey === "string"
+ ? body.projectPathKey.trim()
+ : typeof body.project_path_key === "string"
+ ? body.project_path_key.trim()
+ : "";
+ return {
+ tunnel: await client.updateTunnel({
+ id: String(body.id ?? body.tunnelId ?? body.tunnel_id ?? body.slug ?? ""),
+ targetUrl: String(body.targetUrl ?? body.target_url ?? ""),
+ ttlSeconds:
+ body.ttlSeconds === 0 ||
+ body.ttlSeconds === 900 ||
+ body.ttlSeconds === 3600 ||
+ body.ttlSeconds === 14400
+ ? body.ttlSeconds
+ : body.ttl_seconds === 0 ||
+ body.ttl_seconds === 900 ||
+ body.ttl_seconds === 3600 ||
+ body.ttl_seconds === 14400
+ ? body.ttl_seconds
+ : 3600,
+ name: typeof body.name === "string" ? body.name : undefined,
+ ...(projectPathKey ? { projectPathKey } : {}),
+ }),
+ };
+ }
+ case "tunnel.close":
+ return {
+ tunnel: await client.closeTunnel(
+ String(body.id ?? body.tunnelId ?? body.tunnel_id ?? body.slug ?? ""),
+ ),
+ };
case "provider.models":
return client.getProviderModels(
String(body.type ?? ""),
diff --git a/crates/agent-gateway/web/src/lib/settings/index.ts b/crates/agent-gateway/web/src/lib/settings/index.ts
index 41d85387e..1242a2b3e 100644
--- a/crates/agent-gateway/web/src/lib/settings/index.ts
+++ b/crates/agent-gateway/web/src/lib/settings/index.ts
@@ -142,7 +142,7 @@ export type ChatSidebarSettings = {
recentCollapsed: boolean;
};
-export type ProjectToolsPanelTab = "terminal" | "fileTree" | "gitReview";
+export type ProjectToolsPanelTab = "terminal" | "fileTree" | "gitReview" | "tunnel";
export type ProjectToolsPanelSettings = {
width: number;
@@ -169,6 +169,11 @@ export type ProjectToolsGitReviewSettings = {
openVersion: number;
};
+export type ProjectToolsTunnelSettings = {
+ openProjectPathKeys: string[];
+ openVersion: number;
+};
+
export type ProjectToolsFileTreeStatePatch = Partial & {
bumpRevision?: boolean;
bumpStateVersion?: boolean;
@@ -180,6 +185,7 @@ export type CustomSettings = {
projectToolsPanel: ProjectToolsPanelSettings;
projectToolsFileTree: ProjectToolsFileTreeSettings;
projectToolsGitReview: ProjectToolsGitReviewSettings;
+ projectToolsTunnel: ProjectToolsTunnelSettings;
};
export type SystemSettings = {
@@ -267,6 +273,7 @@ export type RemoteSettings = {
heartbeatInterval: number;
enableWebTerminal: boolean;
enableWebGit: boolean;
+ enableWebTunnels: boolean;
};
export type AppSettings = {
@@ -1055,6 +1062,7 @@ export function normalizeRemoteSettings(input: unknown): RemoteSettings {
heartbeatInterval: normalizePositiveInteger(obj.heartbeatInterval, 30),
enableWebTerminal: obj.enableWebTerminal === true,
enableWebGit: obj.enableWebGit === true,
+ enableWebTunnels: obj.enableWebTunnels === true,
};
}
@@ -1545,6 +1553,23 @@ export function normalizeProjectToolsGitReviewSettings(
};
}
+export function normalizeProjectToolsTunnelSettings(
+ input: unknown,
+): ProjectToolsTunnelSettings {
+ const obj = (input && typeof input === "object" ? input : {}) as Record;
+ const openProjectPathKeys = Array.from(
+ new Set(
+ (Array.isArray(obj.openProjectPathKeys) ? obj.openProjectPathKeys : [])
+ .map((pathKey) => workspaceProjectPathKey(pathKey))
+ .filter(Boolean),
+ ),
+ ).sort();
+ return {
+ openProjectPathKeys,
+ openVersion: normalizeIntegerInRange(obj.openVersion, 0, Number.MAX_SAFE_INTEGER, 0),
+ };
+}
+
export function normalizeProjectToolsPanelTabOrder(input: unknown): string[] {
if (!Array.isArray(input)) return [];
const order: string[] = [];
@@ -1593,7 +1618,8 @@ export function normalizeCustomSettings(
const projectToolsPanelActiveTab =
projectToolsPanel.activeTab === "terminal" ||
projectToolsPanel.activeTab === "fileTree" ||
- projectToolsPanel.activeTab === "gitReview"
+ projectToolsPanel.activeTab === "gitReview" ||
+ projectToolsPanel.activeTab === "tunnel"
? projectToolsPanel.activeTab
: "fileTree";
const projectToolsFileTree = (
@@ -1606,6 +1632,11 @@ export function normalizeCustomSettings(
? obj.projectToolsGitReview
: {}
) as unknown;
+ const projectToolsTunnel = (
+ obj.projectToolsTunnel && typeof obj.projectToolsTunnel === "object"
+ ? obj.projectToolsTunnel
+ : {}
+ ) as unknown;
return {
conversationTitleModel: normalizeSelectedModelForProviders(
normalizeSelectedModel(obj.conversationTitleModel),
@@ -1627,6 +1658,7 @@ export function normalizeCustomSettings(
},
projectToolsFileTree: normalizeProjectToolsFileTreeSettings(projectToolsFileTree),
projectToolsGitReview: normalizeProjectToolsGitReviewSettings(projectToolsGitReview),
+ projectToolsTunnel: normalizeProjectToolsTunnelSettings(projectToolsTunnel),
};
}
@@ -1661,6 +1693,7 @@ export function getDefaultSettings(): AppSettings {
heartbeatInterval: 30,
enableWebTerminal: false,
enableWebGit: false,
+ enableWebTunnels: false,
},
memory: normalizeMemorySettings({}, customProviders),
customSettings: normalizeCustomSettings({}, customProviders),
@@ -1807,6 +1840,10 @@ function hasProjectToolsGitReviewSessionState(state: ProjectToolsGitReviewSettin
return state.openVersion > 0 || state.openProjectPathKeys.length > 0;
}
+function hasProjectToolsTunnelSessionState(state: ProjectToolsTunnelSettings): boolean {
+ return state.openVersion > 0 || state.openProjectPathKeys.length > 0;
+}
+
export function preserveProjectToolsSessionState(
next: AppSettings,
current: AppSettings,
@@ -1817,6 +1854,9 @@ export function preserveProjectToolsSessionState(
const currentGitReview = normalizeProjectToolsGitReviewSettings(
current.customSettings.projectToolsGitReview,
);
+ const currentTunnel = normalizeProjectToolsTunnelSettings(
+ current.customSettings.projectToolsTunnel,
+ );
return normalizeSettings({
...next,
@@ -1828,6 +1868,9 @@ export function preserveProjectToolsSessionState(
projectToolsGitReview: hasProjectToolsGitReviewSessionState(currentGitReview)
? currentGitReview
: next.customSettings.projectToolsGitReview,
+ projectToolsTunnel: hasProjectToolsTunnelSessionState(currentTunnel)
+ ? currentTunnel
+ : next.customSettings.projectToolsTunnel,
},
});
}
@@ -1898,6 +1941,14 @@ export function removeProjectToolsProjectState(
);
const removedGitReviewOpenProjectPathKey =
nextGitReviewOpenProjectPathKeys.length !== gitReviewOpenProjectPathKeys.length;
+ const tunnelOpenProjectPathKeys = prev.customSettings.projectToolsTunnel.openProjectPathKeys
+ .map((pathKey) => workspaceProjectPathKey(pathKey))
+ .filter(Boolean);
+ const nextTunnelOpenProjectPathKeys = tunnelOpenProjectPathKeys.filter(
+ (pathKey) => pathKey !== normalizedPathKey,
+ );
+ const removedTunnelOpenProjectPathKey =
+ nextTunnelOpenProjectPathKeys.length !== tunnelOpenProjectPathKeys.length;
const hasFileTreeProjectState = Object.prototype.hasOwnProperty.call(
prev.customSettings.projectToolsFileTree.projects,
normalizedPathKey,
@@ -1908,6 +1959,7 @@ export function removeProjectToolsProjectState(
!hasTabOrder &&
!removedOpenProjectPathKey &&
!removedGitReviewOpenProjectPathKey &&
+ !removedTunnelOpenProjectPathKey &&
!hasFileTreeProjectState
) {
return prev;
@@ -1951,6 +2003,15 @@ export function removeProjectToolsProjectState(
? prev.customSettings.projectToolsGitReview.openVersion + 1
: prev.customSettings.projectToolsGitReview.openVersion,
},
+ projectToolsTunnel: {
+ ...prev.customSettings.projectToolsTunnel,
+ openProjectPathKeys: removedTunnelOpenProjectPathKey
+ ? nextTunnelOpenProjectPathKeys.sort()
+ : prev.customSettings.projectToolsTunnel.openProjectPathKeys,
+ openVersion: removedTunnelOpenProjectPathKey
+ ? prev.customSettings.projectToolsTunnel.openVersion + 1
+ : prev.customSettings.projectToolsTunnel.openVersion,
+ },
});
}
@@ -2042,6 +2103,44 @@ export function updateProjectToolsGitReviewOpen(
});
}
+export function isProjectToolsTunnelOpen(
+ customSettings: CustomSettings,
+ projectPathKey: string,
+): boolean {
+ const normalizedPathKey = workspaceProjectPathKey(projectPathKey);
+ return (
+ normalizedPathKey !== "" &&
+ customSettings.projectToolsTunnel.openProjectPathKeys.includes(normalizedPathKey)
+ );
+}
+
+export function updateProjectToolsTunnelOpen(
+ prev: AppSettings,
+ projectPathKey: string,
+ open: boolean,
+): AppSettings {
+ const normalizedPathKey = workspaceProjectPathKey(projectPathKey);
+ if (!normalizedPathKey) return prev;
+ const openProjectPathKeys = new Set(
+ prev.customSettings.projectToolsTunnel.openProjectPathKeys
+ .map((pathKey) => workspaceProjectPathKey(pathKey))
+ .filter(Boolean),
+ );
+ if (openProjectPathKeys.has(normalizedPathKey) === open) return prev;
+ if (open) {
+ openProjectPathKeys.add(normalizedPathKey);
+ } else {
+ openProjectPathKeys.delete(normalizedPathKey);
+ }
+ return updateCustomSettings(prev, {
+ projectToolsTunnel: {
+ ...prev.customSettings.projectToolsTunnel,
+ openProjectPathKeys: Array.from(openProjectPathKeys).sort(),
+ openVersion: prev.customSettings.projectToolsTunnel.openVersion + 1,
+ },
+ });
+}
+
function projectToolsFileTreeProjectStateEqual(
left: ProjectToolsFileTreeProjectState,
right: ProjectToolsFileTreeProjectState,
diff --git a/crates/agent-gateway/web/src/lib/settings/storage.ts b/crates/agent-gateway/web/src/lib/settings/storage.ts
index 118099bbf..3f1899501 100644
--- a/crates/agent-gateway/web/src/lib/settings/storage.ts
+++ b/crates/agent-gateway/web/src/lib/settings/storage.ts
@@ -8,6 +8,7 @@ import {
normalizeChatRuntimeControls,
normalizeProjectToolsFileTreeSettings,
normalizeProjectToolsGitReviewSettings,
+ normalizeProjectToolsTunnelSettings,
normalizeProjectToolsPanelTabOrders,
type ChatRuntimeControls,
normalizeSkillsSettings,
@@ -56,6 +57,7 @@ function toPersistedLocalCustomSettings(
...customSettings,
projectToolsFileTree: normalizeProjectToolsFileTreeSettings({}),
projectToolsGitReview: normalizeProjectToolsGitReviewSettings({}),
+ projectToolsTunnel: normalizeProjectToolsTunnelSettings({}),
};
}
@@ -92,7 +94,8 @@ function readLocalUiSettings(): {
const projectToolsPanelActiveTab =
projectToolsPanel.activeTab === "terminal" ||
projectToolsPanel.activeTab === "fileTree" ||
- projectToolsPanel.activeTab === "gitReview"
+ projectToolsPanel.activeTab === "gitReview" ||
+ projectToolsPanel.activeTab === "tunnel"
? projectToolsPanel.activeTab
: "fileTree";
return toPersistedLocalCustomSettings({
@@ -110,6 +113,7 @@ function readLocalUiSettings(): {
},
projectToolsFileTree: normalizeProjectToolsFileTreeSettings({}),
projectToolsGitReview: normalizeProjectToolsGitReviewSettings({}),
+ projectToolsTunnel: normalizeProjectToolsTunnelSettings({}),
});
}
diff --git a/crates/agent-gateway/web/src/lib/settings/sync.ts b/crates/agent-gateway/web/src/lib/settings/sync.ts
index 67d94940a..a7529a653 100644
--- a/crates/agent-gateway/web/src/lib/settings/sync.ts
+++ b/crates/agent-gateway/web/src/lib/settings/sync.ts
@@ -2,7 +2,9 @@ import {
normalizeChatRuntimeControls,
normalizeProjectToolsFileTreeSettings,
normalizeProjectToolsGitReviewSettings,
+ normalizeProjectToolsTunnelSettings,
normalizeSettings,
+ type ProjectToolsPanelTab,
workspaceProjectPathKey,
type AppSettings,
} from "./index";
@@ -14,6 +16,16 @@ export type GatewaySettingsSyncProvider = Omit<
> & {
apiKeyConfigured?: boolean;
};
+export type GatewayProjectToolsPanelSync = Pick<
+ AppSettings["customSettings"]["projectToolsPanel"],
+ "activeTab"
+>;
+export type GatewaySettingsSyncCustomSettings = Omit<
+ Partial,
+ "projectToolsPanel"
+> & {
+ projectToolsPanel?: GatewayProjectToolsPanelSync;
+};
export type GatewaySettingsSyncPayload = {
system: AppSettings["system"];
@@ -22,9 +34,12 @@ export type GatewaySettingsSyncPayload = {
agents: AppSettings["agents"];
hooks: AppSettings["hooks"];
cron: AppSettings["cron"];
- remote?: Pick;
+ remote?: Pick<
+ AppSettings["remote"],
+ "enableWebTerminal" | "enableWebGit" | "enableWebTunnels"
+ >;
memory: AppSettings["memory"];
- customSettings: Partial;
+ customSettings: GatewaySettingsSyncCustomSettings;
skills: AppSettings["skills"];
chatRuntimeControls: AppSettings["chatRuntimeControls"];
selectedModel: AppSettings["selectedModel"] | null;
@@ -85,11 +100,15 @@ function collectProviderApiKeyUpdates(
return Object.keys(updates).length > 0 ? updates : undefined;
}
-function syncableCustomSettings(customSettings: AppSettings["customSettings"]) {
- const syncable = { ...customSettings } as Partial;
- delete syncable.projectToolsPanel;
+function syncableCustomSettings(
+ customSettings: AppSettings["customSettings"],
+): GatewaySettingsSyncCustomSettings {
+ const { projectToolsPanel: _projectToolsPanel, ...syncable } = customSettings;
return {
...syncable,
+ projectToolsPanel: {
+ activeTab: customSettings.projectToolsPanel.activeTab,
+ },
chatSidebar: {
projectsCollapsed: false,
recentCollapsed: false,
@@ -255,7 +274,8 @@ function mergeSyncedRemoteSettings(
const source = asObject(incoming);
if (
!Object.prototype.hasOwnProperty.call(source, "enableWebTerminal") &&
- !Object.prototype.hasOwnProperty.call(source, "enableWebGit")
+ !Object.prototype.hasOwnProperty.call(source, "enableWebGit") &&
+ !Object.prototype.hasOwnProperty.call(source, "enableWebTunnels")
) {
return current;
}
@@ -267,6 +287,9 @@ function mergeSyncedRemoteSettings(
enableWebGit: Object.prototype.hasOwnProperty.call(source, "enableWebGit")
? source.enableWebGit === true
: current.enableWebGit,
+ enableWebTunnels: Object.prototype.hasOwnProperty.call(source, "enableWebTunnels")
+ ? source.enableWebTunnels === true
+ : current.enableWebTunnels,
};
}
@@ -327,6 +350,46 @@ function mergeSyncedProjectToolsGitReviewSettings(
};
}
+function mergeSyncedProjectToolsTunnelSettings(
+ current: AppSettings["customSettings"]["projectToolsTunnel"],
+ incoming: unknown,
+): AppSettings["customSettings"]["projectToolsTunnel"] {
+ const currentState = normalizeProjectToolsTunnelSettings(current);
+ const incomingState = normalizeProjectToolsTunnelSettings(incoming);
+ const openFromIncoming = incomingState.openVersion >= currentState.openVersion;
+ return {
+ openProjectPathKeys: openFromIncoming
+ ? incomingState.openProjectPathKeys
+ : currentState.openProjectPathKeys,
+ openVersion: Math.max(currentState.openVersion, incomingState.openVersion),
+ };
+}
+
+function isProjectToolsPanelTab(value: unknown): value is ProjectToolsPanelTab {
+ return (
+ value === "terminal" ||
+ value === "fileTree" ||
+ value === "gitReview" ||
+ value === "tunnel"
+ );
+}
+
+function mergeSyncedProjectToolsPanelSettings(
+ current: AppSettings["customSettings"]["projectToolsPanel"],
+ incoming: unknown,
+): AppSettings["customSettings"]["projectToolsPanel"] {
+ const source = asObject(incoming);
+ const activeTab = isProjectToolsPanelTab(source.activeTab)
+ ? source.activeTab
+ : current.activeTab;
+ return activeTab === current.activeTab
+ ? current
+ : {
+ ...current,
+ activeTab,
+ };
+}
+
export function buildGatewaySettingsSyncPayload(
settings: AppSettings,
options: { includeProviderApiKeyUpdates?: boolean } = {},
@@ -341,6 +404,7 @@ export function buildGatewaySettingsSyncPayload(
remote: {
enableWebTerminal: settings.remote.enableWebTerminal,
enableWebGit: settings.remote.enableWebGit,
+ enableWebTunnels: settings.remote.enableWebTunnels,
},
memory: settings.memory,
customSettings: syncableCustomSettings(settings.customSettings),
@@ -374,9 +438,9 @@ export function applyGatewaySettingsSyncPayload(
? (source.memory as AppSettings["memory"] | null | undefined) ?? {}
: current.memory;
const customSettings = Object.prototype.hasOwnProperty.call(source, "customSettings")
- ? (source.customSettings as AppSettings["customSettings"] | null | undefined) ?? {}
+ ? (source.customSettings as GatewaySettingsSyncCustomSettings | null | undefined) ?? {}
: current.customSettings;
- const incomingCustomSettings = customSettings as Partial;
+ const incomingCustomSettings = customSettings as GatewaySettingsSyncCustomSettings;
return normalizeSettings({
...current,
@@ -413,8 +477,25 @@ export function applyGatewaySettingsSyncPayload(
incomingCustomSettings.projectToolsGitReview,
)
: current.customSettings.projectToolsGitReview,
+ projectToolsTunnel: Object.prototype.hasOwnProperty.call(
+ incomingCustomSettings,
+ "projectToolsTunnel",
+ )
+ ? mergeSyncedProjectToolsTunnelSettings(
+ current.customSettings.projectToolsTunnel,
+ incomingCustomSettings.projectToolsTunnel,
+ )
+ : current.customSettings.projectToolsTunnel,
chatSidebar: current.customSettings.chatSidebar,
- projectToolsPanel: current.customSettings.projectToolsPanel,
+ projectToolsPanel: Object.prototype.hasOwnProperty.call(
+ incomingCustomSettings,
+ "projectToolsPanel",
+ )
+ ? mergeSyncedProjectToolsPanelSettings(
+ current.customSettings.projectToolsPanel,
+ incomingCustomSettings.projectToolsPanel,
+ )
+ : current.customSettings.projectToolsPanel,
},
skills: (source.skills as AppSettings["skills"] | undefined) ?? current.skills,
chatRuntimeControls: Object.prototype.hasOwnProperty.call(source, "chatRuntimeControls")
diff --git a/crates/agent-gateway/web/src/lib/webSettings.ts b/crates/agent-gateway/web/src/lib/webSettings.ts
index e53b3d040..57b829f17 100644
--- a/crates/agent-gateway/web/src/lib/webSettings.ts
+++ b/crates/agent-gateway/web/src/lib/webSettings.ts
@@ -2,6 +2,7 @@ import {
getDefaultSettings,
normalizeProjectToolsFileTreeSettings,
normalizeProjectToolsGitReviewSettings,
+ normalizeProjectToolsTunnelSettings,
normalizeSettings,
type AppSettings,
} from "@/lib/settings";
@@ -40,6 +41,7 @@ function stripSessionOnlyProjectToolsState(settings: AppSettings): AppSettings {
...settings.customSettings,
projectToolsFileTree: normalizeProjectToolsFileTreeSettings({}),
projectToolsGitReview: normalizeProjectToolsGitReviewSettings({}),
+ projectToolsTunnel: normalizeProjectToolsTunnelSettings({}),
},
};
}
diff --git a/crates/agent-gateway/web/src/pages/chat/ChatComposerBar.tsx b/crates/agent-gateway/web/src/pages/chat/ChatComposerBar.tsx
index 5ecd963d5..2d053570c 100644
--- a/crates/agent-gateway/web/src/pages/chat/ChatComposerBar.tsx
+++ b/crates/agent-gateway/web/src/pages/chat/ChatComposerBar.tsx
@@ -6,7 +6,17 @@ import {
type MutableRefObject,
type ReactNode,
} from "react";
-import { Brain, Globe2, Lightbulb, Loader2, Paperclip, Send, Square, X } from "../../components/icons";
+import {
+ Brain,
+ Globe2,
+ Lightbulb,
+ Link2,
+ Loader2,
+ Paperclip,
+ Send,
+ Square,
+ X,
+} from "../../components/icons";
import {
MentionComposer,
@@ -72,7 +82,10 @@ export const ChatComposerBar = memo(function ChatComposerBar(props: {
gitClient?: GitClient | null;
gitWriteEnabled?: boolean;
gitDisabledMessage?: string;
+ tunnelToolAvailable?: boolean;
+ tunnelToolDisabledMessage?: string;
onGitChanged?: (workdir: string) => void;
+ onOpenTunnelToolPanel?: () => void;
onSend: () => void;
onStop: () => void;
onComposerBusyChange: (isBusy: boolean) => void;
@@ -96,7 +109,10 @@ export const ChatComposerBar = memo(function ChatComposerBar(props: {
gitClient,
gitWriteEnabled = true,
gitDisabledMessage,
+ tunnelToolAvailable = false,
+ tunnelToolDisabledMessage,
onGitChanged,
+ onOpenTunnelToolPanel,
onSend,
onStop,
onComposerBusyChange,
@@ -126,6 +142,11 @@ export const ChatComposerBar = memo(function ChatComposerBar(props: {
? t("chat.runtime.thinkingUnavailable")
: t("chat.runtime.thinkingTooltip");
const webSearchTooltip = t("chat.runtime.webSearchTooltip");
+ const tunnelTooltip =
+ tunnelToolDisabledMessage ??
+ (tunnelToolAvailable
+ ? t("chat.runtime.tunnelToolAvailable")
+ : t("chat.runtime.tunnelToolUnavailable"));
useEffect(() => {
if (
@@ -341,6 +362,31 @@ export const ChatComposerBar = memo(function ChatComposerBar(props: {
+
+ {
+ if (!tunnelToolAvailable) return;
+ onOpenTunnelToolPanel?.();
+ }}
+ aria-label={
+ tunnelToolAvailable
+ ? t("chat.runtime.tunnelToolAvailable")
+ : tunnelTooltip
+ }
+ className={cn(
+ "composer-toolbar-action inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full outline-hidden transition-colors",
+ "disabled:pointer-events-none disabled:opacity-40",
+ tunnelToolAvailable
+ ? "text-cyan-600 hover:text-cyan-700 dark:text-cyan-300 dark:hover:text-cyan-200"
+ : "text-muted-foreground hover:text-foreground dark:hover:text-white",
+ )}
+ >
+
+
+
+
{reasoningOptions.length > 0 ? (
+
+
+
{t("settings.remoteWebTunnels")}
+
+ {t("settings.remoteWebTunnelsHint")}
+
+
+
+ {settings.remote.enableWebTunnels
+ ? t("settings.cronViewStatusEnabled")
+ : t("settings.cronViewStatusDisabled")}
+
+
+
) : null}
- {disabledMessage ? (
+ {showDisabledMessage ? (
{disabledMessage}
@@ -1704,7 +1823,9 @@ export function ProjectToolsPanel(props: ProjectToolsPanelProps) {
@@ -1721,7 +1842,9 @@ export function ProjectToolsPanel(props: ProjectToolsPanelProps) {
@@ -1735,6 +1858,22 @@ export function ProjectToolsPanel(props: ProjectToolsPanelProps) {
+
+
+
+
+
+
{t("projectTools.newTunnel")}
+
+ {t("projectTools.tunnelDescription")}
+
+
+
{loading ? (
@@ -1784,6 +1923,22 @@ export function ProjectToolsPanel(props: ProjectToolsPanelProps) {
/>
) : null}
+ {tunnelInitialized && tunnelClient ? (
+
+
+
+ ) : null}
{sessions.length > 0 ? (
> = {
"chat.runtime.webSearchOn": "联网搜索已开启",
"chat.runtime.webSearchOff": "联网搜索已关闭",
"chat.runtime.webSearchTooltip": "联网搜索",
+ "chat.runtime.tunnelToolAvailable": "内网穿透工具已可用",
+ "chat.runtime.tunnelToolUnavailable": "内网穿透工具不可用",
+ "chat.runtime.tunnelAgentModeRequired": "Agent 模式下可使用内网穿透工具",
+ "chat.runtime.tunnelWebDisabled": "Remote 设置未允许 WebUI 内网穿透",
+ "chat.runtime.tunnelRemoteOffline": "Remote Gateway 未连接",
+ "chat.runtime.tunnelSettingsSyncing": "正在同步桌面端设置",
"chat.runtime.reasoning": "思考程度",
"chat.emptyRound": "(无回复)",
"chat.inputHint": "输入消息,@ 引用文件,Enter 发送,Shift+Enter 换行",
@@ -232,6 +238,7 @@ export const translations: Record
> = {
"projectTools.terminalTitle": "终端",
"projectTools.fileTreeTitle": "文件树",
"projectTools.gitReviewTitle": "Git 审查",
+ "projectTools.tunnelTitle": "内网穿透",
"projectTools.resizePanel": "调整项目工具栏宽度",
"projectTools.getStarted": "开始使用",
"projectTools.getStartedHint": "选择一个工具开始",
@@ -241,6 +248,13 @@ export const translations: Record> = {
"projectTools.fileTreeDescription": "浏览和管理项目文件",
"projectTools.newGitReview": "新建审查",
"projectTools.gitReviewDescription": "查看代码变更和提交历史",
+ "projectTools.newTunnel": "新建内网穿透",
+ "projectTools.tunnelDescription": "通过 Gateway 暴露本机 HTTP 服务",
+ "projectTools.tunnelScopeGroup": "切换内网穿透视角",
+ "projectTools.tunnelScopeProject": "当前项目",
+ "projectTools.tunnelScopeGlobal": "全局",
+ "projectTools.tunnelScopeProjectTitle": "管理当前项目内网穿透",
+ "projectTools.tunnelScopeGlobalTitle": "管理全局内网穿透",
"projectTools.newProjectTool": "新建项目工具",
"projectTools.closePanel": "关闭项目工具栏",
"projectTools.close": "关闭",
@@ -250,6 +264,42 @@ export const translations: Record> = {
"projectTools.closeRunningTerminal": "关闭正在运行的终端「{title}」?",
"projectTools.closeFileTree": "关闭文件树",
"projectTools.closeGitReview": "关闭 Git 审查",
+ "projectTools.closeTunnelTab": "关闭内网穿透",
+ "projectTools.tunnelTargetUrl": "本地服务地址",
+ "projectTools.tunnelTargetPlaceholder": "http://localhost:3000",
+ "projectTools.tunnelName": "名称",
+ "projectTools.tunnelNamePlaceholder": "可选",
+ "projectTools.tunnelTtl": "有效期",
+ "projectTools.tunnelTtl15m": "15m",
+ "projectTools.tunnelTtl1h": "1h",
+ "projectTools.tunnelTtl4h": "4h",
+ "projectTools.tunnelTtlInfinite": "无限",
+ "projectTools.tunnelCreate": "创建临时链接",
+ "projectTools.tunnelCreating": "创建中...",
+ "projectTools.tunnelEdit": "编辑链接",
+ "projectTools.tunnelSave": "保存修改",
+ "projectTools.tunnelUpdating": "保存中...",
+ "projectTools.tunnelCancelEdit": "取消编辑",
+ "projectTools.tunnelLoading": "正在加载内网穿透...",
+ "projectTools.tunnelEmpty": "还没有内网穿透链接",
+ "projectTools.tunnelTargetRequired": "请输入本地 HTTP 服务地址。",
+ "projectTools.tunnelInvalidUrl":
+ "请输入有效的 http://localhost 地址,不能包含账号、密码或片段。",
+ "projectTools.tunnelLocalhostOnly": "仅支持 localhost、127.0.0.1 或 [::1]。",
+ "projectTools.tunnelRemoteOffline": "Remote Gateway 未连接,连接后才能创建或关闭 tunnel。",
+ "projectTools.tunnelWebDisabled": "桌面端 Remote 设置未允许 WebUI 创建或关闭内网穿透。",
+ "projectTools.tunnelStatusActive": "运行中",
+ "projectTools.tunnelStatusExpired": "已过期",
+ "projectTools.tunnelStatusOffline": "离线",
+ "projectTools.tunnelTarget": "目标",
+ "projectTools.tunnelExpiresIn": "剩余 {time}",
+ "projectTools.tunnelExpired": "已过期",
+ "projectTools.tunnelScopeProjectBadge": "项目",
+ "projectTools.tunnelScopeGlobalBadge": "全局",
+ "projectTools.tunnelCopyLink": "复制链接",
+ "projectTools.tunnelCopied": "已复制",
+ "projectTools.tunnelOpenLink": "打开链接",
+ "projectTools.tunnelClose": "删除链接",
"projectTools.gitReview.viewChanges": "查看更改",
"projectTools.gitReview.discardChanges": "放弃更改",
"projectTools.gitReview.stageChanges": "暂存更改",
@@ -951,6 +1001,9 @@ export const translations: Record> = {
"settings.remoteWebGit": "允许 WebUI Git",
"settings.remoteWebGitHint":
"开启后,已登录 WebUI 可对本机项目执行分支、暂存、提交和同步操作。",
+ "settings.remoteWebTunnels": "允许 WebUI 内网穿透",
+ "settings.remoteWebTunnelsHint":
+ "开启后,已登录 WebUI 可为本机 localhost HTTP 服务创建和关闭临时访问链接。",
"settings.remoteHeartbeat": "心跳间隔",
"settings.remoteHeartbeatUnit": "秒",
@@ -1296,6 +1349,12 @@ export const translations: Record> = {
"chat.runtime.webSearchOn": "Web search enabled",
"chat.runtime.webSearchOff": "Web search disabled",
"chat.runtime.webSearchTooltip": "Toggle web search",
+ "chat.runtime.tunnelToolAvailable": "Tunnel tool available",
+ "chat.runtime.tunnelToolUnavailable": "Tunnel tool unavailable",
+ "chat.runtime.tunnelAgentModeRequired": "Tunnel tool requires Agent mode",
+ "chat.runtime.tunnelWebDisabled": "Remote WebUI tunnels are not allowed",
+ "chat.runtime.tunnelRemoteOffline": "Remote Gateway is offline",
+ "chat.runtime.tunnelSettingsSyncing": "Syncing desktop settings",
"chat.runtime.reasoning": "Thinking effort",
"chat.emptyRound": "(No reply)",
"chat.inputHint": "Type a message, @ to mention files, Enter to send, Shift+Enter for newline",
@@ -1442,6 +1501,7 @@ export const translations: Record> = {
"projectTools.terminalTitle": "Terminal",
"projectTools.fileTreeTitle": "File Tree",
"projectTools.gitReviewTitle": "Git Review",
+ "projectTools.tunnelTitle": "Tunnel",
"projectTools.resizePanel": "Resize project tools panel",
"projectTools.getStarted": "Get Started",
"projectTools.getStartedHint": "Choose a tool to begin",
@@ -1451,6 +1511,13 @@ export const translations: Record> = {
"projectTools.fileTreeDescription": "Browse and manage project files",
"projectTools.newGitReview": "New Review",
"projectTools.gitReviewDescription": "Review code changes and commit history",
+ "projectTools.newTunnel": "New Tunnel",
+ "projectTools.tunnelDescription": "Expose local HTTP services through Gateway",
+ "projectTools.tunnelScopeGroup": "Switch tunnel scope",
+ "projectTools.tunnelScopeProject": "Current Project",
+ "projectTools.tunnelScopeGlobal": "Global",
+ "projectTools.tunnelScopeProjectTitle": "Manage current project tunnels",
+ "projectTools.tunnelScopeGlobalTitle": "Manage global tunnels",
"projectTools.newProjectTool": "New project tool",
"projectTools.closePanel": "Close project tools panel",
"projectTools.close": "Close",
@@ -1460,6 +1527,44 @@ export const translations: Record> = {
"projectTools.closeRunningTerminal": 'Close running terminal "{title}"?',
"projectTools.closeFileTree": "Close File Tree",
"projectTools.closeGitReview": "Close Git Review",
+ "projectTools.closeTunnelTab": "Close Tunnel",
+ "projectTools.tunnelTargetUrl": "Local service URL",
+ "projectTools.tunnelTargetPlaceholder": "http://localhost:3000",
+ "projectTools.tunnelName": "Name",
+ "projectTools.tunnelNamePlaceholder": "Optional",
+ "projectTools.tunnelTtl": "TTL",
+ "projectTools.tunnelTtl15m": "15m",
+ "projectTools.tunnelTtl1h": "1h",
+ "projectTools.tunnelTtl4h": "4h",
+ "projectTools.tunnelTtlInfinite": "Unlimited",
+ "projectTools.tunnelCreate": "Create temporary link",
+ "projectTools.tunnelCreating": "Creating...",
+ "projectTools.tunnelEdit": "Edit link",
+ "projectTools.tunnelSave": "Save changes",
+ "projectTools.tunnelUpdating": "Saving...",
+ "projectTools.tunnelCancelEdit": "Cancel edit",
+ "projectTools.tunnelLoading": "Loading tunnels...",
+ "projectTools.tunnelEmpty": "No tunnel links yet",
+ "projectTools.tunnelTargetRequired": "Enter a local HTTP service URL.",
+ "projectTools.tunnelInvalidUrl":
+ "Enter a valid http://localhost URL without credentials or fragments.",
+ "projectTools.tunnelLocalhostOnly": "Only localhost, 127.0.0.1, or [::1] are supported.",
+ "projectTools.tunnelRemoteOffline":
+ "Remote Gateway is not connected. Connect it before creating or closing tunnels.",
+ "projectTools.tunnelWebDisabled":
+ "Desktop Remote settings do not allow WebUI tunnel create or close.",
+ "projectTools.tunnelStatusActive": "Active",
+ "projectTools.tunnelStatusExpired": "Expired",
+ "projectTools.tunnelStatusOffline": "Offline",
+ "projectTools.tunnelTarget": "Target",
+ "projectTools.tunnelExpiresIn": "{time} left",
+ "projectTools.tunnelExpired": "Expired",
+ "projectTools.tunnelScopeProjectBadge": "Project",
+ "projectTools.tunnelScopeGlobalBadge": "Global",
+ "projectTools.tunnelCopyLink": "Copy link",
+ "projectTools.tunnelCopied": "Copied",
+ "projectTools.tunnelOpenLink": "Open link",
+ "projectTools.tunnelClose": "Delete link",
"projectTools.gitReview.viewChanges": "View Changes",
"projectTools.gitReview.discardChanges": "Discard Changes",
"projectTools.gitReview.stageChanges": "Stage Changes",
@@ -2196,6 +2301,9 @@ export const translations: Record> = {
"settings.remoteWebGit": "Allow WebUI Git",
"settings.remoteWebGitHint":
"Allow authenticated WebUI clients to run branch, stage, commit, and sync operations on local projects.",
+ "settings.remoteWebTunnels": "Allow WebUI Tunnels",
+ "settings.remoteWebTunnelsHint":
+ "Allow authenticated WebUI clients to create and close temporary links for local localhost HTTP services.",
"settings.remoteHeartbeat": "Heartbeat Interval",
"settings.remoteHeartbeatUnit": "seconds",
diff --git a/crates/agent-gui/src/lib/settings/index.ts b/crates/agent-gui/src/lib/settings/index.ts
index 58c37e658..3ce59bcd4 100644
--- a/crates/agent-gui/src/lib/settings/index.ts
+++ b/crates/agent-gui/src/lib/settings/index.ts
@@ -127,7 +127,7 @@ export type ChatSidebarSettings = {
recentCollapsed: boolean;
};
-export type ProjectToolsPanelTab = "terminal" | "fileTree" | "gitReview";
+export type ProjectToolsPanelTab = "terminal" | "fileTree" | "gitReview" | "tunnel";
export type ProjectToolsPanelSettings = {
width: number;
@@ -154,6 +154,11 @@ export type ProjectToolsGitReviewSettings = {
openVersion: number;
};
+export type ProjectToolsTunnelSettings = {
+ openProjectPathKeys: string[];
+ openVersion: number;
+};
+
export type ProjectToolsFileTreeStatePatch = Partial & {
bumpRevision?: boolean;
bumpStateVersion?: boolean;
@@ -165,6 +170,7 @@ export type CustomSettings = {
projectToolsPanel: ProjectToolsPanelSettings;
projectToolsFileTree: ProjectToolsFileTreeSettings;
projectToolsGitReview: ProjectToolsGitReviewSettings;
+ projectToolsTunnel: ProjectToolsTunnelSettings;
};
export type UpdateSettings = {
@@ -256,6 +262,7 @@ export type RemoteSettings = {
heartbeatInterval: number;
enableWebTerminal: boolean;
enableWebGit: boolean;
+ enableWebTunnels: boolean;
};
export type AppSettings = {
@@ -1047,6 +1054,7 @@ export function normalizeRemoteSettings(input: unknown): RemoteSettings {
heartbeatInterval: normalizePositiveInteger(obj.heartbeatInterval, 30),
enableWebTerminal: obj.enableWebTerminal === true,
enableWebGit: obj.enableWebGit === true,
+ enableWebTunnels: obj.enableWebTunnels === true,
};
}
@@ -1537,6 +1545,23 @@ export function normalizeProjectToolsGitReviewSettings(
};
}
+export function normalizeProjectToolsTunnelSettings(
+ input: unknown,
+): ProjectToolsTunnelSettings {
+ const obj = (input && typeof input === "object" ? input : {}) as Record;
+ const openProjectPathKeys = Array.from(
+ new Set(
+ (Array.isArray(obj.openProjectPathKeys) ? obj.openProjectPathKeys : [])
+ .map((pathKey) => workspaceProjectPathKey(pathKey))
+ .filter(Boolean),
+ ),
+ ).sort();
+ return {
+ openProjectPathKeys,
+ openVersion: normalizeIntegerInRange(obj.openVersion, 0, Number.MAX_SAFE_INTEGER, 0),
+ };
+}
+
export function normalizeProjectToolsPanelTabOrder(input: unknown): string[] {
if (!Array.isArray(input)) return [];
const order: string[] = [];
@@ -1585,7 +1610,8 @@ export function normalizeCustomSettings(
const projectToolsPanelActiveTab =
projectToolsPanel.activeTab === "terminal" ||
projectToolsPanel.activeTab === "fileTree" ||
- projectToolsPanel.activeTab === "gitReview"
+ projectToolsPanel.activeTab === "gitReview" ||
+ projectToolsPanel.activeTab === "tunnel"
? projectToolsPanel.activeTab
: "fileTree";
const projectToolsFileTree = (
@@ -1598,6 +1624,11 @@ export function normalizeCustomSettings(
? obj.projectToolsGitReview
: {}
) as unknown;
+ const projectToolsTunnel = (
+ obj.projectToolsTunnel && typeof obj.projectToolsTunnel === "object"
+ ? obj.projectToolsTunnel
+ : {}
+ ) as unknown;
return {
conversationTitleModel: normalizeSelectedModelForProviders(
normalizeSelectedModel(obj.conversationTitleModel),
@@ -1619,6 +1650,7 @@ export function normalizeCustomSettings(
},
projectToolsFileTree: normalizeProjectToolsFileTreeSettings(projectToolsFileTree),
projectToolsGitReview: normalizeProjectToolsGitReviewSettings(projectToolsGitReview),
+ projectToolsTunnel: normalizeProjectToolsTunnelSettings(projectToolsTunnel),
};
}
@@ -1660,6 +1692,7 @@ export function getDefaultSettings(): AppSettings {
heartbeatInterval: 30,
enableWebTerminal: false,
enableWebGit: false,
+ enableWebTunnels: false,
},
memory: normalizeMemorySettings({}, customProviders),
customSettings: normalizeCustomSettings({}, customProviders),
@@ -1799,6 +1832,10 @@ function hasProjectToolsGitReviewSessionState(state: ProjectToolsGitReviewSettin
return state.openVersion > 0 || state.openProjectPathKeys.length > 0;
}
+function hasProjectToolsTunnelSessionState(state: ProjectToolsTunnelSettings): boolean {
+ return state.openVersion > 0 || state.openProjectPathKeys.length > 0;
+}
+
export function preserveProjectToolsSessionState(
next: AppSettings,
current: AppSettings,
@@ -1809,6 +1846,9 @@ export function preserveProjectToolsSessionState(
const currentGitReview = normalizeProjectToolsGitReviewSettings(
current.customSettings.projectToolsGitReview,
);
+ const currentTunnel = normalizeProjectToolsTunnelSettings(
+ current.customSettings.projectToolsTunnel,
+ );
return normalizeSettings({
...next,
@@ -1820,6 +1860,9 @@ export function preserveProjectToolsSessionState(
projectToolsGitReview: hasProjectToolsGitReviewSessionState(currentGitReview)
? currentGitReview
: next.customSettings.projectToolsGitReview,
+ projectToolsTunnel: hasProjectToolsTunnelSessionState(currentTunnel)
+ ? currentTunnel
+ : next.customSettings.projectToolsTunnel,
},
});
}
@@ -1881,15 +1924,22 @@ export function removeProjectToolsProjectState(
(pathKey) => pathKey !== normalizedPathKey,
);
const removedOpenProjectPathKey = nextOpenProjectPathKeys.length !== openProjectPathKeys.length;
- const gitReviewOpenProjectPathKeys =
- prev.customSettings.projectToolsGitReview.openProjectPathKeys
- .map((pathKey) => workspaceProjectPathKey(pathKey))
- .filter(Boolean);
+ const gitReviewOpenProjectPathKeys = prev.customSettings.projectToolsGitReview.openProjectPathKeys
+ .map((pathKey) => workspaceProjectPathKey(pathKey))
+ .filter(Boolean);
const nextGitReviewOpenProjectPathKeys = gitReviewOpenProjectPathKeys.filter(
(pathKey) => pathKey !== normalizedPathKey,
);
const removedGitReviewOpenProjectPathKey =
nextGitReviewOpenProjectPathKeys.length !== gitReviewOpenProjectPathKeys.length;
+ const tunnelOpenProjectPathKeys = prev.customSettings.projectToolsTunnel.openProjectPathKeys
+ .map((pathKey) => workspaceProjectPathKey(pathKey))
+ .filter(Boolean);
+ const nextTunnelOpenProjectPathKeys = tunnelOpenProjectPathKeys.filter(
+ (pathKey) => pathKey !== normalizedPathKey,
+ );
+ const removedTunnelOpenProjectPathKey =
+ nextTunnelOpenProjectPathKeys.length !== tunnelOpenProjectPathKeys.length;
const hasFileTreeProjectState = Object.hasOwn(
prev.customSettings.projectToolsFileTree.projects,
normalizedPathKey,
@@ -1900,6 +1950,7 @@ export function removeProjectToolsProjectState(
!hasTabOrder &&
!removedOpenProjectPathKey &&
!removedGitReviewOpenProjectPathKey &&
+ !removedTunnelOpenProjectPathKey &&
!hasFileTreeProjectState
) {
return prev;
@@ -1943,6 +1994,15 @@ export function removeProjectToolsProjectState(
? prev.customSettings.projectToolsGitReview.openVersion + 1
: prev.customSettings.projectToolsGitReview.openVersion,
},
+ projectToolsTunnel: {
+ ...prev.customSettings.projectToolsTunnel,
+ openProjectPathKeys: removedTunnelOpenProjectPathKey
+ ? nextTunnelOpenProjectPathKeys.sort()
+ : prev.customSettings.projectToolsTunnel.openProjectPathKeys,
+ openVersion: removedTunnelOpenProjectPathKey
+ ? prev.customSettings.projectToolsTunnel.openVersion + 1
+ : prev.customSettings.projectToolsTunnel.openVersion,
+ },
});
}
@@ -2034,6 +2094,44 @@ export function updateProjectToolsGitReviewOpen(
});
}
+export function isProjectToolsTunnelOpen(
+ customSettings: CustomSettings,
+ projectPathKey: string,
+): boolean {
+ const normalizedPathKey = workspaceProjectPathKey(projectPathKey);
+ return (
+ normalizedPathKey !== "" &&
+ customSettings.projectToolsTunnel.openProjectPathKeys.includes(normalizedPathKey)
+ );
+}
+
+export function updateProjectToolsTunnelOpen(
+ prev: AppSettings,
+ projectPathKey: string,
+ open: boolean,
+): AppSettings {
+ const normalizedPathKey = workspaceProjectPathKey(projectPathKey);
+ if (!normalizedPathKey) return prev;
+ const openProjectPathKeys = new Set(
+ prev.customSettings.projectToolsTunnel.openProjectPathKeys
+ .map((pathKey) => workspaceProjectPathKey(pathKey))
+ .filter(Boolean),
+ );
+ if (openProjectPathKeys.has(normalizedPathKey) === open) return prev;
+ if (open) {
+ openProjectPathKeys.add(normalizedPathKey);
+ } else {
+ openProjectPathKeys.delete(normalizedPathKey);
+ }
+ return updateCustomSettings(prev, {
+ projectToolsTunnel: {
+ ...prev.customSettings.projectToolsTunnel,
+ openProjectPathKeys: Array.from(openProjectPathKeys).sort(),
+ openVersion: prev.customSettings.projectToolsTunnel.openVersion + 1,
+ },
+ });
+}
+
function projectToolsFileTreeProjectStateEqual(
left: ProjectToolsFileTreeProjectState,
right: ProjectToolsFileTreeProjectState,
diff --git a/crates/agent-gui/src/lib/settings/storage.ts b/crates/agent-gui/src/lib/settings/storage.ts
index e7361e923..1040aa0ad 100644
--- a/crates/agent-gui/src/lib/settings/storage.ts
+++ b/crates/agent-gui/src/lib/settings/storage.ts
@@ -8,6 +8,7 @@ import {
normalizeChatRuntimeControls,
normalizeProjectToolsFileTreeSettings,
normalizeProjectToolsGitReviewSettings,
+ normalizeProjectToolsTunnelSettings,
normalizeProjectToolsPanelTabOrders,
normalizeSelectedModel,
normalizeSettings,
@@ -58,6 +59,7 @@ function toPersistedLocalCustomSettings(
...customSettings,
projectToolsFileTree: normalizeProjectToolsFileTreeSettings({}),
projectToolsGitReview: normalizeProjectToolsGitReviewSettings({}),
+ projectToolsTunnel: normalizeProjectToolsTunnelSettings({}),
};
}
@@ -95,7 +97,8 @@ function readLocalUiSettings(): {
const projectToolsPanelActiveTab =
projectToolsPanel.activeTab === "terminal" ||
projectToolsPanel.activeTab === "fileTree" ||
- projectToolsPanel.activeTab === "gitReview"
+ projectToolsPanel.activeTab === "gitReview" ||
+ projectToolsPanel.activeTab === "tunnel"
? projectToolsPanel.activeTab
: "fileTree";
return toPersistedLocalCustomSettings({
@@ -113,6 +116,7 @@ function readLocalUiSettings(): {
},
projectToolsFileTree: normalizeProjectToolsFileTreeSettings({}),
projectToolsGitReview: normalizeProjectToolsGitReviewSettings({}),
+ projectToolsTunnel: normalizeProjectToolsTunnelSettings({}),
});
}
diff --git a/crates/agent-gui/src/lib/settings/sync.ts b/crates/agent-gui/src/lib/settings/sync.ts
index 0d7282e8e..044f4a2f2 100644
--- a/crates/agent-gui/src/lib/settings/sync.ts
+++ b/crates/agent-gui/src/lib/settings/sync.ts
@@ -3,7 +3,9 @@ import {
normalizeChatRuntimeControls,
normalizeProjectToolsFileTreeSettings,
normalizeProjectToolsGitReviewSettings,
+ normalizeProjectToolsTunnelSettings,
normalizeSettings,
+ type ProjectToolsPanelTab,
workspaceProjectPathKey,
} from "./index";
@@ -11,6 +13,16 @@ export type GatewayProviderApiKeyUpdates = Record;
export type GatewaySettingsSyncProvider = Omit & {
apiKeyConfigured?: boolean;
};
+export type GatewayProjectToolsPanelSync = Pick<
+ AppSettings["customSettings"]["projectToolsPanel"],
+ "activeTab"
+>;
+export type GatewaySettingsSyncCustomSettings = Omit<
+ Partial,
+ "projectToolsPanel"
+> & {
+ projectToolsPanel?: GatewayProjectToolsPanelSync;
+};
export type GatewaySettingsSyncPayload = {
system: AppSettings["system"];
@@ -19,9 +31,9 @@ export type GatewaySettingsSyncPayload = {
agents: AppSettings["agents"];
hooks: AppSettings["hooks"];
cron: AppSettings["cron"];
- remote?: Pick;
+ remote?: Pick;
memory: AppSettings["memory"];
- customSettings: Partial;
+ customSettings: GatewaySettingsSyncCustomSettings;
skills: AppSettings["skills"];
chatRuntimeControls: AppSettings["chatRuntimeControls"];
selectedModel: AppSettings["selectedModel"] | null;
@@ -82,11 +94,15 @@ function collectProviderApiKeyUpdates(
return Object.keys(updates).length > 0 ? updates : undefined;
}
-function syncableCustomSettings(customSettings: AppSettings["customSettings"]) {
- const syncable = { ...customSettings } as Partial;
- delete syncable.projectToolsPanel;
+function syncableCustomSettings(
+ customSettings: AppSettings["customSettings"],
+): GatewaySettingsSyncCustomSettings {
+ const { projectToolsPanel: _projectToolsPanel, ...syncable } = customSettings;
return {
...syncable,
+ projectToolsPanel: {
+ activeTab: customSettings.projectToolsPanel.activeTab,
+ },
chatSidebar: {
projectsCollapsed: false,
recentCollapsed: false,
@@ -161,10 +177,7 @@ function mergeSyncedSystemSettings(
}
const incomingSystem = incoming as AppSettings["system"];
- const activeWorkspaceProjectId = resolveSyncedActiveWorkspaceProjectId(
- current,
- incomingSystem,
- );
+ const activeWorkspaceProjectId = resolveSyncedActiveWorkspaceProjectId(current, incomingSystem);
if (!Array.isArray(incomingSystem.workspaceProjects)) {
return {
...incomingSystem,
@@ -247,7 +260,11 @@ function mergeSyncedRemoteSettings(
incoming: unknown,
): AppSettings["remote"] {
const source = asObject(incoming);
- if (!Object.hasOwn(source, "enableWebTerminal") && !Object.hasOwn(source, "enableWebGit")) {
+ if (
+ !Object.hasOwn(source, "enableWebTerminal") &&
+ !Object.hasOwn(source, "enableWebGit") &&
+ !Object.hasOwn(source, "enableWebTunnels")
+ ) {
return current;
}
return {
@@ -258,6 +275,9 @@ function mergeSyncedRemoteSettings(
enableWebGit: Object.hasOwn(source, "enableWebGit")
? source.enableWebGit === true
: current.enableWebGit,
+ enableWebTunnels: Object.hasOwn(source, "enableWebTunnels")
+ ? source.enableWebTunnels === true
+ : current.enableWebTunnels,
};
}
@@ -318,6 +338,46 @@ function mergeSyncedProjectToolsGitReviewSettings(
};
}
+function mergeSyncedProjectToolsTunnelSettings(
+ current: AppSettings["customSettings"]["projectToolsTunnel"],
+ incoming: unknown,
+): AppSettings["customSettings"]["projectToolsTunnel"] {
+ const currentState = normalizeProjectToolsTunnelSettings(current);
+ const incomingState = normalizeProjectToolsTunnelSettings(incoming);
+ const openFromIncoming = incomingState.openVersion >= currentState.openVersion;
+ return {
+ openProjectPathKeys: openFromIncoming
+ ? incomingState.openProjectPathKeys
+ : currentState.openProjectPathKeys,
+ openVersion: Math.max(currentState.openVersion, incomingState.openVersion),
+ };
+}
+
+function isProjectToolsPanelTab(value: unknown): value is ProjectToolsPanelTab {
+ return (
+ value === "terminal" ||
+ value === "fileTree" ||
+ value === "gitReview" ||
+ value === "tunnel"
+ );
+}
+
+function mergeSyncedProjectToolsPanelSettings(
+ current: AppSettings["customSettings"]["projectToolsPanel"],
+ incoming: unknown,
+): AppSettings["customSettings"]["projectToolsPanel"] {
+ const source = asObject(incoming);
+ const activeTab = isProjectToolsPanelTab(source.activeTab)
+ ? source.activeTab
+ : current.activeTab;
+ return activeTab === current.activeTab
+ ? current
+ : {
+ ...current,
+ activeTab,
+ };
+}
+
export function buildGatewaySettingsSyncPayload(
settings: AppSettings,
options: { includeProviderApiKeyUpdates?: boolean } = {},
@@ -332,6 +392,7 @@ export function buildGatewaySettingsSyncPayload(
remote: {
enableWebTerminal: settings.remote.enableWebTerminal,
enableWebGit: settings.remote.enableWebGit,
+ enableWebTunnels: settings.remote.enableWebTunnels,
},
memory: settings.memory,
customSettings: syncableCustomSettings(settings.customSettings),
@@ -365,9 +426,9 @@ export function applyGatewaySettingsSyncPayload(
? ((source.memory as AppSettings["memory"] | null | undefined) ?? {})
: current.memory;
const customSettings = Object.hasOwn(source, "customSettings")
- ? ((source.customSettings as AppSettings["customSettings"] | null | undefined) ?? {})
+ ? ((source.customSettings as GatewaySettingsSyncCustomSettings | null | undefined) ?? {})
: current.customSettings;
- const incomingCustomSettings = customSettings as Partial;
+ const incomingCustomSettings = customSettings as GatewaySettingsSyncCustomSettings;
return normalizeSettings({
...current,
@@ -398,8 +459,19 @@ export function applyGatewaySettingsSyncPayload(
incomingCustomSettings.projectToolsGitReview,
)
: current.customSettings.projectToolsGitReview,
+ projectToolsTunnel: Object.hasOwn(incomingCustomSettings, "projectToolsTunnel")
+ ? mergeSyncedProjectToolsTunnelSettings(
+ current.customSettings.projectToolsTunnel,
+ incomingCustomSettings.projectToolsTunnel,
+ )
+ : current.customSettings.projectToolsTunnel,
chatSidebar: current.customSettings.chatSidebar,
- projectToolsPanel: current.customSettings.projectToolsPanel,
+ projectToolsPanel: Object.hasOwn(incomingCustomSettings, "projectToolsPanel")
+ ? mergeSyncedProjectToolsPanelSettings(
+ current.customSettings.projectToolsPanel,
+ incomingCustomSettings.projectToolsPanel,
+ )
+ : current.customSettings.projectToolsPanel,
},
skills: (source.skills as AppSettings["skills"] | undefined) ?? current.skills,
chatRuntimeControls: Object.hasOwn(source, "chatRuntimeControls")
diff --git a/crates/agent-gui/src/lib/tools/builtinRegistry.ts b/crates/agent-gui/src/lib/tools/builtinRegistry.ts
index 17dfbcbbc..2ae91e9a6 100644
--- a/crates/agent-gui/src/lib/tools/builtinRegistry.ts
+++ b/crates/agent-gui/src/lib/tools/builtinRegistry.ts
@@ -34,6 +34,7 @@ import { createShellTools } from "./shellTools";
import type { SkillAccessPolicy } from "./skillAccessPolicy";
import { createSkillTools } from "./skillTools";
import { createTerminalTools } from "./terminalTools";
+import { createTunnelManagerTools } from "./tunnelManagerTools";
export type BuiltinToolRegistry = {
tools: BuiltinToolBundle["tools"];
@@ -166,6 +167,23 @@ type BuildBuiltinBaseToolRegistryParams = {
onMcpLoadError?: (message: string) => void;
mcpLoadFailureMode?: "continue" | "throw";
memoryToolMode?: "rw" | "ro";
+ remoteWebTunnelsEnabled?: boolean;
+ remoteGatewayOnline?: boolean;
+ tunnelProjectPathKey?: string;
+ onTunnelsChanged?: (change: {
+ action: "create" | "close";
+ tunnel: {
+ id: string;
+ slug: string;
+ name: string;
+ targetUrl: string;
+ publicUrl: string;
+ createdAt: number;
+ expiresAt: number;
+ status: "active" | "expired" | "offline";
+ projectPathKey?: string;
+ };
+ }) => void | Promise;
};
async function buildBaseBuiltinToolBundles(params: BuildBuiltinBaseToolRegistryParams) {
@@ -217,6 +235,15 @@ async function buildBaseBuiltinToolBundles(params: BuildBuiltinBaseToolRegistryP
workdir: params.workdir,
mode: params.memoryToolMode ?? "rw",
}),
+ createTunnelManagerTools({
+ enabled:
+ params.remoteWebTunnelsEnabled === true &&
+ params.remoteGatewayOnline === true &&
+ params.runtimeScope === "chat",
+ runtimeScope: params.runtimeScope,
+ projectPathKey: params.tunnelProjectPathKey,
+ onTunnelsChanged: params.onTunnelsChanged,
+ }),
...(params.runtimeScope === "chat"
? [
createTerminalTools({
diff --git a/crates/agent-gui/src/lib/tools/tunnelManagerTools.ts b/crates/agent-gui/src/lib/tools/tunnelManagerTools.ts
new file mode 100644
index 000000000..310a0b98a
--- /dev/null
+++ b/crates/agent-gui/src/lib/tools/tunnelManagerTools.ts
@@ -0,0 +1,322 @@
+import type { Tool, ToolCall, ToolResultMessage } from "@earendil-works/pi-ai";
+import { invoke } from "@tauri-apps/api/core";
+import { Type } from "typebox";
+
+import { type BuiltinToolBundle, createBuiltinMetadataMap } from "./builtinTypes";
+
+type TunnelTtlSeconds = 0 | 900 | 3600 | 14400;
+
+export const TUNNEL_MANAGER_CHANGED_EVENT = "liveagent:tunnel-manager-changed";
+
+type TunnelSummary = {
+ id: string;
+ slug: string;
+ name: string;
+ targetUrl: string;
+ publicUrl: string;
+ createdAt: number;
+ expiresAt: number;
+ activeConnections: number;
+ status: "active" | "expired" | "offline";
+ projectPathKey?: string;
+};
+
+type TunnelManagerTunnelSummary = Omit;
+
+export type TunnelChangeAction = "create" | "close";
+
+export type TunnelManagerChange = {
+ action: TunnelChangeAction;
+ tunnel: TunnelManagerTunnelSummary;
+};
+
+type TunnelCreateInput = {
+ targetUrl: string;
+ name?: string;
+ ttlSeconds: TunnelTtlSeconds;
+ projectPathKey?: string;
+};
+
+type TunnelManagerAction = "list" | "create" | "close";
+
+type TunnelManagerDetails = {
+ kind: "tunnel_manager";
+ action: TunnelManagerAction;
+ tunnels?: TunnelManagerTunnelSummary[];
+ tunnel?: TunnelManagerTunnelSummary;
+};
+
+const TUNNEL_MANAGER_TOOL: Tool = {
+ name: "TunnelManager",
+ description:
+ "Manage temporary Remote HTTP tunnels for localhost services through the Gateway. Use list to inspect active tunnels, create to expose a local http://localhost/127.0.0.1/[::1] service, and close to revoke a tunnel.",
+ parameters: Type.Object({
+ action: Type.Union(
+ [Type.Literal("list"), Type.Literal("create"), Type.Literal("close")],
+ {
+ description: "Tunnel action to perform.",
+ },
+ ),
+ targetUrl: Type.Optional(
+ Type.String({
+ description:
+ "Required for action=create. Local HTTP target, e.g. http://localhost:3000 or http://127.0.0.1:5173/app.",
+ }),
+ ),
+ name: Type.Optional(
+ Type.String({
+ description: "Optional display name for a created tunnel.",
+ }),
+ ),
+ ttlSeconds: Type.Optional(
+ Type.Union([Type.Literal(0), Type.Literal(900), Type.Literal(3600), Type.Literal(14400)], {
+ description: "Optional tunnel lifetime. Use 0 for unlimited. Defaults to 3600 seconds.",
+ }),
+ ),
+ id: Type.Optional(
+ Type.String({
+ description: "Tunnel id for action=close. Preferred over slug when available.",
+ }),
+ ),
+ slug: Type.Optional(
+ Type.String({
+ description: "Tunnel slug for action=close when id is not known.",
+ }),
+ ),
+ }),
+};
+
+function asErrorMessage(err: unknown) {
+ return err instanceof Error ? err.message : String(err);
+}
+
+function asArgs(value: unknown): Record {
+ return value && typeof value === "object" && !Array.isArray(value)
+ ? (value as Record)
+ : {};
+}
+
+function normalizeAction(value: unknown): TunnelManagerAction {
+ if (value === "list" || value === "create" || value === "close") {
+ return value;
+ }
+ throw new Error('TunnelManager.action must be "list", "create", or "close".');
+}
+
+function normalizeTtlSeconds(value: unknown): TunnelTtlSeconds {
+ if (value === undefined || value === null) {
+ return 3600;
+ }
+ if (value === 0 || value === 900 || value === 3600 || value === 14400) {
+ return value;
+ }
+ throw new Error("TunnelManager.ttlSeconds must be 0, 900, 3600, or 14400.");
+}
+
+function normalizeOptionalText(value: unknown) {
+ return typeof value === "string" ? value.trim() : "";
+}
+
+function formatRemaining(expiresAt: number) {
+ if (!expiresAt) return "unlimited";
+ const seconds = Math.max(0, Math.floor(expiresAt - Date.now() / 1000));
+ if (seconds <= 0) return "expired";
+ const hours = Math.floor(seconds / 3600);
+ const minutes = Math.ceil((seconds % 3600) / 60);
+ if (hours <= 0) return `${minutes}m`;
+ return minutes > 0 && minutes < 60 ? `${hours}h ${minutes}m` : `${hours}h`;
+}
+
+function stripConnectionCount(tunnel: TunnelSummary): TunnelManagerTunnelSummary {
+ const { activeConnections: _activeConnections, ...summary } = tunnel;
+ return summary;
+}
+
+function formatTunnelLine(tunnel: TunnelManagerTunnelSummary) {
+ const name = tunnel.name.trim() || tunnel.targetUrl;
+ return [
+ `- ${name}`,
+ ` id: ${tunnel.id}`,
+ ` slug: ${tunnel.slug}`,
+ ` target: ${tunnel.targetUrl}`,
+ ` public: ${tunnel.publicUrl}`,
+ ` status: ${tunnel.status}`,
+ ` ttl: ${formatRemaining(tunnel.expiresAt)}`,
+ ].join("\n");
+}
+
+function okResult(params: {
+ toolCall: ToolCall;
+ action: TunnelManagerAction;
+ text: string;
+ tunnels?: TunnelManagerTunnelSummary[];
+ tunnel?: TunnelManagerTunnelSummary;
+}): ToolResultMessage {
+ const details: TunnelManagerDetails = {
+ kind: "tunnel_manager",
+ action: params.action,
+ ...(params.tunnels ? { tunnels: params.tunnels } : {}),
+ ...(params.tunnel ? { tunnel: params.tunnel } : {}),
+ };
+ return {
+ role: "toolResult",
+ toolCallId: params.toolCall.id,
+ toolName: params.toolCall.name,
+ content: [{ type: "text", text: params.text }],
+ details,
+ isError: false,
+ timestamp: Date.now(),
+ };
+}
+
+function errorResult(
+ toolCall: ToolCall,
+ message: string,
+ action: TunnelManagerAction = "list",
+): ToolResultMessage {
+ return {
+ role: "toolResult",
+ toolCallId: toolCall.id,
+ toolName: toolCall.name,
+ content: [{ type: "text", text: `TunnelManager failed: ${message}` }],
+ details: {
+ kind: "tunnel_manager",
+ action,
+ errors: [message],
+ },
+ isError: true,
+ timestamp: Date.now(),
+ };
+}
+
+async function listTunnels() {
+ return invoke("gateway_tunnel_list");
+}
+
+async function createTunnel(input: TunnelCreateInput) {
+ return invoke("gateway_tunnel_create", { input });
+}
+
+async function closeTunnel(id: string) {
+ return invoke("gateway_tunnel_close", { tunnel_id: id });
+}
+
+async function executeTunnelManager(
+ toolCall: ToolCall,
+ params: {
+ projectPathKey?: string;
+ onTunnelsChanged?: (change: TunnelManagerChange) => void | Promise;
+ },
+ signal?: AbortSignal,
+): Promise {
+ if (signal?.aborted) {
+ return {
+ role: "toolResult",
+ toolCallId: toolCall.id,
+ toolName: toolCall.name,
+ content: [{ type: "text", text: "Cancelled" }],
+ details: {},
+ isError: true,
+ timestamp: Date.now(),
+ };
+ }
+
+ try {
+ const args = asArgs(toolCall.arguments);
+ const action = normalizeAction(args.action);
+
+ if (action === "list") {
+ const tunnels = (await listTunnels()).map(stripConnectionCount);
+ const text =
+ tunnels.length === 0
+ ? "No Remote HTTP tunnels are currently registered."
+ : ["Remote HTTP tunnels:", ...tunnels.map(formatTunnelLine)].join("\n");
+ return okResult({ toolCall, action, text, tunnels });
+ }
+
+ if (action === "create") {
+ const targetUrl = normalizeOptionalText(args.targetUrl);
+ if (!targetUrl) {
+ throw new Error("TunnelManager.targetUrl is required for action=create.");
+ }
+ const tunnel = await createTunnel({
+ targetUrl,
+ name: normalizeOptionalText(args.name) || undefined,
+ ttlSeconds: normalizeTtlSeconds(args.ttlSeconds),
+ ...(params.projectPathKey?.trim() ? { projectPathKey: params.projectPathKey.trim() } : {}),
+ });
+ const visibleTunnel = stripConnectionCount(tunnel);
+ await params.onTunnelsChanged?.({ action: "create", tunnel: visibleTunnel });
+ return okResult({
+ toolCall,
+ action,
+ text: ["Created Remote HTTP tunnel:", formatTunnelLine(visibleTunnel)].join("\n"),
+ tunnel: visibleTunnel,
+ });
+ }
+
+ const id = normalizeOptionalText(args.id);
+ const slug = normalizeOptionalText(args.slug);
+ if (!id && !slug) {
+ throw new Error("TunnelManager.id or TunnelManager.slug is required for action=close.");
+ }
+
+ let tunnelId = id;
+ if (!tunnelId) {
+ const tunnels = await listTunnels();
+ tunnelId = tunnels.find((tunnel) => tunnel.slug === slug)?.id ?? "";
+ if (!tunnelId) {
+ throw new Error(`No tunnel found for slug "${slug}".`);
+ }
+ }
+ const tunnel = await closeTunnel(tunnelId);
+ const visibleTunnel = stripConnectionCount(tunnel);
+ await params.onTunnelsChanged?.({ action: "close", tunnel: visibleTunnel });
+ return okResult({
+ toolCall,
+ action,
+ text: ["Closed Remote HTTP tunnel:", formatTunnelLine(visibleTunnel)].join("\n"),
+ tunnel: visibleTunnel,
+ });
+ } catch (err) {
+ const args = asArgs(toolCall.arguments);
+ const action =
+ args.action === "create" || args.action === "close" || args.action === "list"
+ ? args.action
+ : undefined;
+ return errorResult(toolCall, asErrorMessage(err), action);
+ }
+}
+
+export function createTunnelManagerTools(params: {
+ enabled: boolean;
+ runtimeScope: "chat" | "cron_auto_prompt";
+ projectPathKey?: string;
+ onTunnelsChanged?: (change: TunnelManagerChange) => void | Promise;
+}): BuiltinToolBundle {
+ const tools = params.enabled && params.runtimeScope === "chat" ? [TUNNEL_MANAGER_TOOL] : [];
+ return {
+ groupId: "system",
+ tools,
+ executeToolCall: (toolCall, signal) =>
+ executeTunnelManager(
+ toolCall,
+ {
+ projectPathKey: params.projectPathKey,
+ onTunnelsChanged: params.onTunnelsChanged,
+ },
+ signal,
+ ),
+ metadataByName: createBuiltinMetadataMap(
+ tools.map((tool) => [
+ tool.name,
+ {
+ groupId: "system" as const,
+ kind: "tunnel_manager",
+ isReadOnly: false,
+ displayCategory: "system" as const,
+ },
+ ]),
+ ),
+ };
+}
diff --git a/crates/agent-gui/src/pages/ChatPage.tsx b/crates/agent-gui/src/pages/ChatPage.tsx
index 10e0033c0..046e1b8c2 100644
--- a/crates/agent-gui/src/pages/ChatPage.tsx
+++ b/crates/agent-gui/src/pages/ChatPage.tsx
@@ -27,6 +27,12 @@ import { type NotifyItem, NotifyToast } from "../components/chat/NotifyToast";
import { SharedHistoryManagerModal } from "../components/chat/SharedHistoryManagerModal";
import { Ban, PanelRightClose, PanelRightOpen, Terminal, Upload } from "../components/icons";
import { ProjectToolsPanel } from "../components/project-tools/ProjectToolsPanel";
+import type {
+ LocalTunnelClient,
+ TunnelCreateInput,
+ TunnelSummary,
+ TunnelUpdateInput,
+} from "../components/project-tools/LocalTunnelPanel";
import type { WorkspaceCodeEditorOpenRequest } from "../components/workspace-editor/WorkspaceCodeEditorOverlay";
import type { WorkspaceImagePreviewOpenRequest } from "../components/workspace-editor/WorkspaceImagePreviewOverlay";
import { isWorkspaceImagePath } from "../components/workspace-editor/workspaceImagePreview";
@@ -112,6 +118,7 @@ import {
isAgentExecutionMode,
isProjectToolsFileTreeOpen,
isProjectToolsGitReviewOpen,
+ isProjectToolsTunnelOpen,
normalizeChatRuntimeControlsForProvider,
type ProviderId,
type SelectedModel,
@@ -125,6 +132,7 @@ import {
updateProjectToolsFileTreeProjectState,
updateProjectToolsFileTreeOpen,
updateProjectToolsGitReviewOpen,
+ updateProjectToolsTunnelOpen,
updateProjectToolsPanelTabOrder,
updateChatRuntimeControlsForProvider,
updateMcp,
@@ -700,6 +708,7 @@ export function ChatPage(props: ChatPageProps) {
const [sidebarOpen, setSidebarOpen] = useState(true);
const [activeView, setActiveView] = useState<"chat" | "skills-hub" | "mcp-hub">("chat");
const [projectToolsPanelOpen, setProjectToolsPanelOpen] = useState(false);
+ const [tunnelRefreshToken, setTunnelRefreshToken] = useState(0);
const previousProjectToolsFileTreeOpenRef = useRef(false);
const [workspaceEditorMounted, setWorkspaceEditorMounted] = useState(false);
const [workspaceEditorOpen, setWorkspaceEditorOpen] = useState(false);
@@ -717,6 +726,17 @@ export function ChatPage(props: ChatPageProps) {
const [remoteRuntimeStatus, setRemoteRuntimeStatus] = useState(() =>
buildFallbackGatewayStatus(settings.remote),
);
+ const tauriTunnelClient = useMemo(
+ () => ({
+ listTunnels: () => invoke("gateway_tunnel_list"),
+ createTunnel: (input: TunnelCreateInput) =>
+ invoke("gateway_tunnel_create", { input }),
+ updateTunnel: (input: TunnelUpdateInput) =>
+ invoke("gateway_tunnel_update", { input }),
+ closeTunnel: (id: string) => invoke("gateway_tunnel_close", { tunnel_id: id }),
+ }),
+ [],
+ );
const {
historyItems,
@@ -762,6 +782,15 @@ export function ChatPage(props: ChatPageProps) {
remoteRuntimeStatus.online === true &&
remoteRuntimeStatus.enabled === true &&
remoteRuntimeStatus.configured === true;
+ const tunnelManagerToolAvailable =
+ isAgentMode && settings.remote.enableWebTunnels === true && canShareHistory;
+ const tunnelManagerToolDisabledMessage = !isAgentMode
+ ? t("chat.runtime.tunnelAgentModeRequired")
+ : !settings.remote.enableWebTunnels
+ ? t("chat.runtime.tunnelWebDisabled")
+ : !canShareHistory
+ ? t("chat.runtime.tunnelRemoteOffline")
+ : undefined;
const refreshHistoryWorkdirs = useCallback(async () => {
try {
@@ -1088,6 +1117,31 @@ export function ChatPage(props: ChatPageProps) {
[activateWorkspaceProject, checkWorkspaceProjectDirectory, setSettings],
);
+ const openTunnelToolPanel = useCallback((projectPathKey?: string) => {
+ const targetProjectPathKey =
+ workspaceProjectPathKey(projectPathKey) || workspaceProjectPathKey(activeWorkspaceProjectPath);
+ if (!targetProjectPathKey) return;
+ setActiveView("chat");
+ setProjectToolsPanelOpen(true);
+ setSettings((prev) =>
+ updateProjectToolsTunnelOpen(
+ updateCustomSettings(prev, {
+ projectToolsPanel: {
+ ...prev.customSettings.projectToolsPanel,
+ activeTab: "tunnel",
+ },
+ }),
+ targetProjectPathKey,
+ true,
+ ),
+ );
+ }, [activeWorkspaceProjectPath, setSettings]);
+
+ const handleOpenTunnelToolPanel = useCallback(() => {
+ if (!tunnelManagerToolAvailable) return;
+ openTunnelToolPanel();
+ }, [openTunnelToolPanel, tunnelManagerToolAvailable]);
+
const handleBrowseWorkspaceProjectInSystemFileManager = useCallback(
async (project: WorkspaceProject) => {
if (!(await checkWorkspaceProjectDirectory(project))) {
@@ -1478,11 +1532,21 @@ export function ChatPage(props: ChatPageProps) {
settings.customSettings,
terminalProjectPathKey,
);
+ const projectToolsTunnelOpen = isProjectToolsTunnelOpen(
+ settings.customSettings,
+ terminalProjectPathKey,
+ );
const terminalDisabledMessage = !isAgentMode
? "Project tools require Agent project mode."
: !terminalProjectPath
? "Select a project to use project tools."
: undefined;
+ const tunnelEnabled = settings.remote.enableWebTunnels === true && remoteRuntimeStatus.online;
+ const tunnelDisabledMessage = !settings.remote.enableWebTunnels
+ ? t("projectTools.tunnelWebDisabled")
+ : !remoteRuntimeStatus.online
+ ? t("projectTools.tunnelRemoteOffline")
+ : undefined;
const handleOpenWorkspaceFile = useCallback(
(path: string) => {
if (!terminalProjectPath || !terminalProjectPathKey) return;
@@ -2649,7 +2713,14 @@ export function ChatPage(props: ChatPageProps) {
gatewayBridgeEvents.emitError(message, conversationId);
throw new Error(message);
}
- if (runtimeEntry.isSending) return;
+ if (runtimeEntry.isSending) {
+ if (gatewayBridgeRequest) {
+ const message = "Conversation is already sending.";
+ gatewayBridgeEvents.emitError(message, conversationId);
+ gatewayBridgeEvents.close();
+ }
+ return;
+ }
if (isImportingPastedTextRef.current && typeof overrides?.textOverride !== "string") {
return;
}
@@ -2767,7 +2838,14 @@ export function ChatPage(props: ChatPageProps) {
}
const userMessage = createUserMessageWithUploads(text, uploadedFiles, Date.now());
- if (!userMessage) return;
+ if (!userMessage) {
+ if (gatewayBridgeRequest) {
+ const message = "Message is required.";
+ gatewayBridgeEvents.emitError(message, conversationId);
+ gatewayBridgeEvents.close();
+ }
+ return;
+ }
const pendingUserMessage = userMessage;
const content =
typeof pendingUserMessage.content === "string" ? pendingUserMessage.content : "";
@@ -3546,6 +3624,14 @@ export function ChatPage(props: ChatPageProps) {
},
enabledMcpServerIds,
selectableMcpServers,
+ remoteWebTunnelsEnabled: settings.remote.enableWebTunnels,
+ remoteGatewayOnline: canShareHistory,
+ onTunnelsChanged: (change) => {
+ setTunnelRefreshToken((current) => current + 1);
+ if (change.action === "create") {
+ openTunnelToolPanel(change.tunnel.projectPathKey);
+ }
+ },
sessionId,
conversationId,
conversationCwd,
@@ -4441,6 +4527,8 @@ export function ChatPage(props: ChatPageProps) {
chatRuntimeControls={chatRuntimeControlsForCurrentProvider}
reasoningOptions={chatRuntimeReasoningOptions}
gitClient={tauriGitClient}
+ tunnelToolAvailable={tunnelManagerToolAvailable}
+ tunnelToolDisabledMessage={tunnelManagerToolDisabledMessage}
onGitChanged={(gitWorkdir) =>
window.dispatchEvent(
new CustomEvent("liveagent:git-changed", {
@@ -4448,6 +4536,7 @@ export function ChatPage(props: ChatPageProps) {
}),
)
}
+ onOpenTunnelToolPanel={handleOpenTunnelToolPanel}
onSend={handleSend}
onStop={handleStopSending}
onComposerBusyChange={handleComposerBusyChange}
@@ -4590,9 +4679,14 @@ export function ChatPage(props: ChatPageProps) {
terminalProjectPathKey,
)}
gitReviewOpen={isProjectToolsGitReviewOpen(settings.customSettings, terminalProjectPathKey)}
+ tunnelOpen={projectToolsTunnelOpen}
client={tauriTerminalClient}
gitClient={tauriGitClient}
gitWriteEnabled
+ tunnelClient={isAgentMode ? tauriTunnelClient : null}
+ tunnelEnabled={tunnelEnabled}
+ tunnelDisabledMessage={tunnelDisabledMessage}
+ tunnelRefreshToken={tunnelRefreshToken}
onWidthChange={(nextWidth) =>
setSettings((prev) =>
updateCustomSettings(prev, {
@@ -4629,6 +4723,9 @@ export function ChatPage(props: ChatPageProps) {
onGitReviewOpenChange={(open) =>
setSettings((prev) => updateProjectToolsGitReviewOpen(prev, terminalProjectPathKey, open))
}
+ onTunnelOpenChange={(open) =>
+ setSettings((prev) => updateProjectToolsTunnelOpen(prev, terminalProjectPathKey, open))
+ }
onSessionsChange={setProjectTerminalSessions}
onInsertFileMention={(path, kind) => {
composerRef.current?.insertFileMention(path, kind);
diff --git a/crates/agent-gui/src/pages/chat/ChatComposerBar.tsx b/crates/agent-gui/src/pages/chat/ChatComposerBar.tsx
index 75872e50e..2a7966591 100644
--- a/crates/agent-gui/src/pages/chat/ChatComposerBar.tsx
+++ b/crates/agent-gui/src/pages/chat/ChatComposerBar.tsx
@@ -9,6 +9,7 @@ import {
Brain,
Globe2,
Lightbulb,
+ Link2,
Loader2,
Paperclip,
Send,
@@ -77,7 +78,10 @@ export const ChatComposerBar = memo(function ChatComposerBar(props: {
gitClient?: GitClient | null;
gitWriteEnabled?: boolean;
gitDisabledMessage?: string;
+ tunnelToolAvailable?: boolean;
+ tunnelToolDisabledMessage?: string;
onGitChanged?: (workdir: string) => void;
+ onOpenTunnelToolPanel?: () => void;
onSend: () => void;
onStop: () => void;
onComposerBusyChange: (isBusy: boolean) => void;
@@ -101,7 +105,10 @@ export const ChatComposerBar = memo(function ChatComposerBar(props: {
gitClient,
gitWriteEnabled = true,
gitDisabledMessage,
+ tunnelToolAvailable = false,
+ tunnelToolDisabledMessage,
onGitChanged,
+ onOpenTunnelToolPanel,
onSend,
onStop,
onComposerBusyChange,
@@ -129,6 +136,11 @@ export const ChatComposerBar = memo(function ChatComposerBar(props: {
? t("chat.runtime.thinkingUnavailable")
: t("chat.runtime.thinkingTooltip");
const webSearchTooltip = t("chat.runtime.webSearchTooltip");
+ const tunnelTooltip =
+ tunnelToolDisabledMessage ??
+ (tunnelToolAvailable
+ ? t("chat.runtime.tunnelToolAvailable")
+ : t("chat.runtime.tunnelToolUnavailable"));
useEffect(() => {
if (reasoningOptions.length > 0 && reasoningOptions.includes(chatRuntimeControls.reasoning)) {
@@ -302,6 +314,31 @@ export const ChatComposerBar = memo(function ChatComposerBar(props: {
+
+ {
+ if (!tunnelToolAvailable) return;
+ onOpenTunnelToolPanel?.();
+ }}
+ aria-label={
+ tunnelToolAvailable
+ ? t("chat.runtime.tunnelToolAvailable")
+ : tunnelTooltip
+ }
+ className={cn(
+ "composer-toolbar-action inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full outline-hidden transition-colors",
+ "disabled:pointer-events-none disabled:opacity-40",
+ tunnelToolAvailable
+ ? "text-cyan-600 hover:text-cyan-700 dark:text-cyan-300 dark:hover:text-cyan-200"
+ : "text-muted-foreground hover:text-foreground dark:hover:text-white",
+ )}
+ >
+
+
+
+
{reasoningOptions.length > 0 ? (
+
+
+
+
{t("settings.remoteWebTunnels")}
+
+ {t("settings.remoteWebTunnelsHint")}
+
+
+
+ updateRemoteSettings(setSettings, {
+ enableWebTunnels: !settings.remote.enableWebTunnels,
+ })
+ }
+ />
+
diff --git a/crates/agent-gui/test/settings/normalization.test.mjs b/crates/agent-gui/test/settings/normalization.test.mjs
index b65186a33..e077f0b00 100644
--- a/crates/agent-gui/test/settings/normalization.test.mjs
+++ b/crates/agent-gui/test/settings/normalization.test.mjs
@@ -460,6 +460,13 @@ test("gateway settings sync payload redacts provider api keys", () => {
},
customSettings: {
conversationTitleModel: { customProviderId: "provider-1", model: "gpt-5" },
+ projectToolsPanel: {
+ width: 612,
+ activeTab: "tunnel",
+ tabOrders: {
+ "/workspace/a": ["__tunnel__", "__file_tree__"],
+ },
+ },
projectToolsFileTree: {
openProjectPathKeys: ["/workspace/b", " ", "/workspace/a", "/workspace/a"],
projects: {
@@ -475,6 +482,10 @@ test("gateway settings sync payload redacts provider api keys", () => {
openProjectPathKeys: ["/workspace/b", "/workspace/a", "/workspace/a"],
openVersion: 2,
},
+ projectToolsTunnel: {
+ openProjectPathKeys: ["/workspace/b", "/workspace/a", "/workspace/a"],
+ openVersion: 3,
+ },
},
});
@@ -507,7 +518,15 @@ test("gateway settings sync payload redacts provider api keys", () => {
openProjectPathKeys: ["/workspace/a", "/workspace/b"],
openVersion: 2,
});
- assert.equal(Object.hasOwn(payload.customSettings, "projectToolsPanel"), false);
+ assert.deepEqual(payload.customSettings.projectToolsTunnel, {
+ openProjectPathKeys: ["/workspace/a", "/workspace/b"],
+ openVersion: 3,
+ });
+ assert.deepEqual(payload.customSettings.projectToolsPanel, {
+ activeTab: "tunnel",
+ });
+ assert.equal(Object.hasOwn(payload.customSettings.projectToolsPanel, "width"), false);
+ assert.equal(Object.hasOwn(payload.customSettings.projectToolsPanel, "tabOrders"), false);
assert.deepEqual(payload.chatRuntimeControls, appSettings.chatRuntimeControls);
assert.equal(payload.providerApiKeyUpdates, undefined);
@@ -723,6 +742,10 @@ test("settings reload preserves session-only project tools state", () => {
openProjectPathKeys: ["/workspace/app"],
openVersion: 5,
},
+ projectToolsTunnel: {
+ openProjectPathKeys: ["/workspace/app"],
+ openVersion: 6,
+ },
},
});
const reloaded = settings.normalizeSettings({
@@ -737,6 +760,7 @@ test("settings reload preserves session-only project tools state", () => {
},
projectToolsFileTree: {},
projectToolsGitReview: {},
+ projectToolsTunnel: {},
},
});
@@ -762,6 +786,10 @@ test("settings reload preserves session-only project tools state", () => {
"/workspace/app",
]);
assert.equal(merged.customSettings.projectToolsGitReview.openVersion, 5);
+ assert.deepEqual(merged.customSettings.projectToolsTunnel.openProjectPathKeys, [
+ "/workspace/app",
+ ]);
+ assert.equal(merged.customSettings.projectToolsTunnel.openVersion, 6);
const loadedWithProjectTools = settings.normalizeSettings({
customSettings: {
@@ -773,6 +801,10 @@ test("settings reload preserves session-only project tools state", () => {
openProjectPathKeys: ["/loaded/project"],
openVersion: 1,
},
+ projectToolsTunnel: {
+ openProjectPathKeys: ["/loaded/project"],
+ openVersion: 1,
+ },
},
});
const emptyCurrent = settings.normalizeSettings({});
@@ -787,6 +819,9 @@ test("settings reload preserves session-only project tools state", () => {
assert.deepEqual(loadedOnly.customSettings.projectToolsGitReview.openProjectPathKeys, [
"/loaded/project",
]);
+ assert.deepEqual(loadedOnly.customSettings.projectToolsTunnel.openProjectPathKeys, [
+ "/loaded/project",
+ ]);
});
test("removes project tools state when a workspace project is deleted", () => {
@@ -823,6 +858,10 @@ test("removes project tools state when a workspace project is deleted", () => {
openProjectPathKeys: ["/workspace/app", "/workspace/other"],
openVersion: 5,
},
+ projectToolsTunnel: {
+ openProjectPathKeys: ["/workspace/app", "/workspace/other"],
+ openVersion: 6,
+ },
},
});
@@ -842,6 +881,10 @@ test("removes project tools state when a workspace project is deleted", () => {
"/workspace/other",
]);
assert.equal(cleaned.customSettings.projectToolsGitReview.openVersion, 6);
+ assert.deepEqual(cleaned.customSettings.projectToolsTunnel.openProjectPathKeys, [
+ "/workspace/other",
+ ]);
+ assert.equal(cleaned.customSettings.projectToolsTunnel.openVersion, 7);
assert.equal(settings.removeProjectToolsProjectState(cleaned, "/workspace/app"), cleaned);
const projectOnlyState = settings.normalizeSettings({
@@ -880,9 +923,24 @@ test("removes project tools state when a workspace project is deleted", () => {
);
assert.equal(gitReviewOnlyCleaned.customSettings.projectToolsGitReview.openVersion, 10);
assert.deepEqual(gitReviewOnlyCleaned.customSettings.projectToolsGitReview.openProjectPathKeys, []);
+
+ const tunnelOnlyState = settings.normalizeSettings({
+ customSettings: {
+ projectToolsTunnel: {
+ openProjectPathKeys: ["/workspace/app"],
+ openVersion: 11,
+ },
+ },
+ });
+ const tunnelOnlyCleaned = settings.removeProjectToolsProjectState(
+ tunnelOnlyState,
+ "/workspace/app",
+ );
+ assert.equal(tunnelOnlyCleaned.customSettings.projectToolsTunnel.openVersion, 12);
+ assert.deepEqual(tunnelOnlyCleaned.customSettings.projectToolsTunnel.openProjectPathKeys, []);
});
-test("gateway settings sync keeps project tools panel state local", () => {
+test("gateway settings sync keeps project tools panel layout local and syncs active tab", () => {
const current = settings.normalizeSettings({
customSettings: {
projectToolsPanel: {
@@ -908,6 +966,10 @@ test("gateway settings sync keeps project tools panel state local", () => {
openProjectPathKeys: ["/desktop/project"],
openVersion: 1,
},
+ projectToolsTunnel: {
+ openProjectPathKeys: ["/desktop/project"],
+ openVersion: 1,
+ },
},
});
const incoming = settings.normalizeSettings({
@@ -935,25 +997,24 @@ test("gateway settings sync keeps project tools panel state local", () => {
openProjectPathKeys: ["/web/project"],
openVersion: 2,
},
+ projectToolsTunnel: {
+ openProjectPathKeys: ["/web/project"],
+ openVersion: 2,
+ },
},
});
const payload = sync.buildGatewaySettingsSyncPayload(incoming);
- assert.equal(Object.hasOwn(payload.customSettings, "projectToolsPanel"), false);
-
- const synced = sync.applyGatewaySettingsSyncPayload(current, {
- ...payload,
- customSettings: {
- ...payload.customSettings,
- projectToolsPanel: {
- width: 360,
- activeTab: "fileTree",
- },
- },
+ assert.deepEqual(payload.customSettings.projectToolsPanel, {
+ activeTab: "fileTree",
});
+ assert.equal(Object.hasOwn(payload.customSettings.projectToolsPanel, "width"), false);
+ assert.equal(Object.hasOwn(payload.customSettings.projectToolsPanel, "tabOrders"), false);
+
+ const synced = sync.applyGatewaySettingsSyncPayload(current, payload);
assert.equal(synced.customSettings.projectToolsPanel.width, 612);
- assert.equal(synced.customSettings.projectToolsPanel.activeTab, "terminal");
+ assert.equal(synced.customSettings.projectToolsPanel.activeTab, "fileTree");
assert.deepEqual(synced.customSettings.projectToolsPanel.tabOrders, {
"/desktop/project": ["desktop-terminal", "__file_tree__"],
});
@@ -971,10 +1032,16 @@ test("gateway settings sync keeps project tools panel state local", () => {
"/web/project",
]);
assert.equal(synced.customSettings.projectToolsGitReview.openVersion, 2);
+ assert.deepEqual(synced.customSettings.projectToolsTunnel.openProjectPathKeys, [
+ "/web/project",
+ ]);
+ assert.equal(synced.customSettings.projectToolsTunnel.openVersion, 2);
const {
projectToolsFileTree: _projectToolsFileTree,
projectToolsGitReview: _projectToolsGitReview,
+ projectToolsTunnel: _projectToolsTunnel,
+ projectToolsPanel: _projectToolsPanel,
...legacyCustomSettings
} = payload.customSettings;
const legacySynced = sync.applyGatewaySettingsSyncPayload(current, {
@@ -984,6 +1051,7 @@ test("gateway settings sync keeps project tools panel state local", () => {
assert.deepEqual(legacySynced.customSettings.projectToolsFileTree.openProjectPathKeys, [
"/desktop/project",
]);
+ assert.equal(legacySynced.customSettings.projectToolsPanel.activeTab, "terminal");
assert.deepEqual(legacySynced.customSettings.projectToolsFileTree.projects["/desktop/project"], {
query: "desktop",
selectedPath: "desktop.ts",
@@ -995,6 +1063,10 @@ test("gateway settings sync keeps project tools panel state local", () => {
"/desktop/project",
]);
assert.equal(legacySynced.customSettings.projectToolsGitReview.openVersion, 1);
+ assert.deepEqual(legacySynced.customSettings.projectToolsTunnel.openProjectPathKeys, [
+ "/desktop/project",
+ ]);
+ assert.equal(legacySynced.customSettings.projectToolsTunnel.openVersion, 1);
});
test("gateway settings sync ignores stale project file tree UI snapshots", () => {
@@ -1172,6 +1244,65 @@ test("gateway settings sync ignores stale project git review open snapshots", ()
assert.equal(deletedProjectSynced.customSettings.projectToolsGitReview.openVersion, 5);
});
+test("gateway settings sync ignores stale project tunnel open snapshots", () => {
+ const current = settings.normalizeSettings({
+ customSettings: {
+ projectToolsTunnel: {
+ openProjectPathKeys: ["/workspace/app"],
+ openVersion: 2,
+ },
+ },
+ });
+
+ const staleSynced = sync.applyGatewaySettingsSyncPayload(current, {
+ customSettings: {
+ projectToolsTunnel: {
+ openProjectPathKeys: [],
+ openVersion: 1,
+ },
+ },
+ });
+
+ assert.deepEqual(staleSynced.customSettings.projectToolsTunnel.openProjectPathKeys, [
+ "/workspace/app",
+ ]);
+ assert.equal(staleSynced.customSettings.projectToolsTunnel.openVersion, 2);
+
+ const newerSynced = sync.applyGatewaySettingsSyncPayload(staleSynced, {
+ customSettings: {
+ projectToolsTunnel: {
+ openProjectPathKeys: [],
+ openVersion: 3,
+ },
+ },
+ });
+
+ assert.deepEqual(newerSynced.customSettings.projectToolsTunnel.openProjectPathKeys, []);
+ assert.equal(newerSynced.customSettings.projectToolsTunnel.openVersion, 3);
+
+ const deletedProjectLocal = settings.removeProjectToolsProjectState(
+ settings.normalizeSettings({
+ customSettings: {
+ projectToolsTunnel: {
+ openProjectPathKeys: ["/workspace/deleted"],
+ openVersion: 4,
+ },
+ },
+ }),
+ "/workspace/deleted",
+ );
+ const deletedProjectSynced = sync.applyGatewaySettingsSyncPayload(deletedProjectLocal, {
+ customSettings: {
+ projectToolsTunnel: {
+ openProjectPathKeys: ["/workspace/deleted"],
+ openVersion: 4,
+ },
+ },
+ });
+ assert.deepEqual(deletedProjectSynced.customSettings.projectToolsTunnel.openProjectPathKeys, []);
+ assert.equal(deletedProjectSynced.customSettings.projectToolsTunnel.openVersion, 5);
+});
+
test("gateway settings sync keeps newer project conversation activity", () => {
const current = settings.normalizeSettings({
system: {
diff --git a/crates/agent-gui/test/tools/tunnel-manager-tools.test.mjs b/crates/agent-gui/test/tools/tunnel-manager-tools.test.mjs
new file mode 100644
index 000000000..78ef1b29d
--- /dev/null
+++ b/crates/agent-gui/test/tools/tunnel-manager-tools.test.mjs
@@ -0,0 +1,209 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+import { createTsModuleLoader } from "../helpers/load-ts-module.mjs";
+
+function createTunnel(overrides = {}) {
+ return {
+ id: "tun-1",
+ slug: "abc123",
+ name: "Local app",
+ targetUrl: "http://localhost:3000",
+ publicUrl: "https://gateway.example.test/t/abc123",
+ createdAt: 1_700_000_000,
+ expiresAt: Math.floor(Date.now() / 1000) + 3600,
+ activeConnections: 0,
+ status: "active",
+ ...overrides,
+ };
+}
+
+function createToolCall(args) {
+ return {
+ type: "toolCall",
+ id: "call-tunnel",
+ name: "TunnelManager",
+ arguments: args,
+ };
+}
+
+async function buildRegistry(params = {}) {
+ const loader = createTsModuleLoader();
+ const { buildBuiltinToolRegistry } = loader.loadModule("src/lib/tools/builtinRegistry.ts");
+ const { createFileToolState } = loader.loadModule("src/lib/tools/fileToolState.ts");
+ return buildBuiltinToolRegistry({
+ workdir: "/workspace",
+ providerId: "codex",
+ fileState: createFileToolState(),
+ skillsEnabled: false,
+ runtimeScope: "chat",
+ currentChatModel: { customProviderId: "p", model: "m" },
+ selectedSystemToolIds: [],
+ mcpSettings: { selected: [], servers: [] },
+ enabledMcpServerIds: [],
+ selectableMcpServers: [],
+ ...params,
+ });
+}
+
+test("TunnelManager is injected only when Remote Web Tunnels are enabled and gateway is online", async () => {
+ const disabledRegistry = await buildRegistry({
+ remoteWebTunnelsEnabled: false,
+ remoteGatewayOnline: true,
+ });
+ assert.equal(disabledRegistry.hasTool("TunnelManager"), false);
+
+ const offlineRegistry = await buildRegistry({
+ remoteWebTunnelsEnabled: true,
+ remoteGatewayOnline: false,
+ });
+ assert.equal(offlineRegistry.hasTool("TunnelManager"), false);
+
+ const enabledRegistry = await buildRegistry({
+ remoteWebTunnelsEnabled: true,
+ remoteGatewayOnline: true,
+ });
+ assert.equal(enabledRegistry.hasTool("TunnelManager"), true);
+ assert.equal(
+ enabledRegistry.metadataByName.get("TunnelManager").kind,
+ "tunnel_manager",
+ );
+
+ const cronRegistry = await buildRegistry({
+ runtimeScope: "cron_auto_prompt",
+ remoteWebTunnelsEnabled: true,
+ remoteGatewayOnline: true,
+ });
+ assert.equal(cronRegistry.hasTool("TunnelManager"), false);
+});
+
+test("TunnelManager list/create/close call gateway tunnel commands", async () => {
+ const invocations = [];
+ const loader = createTsModuleLoader({
+ mocks: {
+ "@tauri-apps/api/core": {
+ async invoke(command, args) {
+ invocations.push({ command, args });
+ if (command === "gateway_tunnel_list") {
+ return [createTunnel()];
+ }
+ if (command === "gateway_tunnel_create") {
+ return createTunnel({
+ id: "tun-created",
+ slug: "created",
+ targetUrl: args.input.targetUrl,
+ name: args.input.name ?? "",
+ ...(args.input.ttlSeconds === 0 ? { expiresAt: 0 } : {}),
+ });
+ }
+ if (command === "gateway_tunnel_close") {
+ return createTunnel({
+ id: args.tunnel_id,
+ status: "expired",
+ });
+ }
+ throw new Error(`unexpected invoke ${command}`);
+ },
+ },
+ },
+ });
+ const { createTunnelManagerTools } = loader.loadModule("src/lib/tools/tunnelManagerTools.ts");
+ const changes = [];
+ const bundle = createTunnelManagerTools({
+ enabled: true,
+ runtimeScope: "chat",
+ projectPathKey: "project:/workspace",
+ onTunnelsChanged: (change) => changes.push(change),
+ });
+
+ assert.deepEqual(bundle.tools.map((tool) => tool.name), ["TunnelManager"]);
+
+ const listResult = await bundle.executeToolCall(createToolCall({ action: "list" }));
+ assert.equal(listResult.isError, false);
+ assert.equal(listResult.details.kind, "tunnel_manager");
+ assert.equal(listResult.details.tunnels.length, 1);
+ assert.equal(listResult.details.tunnels[0].activeConnections, undefined);
+ assert.doesNotMatch(listResult.content[0].text, /activeConnections|connections/i);
+
+ const createResult = await bundle.executeToolCall(
+ createToolCall({
+ action: "create",
+ targetUrl: "http://localhost:5173/app",
+ name: "Vite",
+ ttlSeconds: 0,
+ }),
+ );
+ assert.equal(createResult.isError, false);
+ assert.equal(createResult.details.tunnel.id, "tun-created");
+ assert.equal(createResult.details.tunnel.activeConnections, undefined);
+ assert.doesNotMatch(createResult.content[0].text, /activeConnections|connections/i);
+ assert.match(createResult.content[0].text, /unlimited/);
+
+ const closeBySlugResult = await bundle.executeToolCall(
+ createToolCall({ action: "close", slug: "abc123" }),
+ );
+ assert.equal(closeBySlugResult.isError, false);
+ assert.equal(closeBySlugResult.details.tunnel.activeConnections, undefined);
+
+ assert.deepEqual(
+ invocations.map((call) => [call.command, call.args]),
+ [
+ ["gateway_tunnel_list", undefined],
+ [
+ "gateway_tunnel_create",
+ {
+ input: {
+ targetUrl: "http://localhost:5173/app",
+ name: "Vite",
+ ttlSeconds: 0,
+ projectPathKey: "project:/workspace",
+ },
+ },
+ ],
+ ["gateway_tunnel_list", undefined],
+ ["gateway_tunnel_close", { tunnel_id: "tun-1" }],
+ ],
+ );
+ assert.deepEqual(
+ changes.map((change) => [change.action, change.tunnel.id, change.tunnel.activeConnections]),
+ [
+ ["create", "tun-created", undefined],
+ ["close", "tun-1", undefined],
+ ],
+ );
+});
+
+test("TunnelManager rejects invalid arguments before invoking gateway commands", async () => {
+ const invocations = [];
+ const loader = createTsModuleLoader({
+ mocks: {
+ "@tauri-apps/api/core": {
+ async invoke(command, args) {
+ invocations.push({ command, args });
+ throw new Error("unexpected invoke");
+ },
+ },
+ },
+ });
+ const { createTunnelManagerTools } = loader.loadModule("src/lib/tools/tunnelManagerTools.ts");
+ const bundle = createTunnelManagerTools({ enabled: true, runtimeScope: "chat" });
+
+ const invalidAction = await bundle.executeToolCall(createToolCall({ action: "delete" }));
+ assert.equal(invalidAction.isError, true);
+ assert.match(invalidAction.content[0].text, /action/);
+
+ const missingTarget = await bundle.executeToolCall(createToolCall({ action: "create" }));
+ assert.equal(missingTarget.isError, true);
+ assert.match(missingTarget.content[0].text, /targetUrl/);
+
+ const invalidTtl = await bundle.executeToolCall(
+ createToolCall({ action: "create", targetUrl: "http://localhost:3000", ttlSeconds: 60 }),
+ );
+ assert.equal(invalidTtl.isError, true);
+ assert.match(invalidTtl.content[0].text, /ttlSeconds/);
+
+ const missingCloseTarget = await bundle.executeToolCall(createToolCall({ action: "close" }));
+ assert.equal(missingCloseTarget.isError, true);
+ assert.match(missingCloseTarget.content[0].text, /id or TunnelManager.slug/);
+
+ assert.deepEqual(invocations, []);
+});
From 578c451362ae2c41b2388f737aa03a38660f28f9 Mon Sep 17 00:00:00 2001
From: su-fen <715041@qq.com>
Date: Mon, 8 Jun 2026 19:04:41 +0800
Subject: [PATCH 2/9] feat(tunnel): add local project tunnel support
---
.../test/webui/web-settings.test.mjs | 23 +++-
crates/agent-gateway/web/src/App.tsx | 28 ++---
.../web/src/lib/settings/index.ts | 101 ++++++++++++++++--
.../web/src/lib/settings/storage.ts | 13 ++-
.../web/src/lib/settings/sync.ts | 31 +++---
crates/agent-gui/src/lib/settings/index.ts | 101 ++++++++++++++++--
crates/agent-gui/src/lib/settings/storage.ts | 13 ++-
crates/agent-gui/src/lib/settings/sync.ts | 31 +++---
crates/agent-gui/src/pages/ChatPage.tsx | 28 ++---
.../test/settings/normalization.test.mjs | 69 +++++++++++-
10 files changed, 339 insertions(+), 99 deletions(-)
diff --git a/crates/agent-gateway/test/webui/web-settings.test.mjs b/crates/agent-gateway/test/webui/web-settings.test.mjs
index 0532fa4bd..192ec13c0 100644
--- a/crates/agent-gateway/test/webui/web-settings.test.mjs
+++ b/crates/agent-gateway/test/webui/web-settings.test.mjs
@@ -351,6 +351,9 @@ test("gateway settings sync keeps newer project tool tab open state", () => {
projectToolsPanel: {
width: 612,
activeTab: "gitReview",
+ activeTabs: {
+ "/web/project": "gitReview",
+ },
tabOrders: {
"/web/project": ["__git_review__", "__file_tree__"],
},
@@ -394,7 +397,10 @@ test("gateway settings sync keeps newer project tool tab open state", () => {
]);
assert.equal(staleSynced.customSettings.projectToolsTunnel.openVersion, 2);
assert.equal(staleSynced.customSettings.projectToolsPanel.width, 612);
- assert.equal(staleSynced.customSettings.projectToolsPanel.activeTab, "terminal");
+ assert.equal(staleSynced.customSettings.projectToolsPanel.activeTab, "gitReview");
+ assert.deepEqual(staleSynced.customSettings.projectToolsPanel.activeTabs, {
+ "/web/project": "gitReview",
+ });
assert.deepEqual(staleSynced.customSettings.projectToolsPanel.tabOrders, {
"/web/project": ["__git_review__", "__file_tree__"],
});
@@ -404,6 +410,9 @@ test("gateway settings sync keeps newer project tool tab open state", () => {
projectToolsPanel: {
width: 360,
activeTab: "tunnel",
+ activeTabs: {
+ "/desktop/project": "tunnel",
+ },
tabOrders: {
"/desktop/project": ["terminal-1", "__tunnel__"],
},
@@ -427,15 +436,23 @@ test("gateway settings sync keeps newer project tool tab open state", () => {
]);
assert.equal(newerSynced.customSettings.projectToolsTunnel.openVersion, 3);
assert.equal(newerSynced.customSettings.projectToolsPanel.width, 612);
- assert.equal(newerSynced.customSettings.projectToolsPanel.activeTab, "tunnel");
+ assert.equal(newerSynced.customSettings.projectToolsPanel.activeTab, "gitReview");
+ assert.deepEqual(newerSynced.customSettings.projectToolsPanel.activeTabs, {
+ "/web/project": "gitReview",
+ "/desktop/project": "tunnel",
+ });
assert.deepEqual(newerSynced.customSettings.projectToolsPanel.tabOrders, {
"/web/project": ["__git_review__", "__file_tree__"],
});
const payload = settingsSync.buildGatewaySettingsSyncPayload(newerSynced);
assert.deepEqual(payload.customSettings.projectToolsPanel, {
- activeTab: "tunnel",
+ activeTabs: {
+ "/web/project": "gitReview",
+ "/desktop/project": "tunnel",
+ },
});
+ assert.equal(Object.hasOwn(payload.customSettings.projectToolsPanel, "activeTab"), false);
assert.equal(Object.hasOwn(payload.customSettings.projectToolsPanel, "width"), false);
assert.equal(Object.hasOwn(payload.customSettings.projectToolsPanel, "tabOrders"), false);
assert.deepEqual(payload.customSettings.projectToolsGitReview, {
diff --git a/crates/agent-gateway/web/src/App.tsx b/crates/agent-gateway/web/src/App.tsx
index 1c9e2ae2e..771eb2786 100644
--- a/crates/agent-gateway/web/src/App.tsx
+++ b/crates/agent-gateway/web/src/App.tsx
@@ -65,6 +65,7 @@ import {
findProviderModelConfig,
getChatRuntimeReasoningLevelsForProvider,
getProjectToolsFileTreeProjectState,
+ getProjectToolsPanelActiveTab,
getProjectToolsPanelTabOrder,
isAgentDevMode,
isProjectToolsFileTreeOpen,
@@ -81,6 +82,7 @@ import {
updateProjectToolsFileTreeOpen,
updateProjectToolsGitReviewOpen,
updateProjectToolsTunnelOpen,
+ updateProjectToolsPanelActiveTab,
updateProjectToolsPanelTabOrder,
type AppSettings,
type ChatRuntimeControls,
@@ -2080,12 +2082,7 @@ export default function App() {
setProjectToolsPanelOpen(true);
setSettings((prev) =>
updateProjectToolsTunnelOpen(
- updateCustomSettings(prev, {
- projectToolsPanel: {
- ...prev.customSettings.projectToolsPanel,
- activeTab: "tunnel",
- },
- }),
+ updateProjectToolsPanelActiveTab(prev, targetProjectPathKey, "tunnel"),
targetProjectPathKey,
true,
),
@@ -2319,12 +2316,7 @@ export default function App() {
activateWorkspaceProject(project);
setSettings((prev) =>
updateProjectToolsFileTreeOpen(
- updateCustomSettings(prev, {
- projectToolsPanel: {
- ...prev.customSettings.projectToolsPanel,
- activeTab: "fileTree",
- },
- }),
+ updateProjectToolsPanelActiveTab(prev, pathKey, "fileTree"),
pathKey,
true,
),
@@ -6763,7 +6755,10 @@ export default function App() {
theme={settings.theme}
disabledMessage={projectToolsDisabledMessage}
terminalDisabledMessage={terminalDisabledMessage}
- activeTab={settings.customSettings.projectToolsPanel.activeTab}
+ activeTab={getProjectToolsPanelActiveTab(
+ settings.customSettings,
+ terminalProjectPathKey,
+ )}
tabOrder={getProjectToolsPanelTabOrder(settings.customSettings, terminalProjectPathKey)}
fileTreeOpen={projectToolsFileTreeOpen}
fileTreeState={getProjectToolsFileTreeProjectState(
@@ -6795,12 +6790,7 @@ export default function App() {
}
onActiveTabChange={(activeTab) =>
setSettings((prev) =>
- updateCustomSettings(prev, {
- projectToolsPanel: {
- ...prev.customSettings.projectToolsPanel,
- activeTab,
- },
- }),
+ updateProjectToolsPanelActiveTab(prev, terminalProjectPathKey, activeTab),
)
}
onTabOrderChange={(tabOrder) =>
diff --git a/crates/agent-gateway/web/src/lib/settings/index.ts b/crates/agent-gateway/web/src/lib/settings/index.ts
index 1242a2b3e..9ec5e88e3 100644
--- a/crates/agent-gateway/web/src/lib/settings/index.ts
+++ b/crates/agent-gateway/web/src/lib/settings/index.ts
@@ -147,6 +147,7 @@ export type ProjectToolsPanelTab = "terminal" | "fileTree" | "gitReview" | "tunn
export type ProjectToolsPanelSettings = {
width: number;
activeTab: ProjectToolsPanelTab;
+ activeTabs: Record;
tabOrders: Record;
};
@@ -1585,6 +1586,33 @@ export function normalizeProjectToolsPanelTabOrder(input: unknown): string[] {
return order;
}
+function isProjectToolsPanelTab(input: unknown): input is ProjectToolsPanelTab {
+ return input === "terminal" ||
+ input === "fileTree" ||
+ input === "gitReview" ||
+ input === "tunnel";
+}
+
+export function normalizeProjectToolsPanelActiveTab(input: unknown): ProjectToolsPanelTab {
+ return isProjectToolsPanelTab(input) ? input : "fileTree";
+}
+
+export function normalizeProjectToolsPanelActiveTabs(
+ input: unknown,
+): Record {
+ const rawTabs = (
+ input && typeof input === "object" && !Array.isArray(input) ? input : {}
+ ) as Record;
+ const activeTabs: Record = {};
+ for (const [pathKey, value] of Object.entries(rawTabs)) {
+ const normalizedPathKey = workspaceProjectPathKey(pathKey);
+ if (!normalizedPathKey || !isProjectToolsPanelTab(value)) continue;
+ activeTabs[normalizedPathKey] = value;
+ if (Object.keys(activeTabs).length >= 100) break;
+ }
+ return activeTabs;
+}
+
export function normalizeProjectToolsPanelTabOrders(input: unknown): Record {
const rawOrders = (
input && typeof input === "object" && !Array.isArray(input) ? input : {}
@@ -1615,13 +1643,9 @@ export function normalizeCustomSettings(
const projectToolsPanel = (obj.projectToolsPanel && typeof obj.projectToolsPanel === "object"
? obj.projectToolsPanel
: {}) as Record;
- const projectToolsPanelActiveTab =
- projectToolsPanel.activeTab === "terminal" ||
- projectToolsPanel.activeTab === "fileTree" ||
- projectToolsPanel.activeTab === "gitReview" ||
- projectToolsPanel.activeTab === "tunnel"
- ? projectToolsPanel.activeTab
- : "fileTree";
+ const projectToolsPanelActiveTab = normalizeProjectToolsPanelActiveTab(
+ projectToolsPanel.activeTab,
+ );
const projectToolsFileTree = (
obj.projectToolsFileTree && typeof obj.projectToolsFileTree === "object"
? obj.projectToolsFileTree
@@ -1654,6 +1678,7 @@ export function normalizeCustomSettings(
420,
),
activeTab: projectToolsPanelActiveTab,
+ activeTabs: normalizeProjectToolsPanelActiveTabs(projectToolsPanel.activeTabs),
tabOrders: normalizeProjectToolsPanelTabOrders(projectToolsPanel.tabOrders),
},
projectToolsFileTree: normalizeProjectToolsFileTreeSettings(projectToolsFileTree),
@@ -1884,6 +1909,56 @@ export function getProjectToolsPanelTabOrder(
return customSettings.projectToolsPanel.tabOrders[normalizedPathKey] ?? [];
}
+export function getProjectToolsPanelActiveTab(
+ customSettings: CustomSettings,
+ projectPathKey: string,
+): ProjectToolsPanelTab {
+ const normalizedPathKey = workspaceProjectPathKey(projectPathKey);
+ if (!normalizedPathKey) return customSettings.projectToolsPanel.activeTab;
+ return (
+ customSettings.projectToolsPanel.activeTabs[normalizedPathKey] ??
+ customSettings.projectToolsPanel.activeTab
+ );
+}
+
+export function updateProjectToolsPanelActiveTab(
+ prev: AppSettings,
+ projectPathKey: string,
+ activeTab: ProjectToolsPanelTab,
+): AppSettings {
+ const nextActiveTab = normalizeProjectToolsPanelActiveTab(activeTab);
+ const normalizedPathKey = workspaceProjectPathKey(projectPathKey);
+ if (!normalizedPathKey) {
+ if (prev.customSettings.projectToolsPanel.activeTab === nextActiveTab) return prev;
+ return updateCustomSettings(prev, {
+ projectToolsPanel: {
+ ...prev.customSettings.projectToolsPanel,
+ activeTab: nextActiveTab,
+ },
+ });
+ }
+
+ const currentProjectActiveTab =
+ prev.customSettings.projectToolsPanel.activeTabs[normalizedPathKey];
+ if (
+ prev.customSettings.projectToolsPanel.activeTab === nextActiveTab &&
+ currentProjectActiveTab === nextActiveTab
+ ) {
+ return prev;
+ }
+
+ return updateCustomSettings(prev, {
+ projectToolsPanel: {
+ ...prev.customSettings.projectToolsPanel,
+ activeTab: nextActiveTab,
+ activeTabs: {
+ ...prev.customSettings.projectToolsPanel.activeTabs,
+ [normalizedPathKey]: nextActiveTab,
+ },
+ },
+ });
+}
+
function projectToolsPanelTabOrderEqual(left: readonly string[], right: readonly string[]) {
return left.length === right.length && left.every((item, index) => item === right[index]);
}
@@ -1925,6 +2000,10 @@ export function removeProjectToolsProjectState(
prev.customSettings.projectToolsPanel.tabOrders,
normalizedPathKey,
);
+ const hasActiveTab = Object.prototype.hasOwnProperty.call(
+ prev.customSettings.projectToolsPanel.activeTabs,
+ normalizedPathKey,
+ );
const openProjectPathKeys = prev.customSettings.projectToolsFileTree.openProjectPathKeys
.map((pathKey) => workspaceProjectPathKey(pathKey))
.filter(Boolean);
@@ -1957,6 +2036,7 @@ export function removeProjectToolsProjectState(
if (
!hasTabOrder &&
+ !hasActiveTab &&
!removedOpenProjectPathKey &&
!removedGitReviewOpenProjectPathKey &&
!removedTunnelOpenProjectPathKey &&
@@ -1971,6 +2051,12 @@ export function removeProjectToolsProjectState(
if (hasTabOrder) {
delete tabOrders[normalizedPathKey];
}
+ const activeTabs = hasActiveTab
+ ? { ...prev.customSettings.projectToolsPanel.activeTabs }
+ : prev.customSettings.projectToolsPanel.activeTabs;
+ if (hasActiveTab) {
+ delete activeTabs[normalizedPathKey];
+ }
const projects = hasFileTreeProjectState
? { ...prev.customSettings.projectToolsFileTree.projects }
@@ -1982,6 +2068,7 @@ export function removeProjectToolsProjectState(
return updateCustomSettings(prev, {
projectToolsPanel: {
...prev.customSettings.projectToolsPanel,
+ activeTabs,
tabOrders,
},
projectToolsFileTree: {
diff --git a/crates/agent-gateway/web/src/lib/settings/storage.ts b/crates/agent-gateway/web/src/lib/settings/storage.ts
index 3f1899501..b87d00929 100644
--- a/crates/agent-gateway/web/src/lib/settings/storage.ts
+++ b/crates/agent-gateway/web/src/lib/settings/storage.ts
@@ -9,6 +9,8 @@ import {
normalizeProjectToolsFileTreeSettings,
normalizeProjectToolsGitReviewSettings,
normalizeProjectToolsTunnelSettings,
+ normalizeProjectToolsPanelActiveTab,
+ normalizeProjectToolsPanelActiveTabs,
normalizeProjectToolsPanelTabOrders,
type ChatRuntimeControls,
normalizeSkillsSettings,
@@ -91,13 +93,9 @@ function readLocalUiSettings(): {
typeof legacyTerminalPanel.width === "string"
? Number(legacyTerminalPanel.width)
: 420;
- const projectToolsPanelActiveTab =
- projectToolsPanel.activeTab === "terminal" ||
- projectToolsPanel.activeTab === "fileTree" ||
- projectToolsPanel.activeTab === "gitReview" ||
- projectToolsPanel.activeTab === "tunnel"
- ? projectToolsPanel.activeTab
- : "fileTree";
+ const projectToolsPanelActiveTab = normalizeProjectToolsPanelActiveTab(
+ projectToolsPanel.activeTab,
+ );
return toPersistedLocalCustomSettings({
conversationTitleModel: normalizeSelectedModel(obj.conversationTitleModel),
chatSidebar: {
@@ -109,6 +107,7 @@ function readLocalUiSettings(): {
? Math.min(1280, Math.max(320, Math.floor(projectToolsPanelWidth)))
: 420,
activeTab: projectToolsPanelActiveTab,
+ activeTabs: normalizeProjectToolsPanelActiveTabs(projectToolsPanel.activeTabs),
tabOrders: normalizeProjectToolsPanelTabOrders(projectToolsPanel.tabOrders),
},
projectToolsFileTree: normalizeProjectToolsFileTreeSettings({}),
diff --git a/crates/agent-gateway/web/src/lib/settings/sync.ts b/crates/agent-gateway/web/src/lib/settings/sync.ts
index a7529a653..74dfdcef8 100644
--- a/crates/agent-gateway/web/src/lib/settings/sync.ts
+++ b/crates/agent-gateway/web/src/lib/settings/sync.ts
@@ -2,9 +2,9 @@ import {
normalizeChatRuntimeControls,
normalizeProjectToolsFileTreeSettings,
normalizeProjectToolsGitReviewSettings,
+ normalizeProjectToolsPanelActiveTabs,
normalizeProjectToolsTunnelSettings,
normalizeSettings,
- type ProjectToolsPanelTab,
workspaceProjectPathKey,
type AppSettings,
} from "./index";
@@ -18,7 +18,7 @@ export type GatewaySettingsSyncProvider = Omit<
};
export type GatewayProjectToolsPanelSync = Pick<
AppSettings["customSettings"]["projectToolsPanel"],
- "activeTab"
+ "activeTabs"
>;
export type GatewaySettingsSyncCustomSettings = Omit<
Partial,
@@ -107,7 +107,7 @@ function syncableCustomSettings(
return {
...syncable,
projectToolsPanel: {
- activeTab: customSettings.projectToolsPanel.activeTab,
+ activeTabs: customSettings.projectToolsPanel.activeTabs,
},
chatSidebar: {
projectsCollapsed: false,
@@ -365,12 +365,15 @@ function mergeSyncedProjectToolsTunnelSettings(
};
}
-function isProjectToolsPanelTab(value: unknown): value is ProjectToolsPanelTab {
+function projectToolsPanelActiveTabsEqual(
+ left: AppSettings["customSettings"]["projectToolsPanel"]["activeTabs"],
+ right: AppSettings["customSettings"]["projectToolsPanel"]["activeTabs"],
+) {
+ const leftEntries = Object.entries(left);
+ const rightEntries = Object.entries(right);
return (
- value === "terminal" ||
- value === "fileTree" ||
- value === "gitReview" ||
- value === "tunnel"
+ leftEntries.length === rightEntries.length &&
+ leftEntries.every(([pathKey, activeTab]) => right[pathKey] === activeTab)
);
}
@@ -379,14 +382,16 @@ function mergeSyncedProjectToolsPanelSettings(
incoming: unknown,
): AppSettings["customSettings"]["projectToolsPanel"] {
const source = asObject(incoming);
- const activeTab = isProjectToolsPanelTab(source.activeTab)
- ? source.activeTab
- : current.activeTab;
- return activeTab === current.activeTab
+ if (!Object.prototype.hasOwnProperty.call(source, "activeTabs")) return current;
+ const activeTabs = {
+ ...current.activeTabs,
+ ...normalizeProjectToolsPanelActiveTabs(source.activeTabs),
+ };
+ return projectToolsPanelActiveTabsEqual(activeTabs, current.activeTabs)
? current
: {
...current,
- activeTab,
+ activeTabs,
};
}
diff --git a/crates/agent-gui/src/lib/settings/index.ts b/crates/agent-gui/src/lib/settings/index.ts
index 3ce59bcd4..fd633cf85 100644
--- a/crates/agent-gui/src/lib/settings/index.ts
+++ b/crates/agent-gui/src/lib/settings/index.ts
@@ -132,6 +132,7 @@ export type ProjectToolsPanelTab = "terminal" | "fileTree" | "gitReview" | "tunn
export type ProjectToolsPanelSettings = {
width: number;
activeTab: ProjectToolsPanelTab;
+ activeTabs: Record;
tabOrders: Record;
};
@@ -1577,6 +1578,33 @@ export function normalizeProjectToolsPanelTabOrder(input: unknown): string[] {
return order;
}
+function isProjectToolsPanelTab(input: unknown): input is ProjectToolsPanelTab {
+ return input === "terminal" ||
+ input === "fileTree" ||
+ input === "gitReview" ||
+ input === "tunnel";
+}
+
+export function normalizeProjectToolsPanelActiveTab(input: unknown): ProjectToolsPanelTab {
+ return isProjectToolsPanelTab(input) ? input : "fileTree";
+}
+
+export function normalizeProjectToolsPanelActiveTabs(
+ input: unknown,
+): Record {
+ const rawTabs = (
+ input && typeof input === "object" && !Array.isArray(input) ? input : {}
+ ) as Record;
+ const activeTabs: Record = {};
+ for (const [pathKey, value] of Object.entries(rawTabs)) {
+ const normalizedPathKey = workspaceProjectPathKey(pathKey);
+ if (!normalizedPathKey || !isProjectToolsPanelTab(value)) continue;
+ activeTabs[normalizedPathKey] = value;
+ if (Object.keys(activeTabs).length >= 100) break;
+ }
+ return activeTabs;
+}
+
export function normalizeProjectToolsPanelTabOrders(input: unknown): Record {
const rawOrders = (
input && typeof input === "object" && !Array.isArray(input) ? input : {}
@@ -1607,13 +1635,9 @@ export function normalizeCustomSettings(
const projectToolsPanel = (
obj.projectToolsPanel && typeof obj.projectToolsPanel === "object" ? obj.projectToolsPanel : {}
) as Record;
- const projectToolsPanelActiveTab =
- projectToolsPanel.activeTab === "terminal" ||
- projectToolsPanel.activeTab === "fileTree" ||
- projectToolsPanel.activeTab === "gitReview" ||
- projectToolsPanel.activeTab === "tunnel"
- ? projectToolsPanel.activeTab
- : "fileTree";
+ const projectToolsPanelActiveTab = normalizeProjectToolsPanelActiveTab(
+ projectToolsPanel.activeTab,
+ );
const projectToolsFileTree = (
obj.projectToolsFileTree && typeof obj.projectToolsFileTree === "object"
? obj.projectToolsFileTree
@@ -1646,6 +1670,7 @@ export function normalizeCustomSettings(
420,
),
activeTab: projectToolsPanelActiveTab,
+ activeTabs: normalizeProjectToolsPanelActiveTabs(projectToolsPanel.activeTabs),
tabOrders: normalizeProjectToolsPanelTabOrders(projectToolsPanel.tabOrders),
},
projectToolsFileTree: normalizeProjectToolsFileTreeSettings(projectToolsFileTree),
@@ -1876,6 +1901,56 @@ export function getProjectToolsPanelTabOrder(
return customSettings.projectToolsPanel.tabOrders[normalizedPathKey] ?? [];
}
+export function getProjectToolsPanelActiveTab(
+ customSettings: CustomSettings,
+ projectPathKey: string,
+): ProjectToolsPanelTab {
+ const normalizedPathKey = workspaceProjectPathKey(projectPathKey);
+ if (!normalizedPathKey) return customSettings.projectToolsPanel.activeTab;
+ return (
+ customSettings.projectToolsPanel.activeTabs[normalizedPathKey] ??
+ customSettings.projectToolsPanel.activeTab
+ );
+}
+
+export function updateProjectToolsPanelActiveTab(
+ prev: AppSettings,
+ projectPathKey: string,
+ activeTab: ProjectToolsPanelTab,
+): AppSettings {
+ const nextActiveTab = normalizeProjectToolsPanelActiveTab(activeTab);
+ const normalizedPathKey = workspaceProjectPathKey(projectPathKey);
+ if (!normalizedPathKey) {
+ if (prev.customSettings.projectToolsPanel.activeTab === nextActiveTab) return prev;
+ return updateCustomSettings(prev, {
+ projectToolsPanel: {
+ ...prev.customSettings.projectToolsPanel,
+ activeTab: nextActiveTab,
+ },
+ });
+ }
+
+ const currentProjectActiveTab =
+ prev.customSettings.projectToolsPanel.activeTabs[normalizedPathKey];
+ if (
+ prev.customSettings.projectToolsPanel.activeTab === nextActiveTab &&
+ currentProjectActiveTab === nextActiveTab
+ ) {
+ return prev;
+ }
+
+ return updateCustomSettings(prev, {
+ projectToolsPanel: {
+ ...prev.customSettings.projectToolsPanel,
+ activeTab: nextActiveTab,
+ activeTabs: {
+ ...prev.customSettings.projectToolsPanel.activeTabs,
+ [normalizedPathKey]: nextActiveTab,
+ },
+ },
+ });
+}
+
function projectToolsPanelTabOrderEqual(left: readonly string[], right: readonly string[]) {
return left.length === right.length && left.every((item, index) => item === right[index]);
}
@@ -1917,6 +1992,10 @@ export function removeProjectToolsProjectState(
prev.customSettings.projectToolsPanel.tabOrders,
normalizedPathKey,
);
+ const hasActiveTab = Object.hasOwn(
+ prev.customSettings.projectToolsPanel.activeTabs,
+ normalizedPathKey,
+ );
const openProjectPathKeys = prev.customSettings.projectToolsFileTree.openProjectPathKeys
.map((pathKey) => workspaceProjectPathKey(pathKey))
.filter(Boolean);
@@ -1948,6 +2027,7 @@ export function removeProjectToolsProjectState(
if (
!hasTabOrder &&
+ !hasActiveTab &&
!removedOpenProjectPathKey &&
!removedGitReviewOpenProjectPathKey &&
!removedTunnelOpenProjectPathKey &&
@@ -1962,6 +2042,12 @@ export function removeProjectToolsProjectState(
if (hasTabOrder) {
delete tabOrders[normalizedPathKey];
}
+ const activeTabs = hasActiveTab
+ ? { ...prev.customSettings.projectToolsPanel.activeTabs }
+ : prev.customSettings.projectToolsPanel.activeTabs;
+ if (hasActiveTab) {
+ delete activeTabs[normalizedPathKey];
+ }
const projects = hasFileTreeProjectState
? { ...prev.customSettings.projectToolsFileTree.projects }
@@ -1973,6 +2059,7 @@ export function removeProjectToolsProjectState(
return updateCustomSettings(prev, {
projectToolsPanel: {
...prev.customSettings.projectToolsPanel,
+ activeTabs,
tabOrders,
},
projectToolsFileTree: {
diff --git a/crates/agent-gui/src/lib/settings/storage.ts b/crates/agent-gui/src/lib/settings/storage.ts
index 1040aa0ad..e2f715d19 100644
--- a/crates/agent-gui/src/lib/settings/storage.ts
+++ b/crates/agent-gui/src/lib/settings/storage.ts
@@ -9,6 +9,8 @@ import {
normalizeProjectToolsFileTreeSettings,
normalizeProjectToolsGitReviewSettings,
normalizeProjectToolsTunnelSettings,
+ normalizeProjectToolsPanelActiveTab,
+ normalizeProjectToolsPanelActiveTabs,
normalizeProjectToolsPanelTabOrders,
normalizeSelectedModel,
normalizeSettings,
@@ -94,13 +96,9 @@ function readLocalUiSettings(): {
typeof legacyTerminalPanel.width === "string"
? Number(legacyTerminalPanel.width)
: 420;
- const projectToolsPanelActiveTab =
- projectToolsPanel.activeTab === "terminal" ||
- projectToolsPanel.activeTab === "fileTree" ||
- projectToolsPanel.activeTab === "gitReview" ||
- projectToolsPanel.activeTab === "tunnel"
- ? projectToolsPanel.activeTab
- : "fileTree";
+ const projectToolsPanelActiveTab = normalizeProjectToolsPanelActiveTab(
+ projectToolsPanel.activeTab,
+ );
return toPersistedLocalCustomSettings({
conversationTitleModel: normalizeSelectedModel(obj.conversationTitleModel),
chatSidebar: {
@@ -112,6 +110,7 @@ function readLocalUiSettings(): {
? Math.min(1280, Math.max(320, Math.floor(projectToolsPanelWidth)))
: 420,
activeTab: projectToolsPanelActiveTab,
+ activeTabs: normalizeProjectToolsPanelActiveTabs(projectToolsPanel.activeTabs),
tabOrders: normalizeProjectToolsPanelTabOrders(projectToolsPanel.tabOrders),
},
projectToolsFileTree: normalizeProjectToolsFileTreeSettings({}),
diff --git a/crates/agent-gui/src/lib/settings/sync.ts b/crates/agent-gui/src/lib/settings/sync.ts
index 044f4a2f2..c9d7ad817 100644
--- a/crates/agent-gui/src/lib/settings/sync.ts
+++ b/crates/agent-gui/src/lib/settings/sync.ts
@@ -3,9 +3,9 @@ import {
normalizeChatRuntimeControls,
normalizeProjectToolsFileTreeSettings,
normalizeProjectToolsGitReviewSettings,
+ normalizeProjectToolsPanelActiveTabs,
normalizeProjectToolsTunnelSettings,
normalizeSettings,
- type ProjectToolsPanelTab,
workspaceProjectPathKey,
} from "./index";
@@ -15,7 +15,7 @@ export type GatewaySettingsSyncProvider = Omit;
export type GatewaySettingsSyncCustomSettings = Omit<
Partial,
@@ -101,7 +101,7 @@ function syncableCustomSettings(
return {
...syncable,
projectToolsPanel: {
- activeTab: customSettings.projectToolsPanel.activeTab,
+ activeTabs: customSettings.projectToolsPanel.activeTabs,
},
chatSidebar: {
projectsCollapsed: false,
@@ -353,12 +353,15 @@ function mergeSyncedProjectToolsTunnelSettings(
};
}
-function isProjectToolsPanelTab(value: unknown): value is ProjectToolsPanelTab {
+function projectToolsPanelActiveTabsEqual(
+ left: AppSettings["customSettings"]["projectToolsPanel"]["activeTabs"],
+ right: AppSettings["customSettings"]["projectToolsPanel"]["activeTabs"],
+) {
+ const leftEntries = Object.entries(left);
+ const rightEntries = Object.entries(right);
return (
- value === "terminal" ||
- value === "fileTree" ||
- value === "gitReview" ||
- value === "tunnel"
+ leftEntries.length === rightEntries.length &&
+ leftEntries.every(([pathKey, activeTab]) => right[pathKey] === activeTab)
);
}
@@ -367,14 +370,16 @@ function mergeSyncedProjectToolsPanelSettings(
incoming: unknown,
): AppSettings["customSettings"]["projectToolsPanel"] {
const source = asObject(incoming);
- const activeTab = isProjectToolsPanelTab(source.activeTab)
- ? source.activeTab
- : current.activeTab;
- return activeTab === current.activeTab
+ if (!Object.hasOwn(source, "activeTabs")) return current;
+ const activeTabs = {
+ ...current.activeTabs,
+ ...normalizeProjectToolsPanelActiveTabs(source.activeTabs),
+ };
+ return projectToolsPanelActiveTabsEqual(activeTabs, current.activeTabs)
? current
: {
...current,
- activeTab,
+ activeTabs,
};
}
diff --git a/crates/agent-gui/src/pages/ChatPage.tsx b/crates/agent-gui/src/pages/ChatPage.tsx
index 046e1b8c2..9a84fb29d 100644
--- a/crates/agent-gui/src/pages/ChatPage.tsx
+++ b/crates/agent-gui/src/pages/ChatPage.tsx
@@ -113,6 +113,7 @@ import {
findProviderModelConfig,
getChatRuntimeReasoningLevelsForProvider,
getProjectToolsFileTreeProjectState,
+ getProjectToolsPanelActiveTab,
getProjectToolsPanelTabOrder,
isAgentDevMode,
isAgentExecutionMode,
@@ -133,6 +134,7 @@ import {
updateProjectToolsFileTreeOpen,
updateProjectToolsGitReviewOpen,
updateProjectToolsTunnelOpen,
+ updateProjectToolsPanelActiveTab,
updateProjectToolsPanelTabOrder,
updateChatRuntimeControlsForProvider,
updateMcp,
@@ -1103,12 +1105,7 @@ export function ChatPage(props: ChatPageProps) {
activateWorkspaceProject(project);
setSettings((prev) =>
updateProjectToolsFileTreeOpen(
- updateCustomSettings(prev, {
- projectToolsPanel: {
- ...prev.customSettings.projectToolsPanel,
- activeTab: "fileTree",
- },
- }),
+ updateProjectToolsPanelActiveTab(prev, pathKey, "fileTree"),
pathKey,
true,
),
@@ -1125,12 +1122,7 @@ export function ChatPage(props: ChatPageProps) {
setProjectToolsPanelOpen(true);
setSettings((prev) =>
updateProjectToolsTunnelOpen(
- updateCustomSettings(prev, {
- projectToolsPanel: {
- ...prev.customSettings.projectToolsPanel,
- activeTab: "tunnel",
- },
- }),
+ updateProjectToolsPanelActiveTab(prev, targetProjectPathKey, "tunnel"),
targetProjectPathKey,
true,
),
@@ -4671,7 +4663,10 @@ export function ChatPage(props: ChatPageProps) {
width={settings.customSettings.projectToolsPanel.width}
theme={settings.theme}
disabledMessage={terminalDisabledMessage}
- activeTab={settings.customSettings.projectToolsPanel.activeTab}
+ activeTab={getProjectToolsPanelActiveTab(
+ settings.customSettings,
+ terminalProjectPathKey,
+ )}
tabOrder={getProjectToolsPanelTabOrder(settings.customSettings, terminalProjectPathKey)}
fileTreeOpen={projectToolsFileTreeOpen}
fileTreeState={getProjectToolsFileTreeProjectState(
@@ -4699,12 +4694,7 @@ export function ChatPage(props: ChatPageProps) {
}
onActiveTabChange={(activeTab) =>
setSettings((prev) =>
- updateCustomSettings(prev, {
- projectToolsPanel: {
- ...prev.customSettings.projectToolsPanel,
- activeTab,
- },
- }),
+ updateProjectToolsPanelActiveTab(prev, terminalProjectPathKey, activeTab),
)
}
onTabOrderChange={(tabOrder) =>
diff --git a/crates/agent-gui/test/settings/normalization.test.mjs b/crates/agent-gui/test/settings/normalization.test.mjs
index e077f0b00..d77017dc9 100644
--- a/crates/agent-gui/test/settings/normalization.test.mjs
+++ b/crates/agent-gui/test/settings/normalization.test.mjs
@@ -463,6 +463,11 @@ test("gateway settings sync payload redacts provider api keys", () => {
projectToolsPanel: {
width: 612,
activeTab: "tunnel",
+ activeTabs: {
+ " /workspace/a ": "tunnel",
+ "/workspace/b": "gitReview",
+ " ": "terminal",
+ },
tabOrders: {
"/workspace/a": ["__tunnel__", "__file_tree__"],
},
@@ -523,8 +528,12 @@ test("gateway settings sync payload redacts provider api keys", () => {
openVersion: 3,
});
assert.deepEqual(payload.customSettings.projectToolsPanel, {
- activeTab: "tunnel",
+ activeTabs: {
+ "/workspace/a": "tunnel",
+ "/workspace/b": "gitReview",
+ },
});
+ assert.equal(Object.hasOwn(payload.customSettings.projectToolsPanel, "activeTab"), false);
assert.equal(Object.hasOwn(payload.customSettings.projectToolsPanel, "width"), false);
assert.equal(Object.hasOwn(payload.customSettings.projectToolsPanel, "tabOrders"), false);
assert.deepEqual(payload.chatRuntimeControls, appSettings.chatRuntimeControls);
@@ -647,6 +656,11 @@ test("normalizes project tools panel from current and legacy terminal panel sett
projectToolsPanel: {
width: 544,
activeTab: "terminal",
+ activeTabs: {
+ " /workspace/app ": "gitReview",
+ "/workspace/other": "invalid",
+ " ": "terminal",
+ },
tabOrders: {
" /workspace/app ": [
"terminal-2",
@@ -664,11 +678,38 @@ test("normalizes project tools panel from current and legacy terminal panel sett
assert.equal(currentShape.customSettings.projectToolsPanel.width, 544);
assert.equal(currentShape.customSettings.projectToolsPanel.activeTab, "terminal");
+ assert.deepEqual(currentShape.customSettings.projectToolsPanel.activeTabs, {
+ "/workspace/app": "gitReview",
+ });
assert.deepEqual(currentShape.customSettings.projectToolsPanel.tabOrders, {
"/workspace/app": ["terminal-2", "terminal-1", "__file_tree__"],
});
});
+test("updates project tools panel active tab per project", () => {
+ const base = settings.normalizeSettings({
+ customSettings: {
+ projectToolsPanel: {
+ activeTab: "terminal",
+ },
+ },
+ });
+ const updated = settings.updateProjectToolsPanelActiveTab(base, "/workspace/app", "gitReview");
+
+ assert.equal(updated.customSettings.projectToolsPanel.activeTab, "gitReview");
+ assert.equal(
+ settings.getProjectToolsPanelActiveTab(updated.customSettings, "/workspace/app"),
+ "gitReview",
+ );
+ assert.equal(
+ settings.getProjectToolsPanelActiveTab(updated.customSettings, "/workspace/other"),
+ "gitReview",
+ );
+ assert.deepEqual(updated.customSettings.projectToolsPanel.activeTabs, {
+ "/workspace/app": "gitReview",
+ });
+});
+
test("updates project tools panel tab order per project", () => {
const base = settings.normalizeSettings({});
const updated = settings.updateProjectToolsPanelTabOrder(base, "/workspace/app", [
@@ -829,6 +870,10 @@ test("removes project tools state when a workspace project is deleted", () => {
customSettings: {
projectToolsPanel: {
activeTab: "fileTree",
+ activeTabs: {
+ "/workspace/app": "fileTree",
+ "/workspace/other": "gitReview",
+ },
tabOrders: {
"/workspace/app": ["terminal-a", "__file_tree__"],
"/workspace/other": ["terminal-b"],
@@ -867,6 +912,9 @@ test("removes project tools state when a workspace project is deleted", () => {
const cleaned = settings.removeProjectToolsProjectState(base, "/workspace/app");
+ assert.deepEqual(cleaned.customSettings.projectToolsPanel.activeTabs, {
+ "/workspace/other": "gitReview",
+ });
assert.deepEqual(cleaned.customSettings.projectToolsPanel.tabOrders, {
"/workspace/other": ["terminal-b"],
});
@@ -940,12 +988,15 @@ test("removes project tools state when a workspace project is deleted", () => {
assert.deepEqual(tunnelOnlyCleaned.customSettings.projectToolsTunnel.openProjectPathKeys, []);
});
-test("gateway settings sync keeps project tools panel layout local and syncs active tab", () => {
+test("gateway settings sync keeps project tools panel layout local and syncs project active tabs", () => {
const current = settings.normalizeSettings({
customSettings: {
projectToolsPanel: {
width: 612,
activeTab: "terminal",
+ activeTabs: {
+ "/desktop/project": "terminal",
+ },
tabOrders: {
"/desktop/project": ["desktop-terminal", "__file_tree__"],
},
@@ -977,6 +1028,9 @@ test("gateway settings sync keeps project tools panel layout local and syncs act
projectToolsPanel: {
width: 360,
activeTab: "fileTree",
+ activeTabs: {
+ "/web/project": "fileTree",
+ },
tabOrders: {
"/web/project": ["web-terminal", "__file_tree__"],
},
@@ -1006,15 +1060,22 @@ test("gateway settings sync keeps project tools panel layout local and syncs act
const payload = sync.buildGatewaySettingsSyncPayload(incoming);
assert.deepEqual(payload.customSettings.projectToolsPanel, {
- activeTab: "fileTree",
+ activeTabs: {
+ "/web/project": "fileTree",
+ },
});
+ assert.equal(Object.hasOwn(payload.customSettings.projectToolsPanel, "activeTab"), false);
assert.equal(Object.hasOwn(payload.customSettings.projectToolsPanel, "width"), false);
assert.equal(Object.hasOwn(payload.customSettings.projectToolsPanel, "tabOrders"), false);
const synced = sync.applyGatewaySettingsSyncPayload(current, payload);
assert.equal(synced.customSettings.projectToolsPanel.width, 612);
- assert.equal(synced.customSettings.projectToolsPanel.activeTab, "fileTree");
+ assert.equal(synced.customSettings.projectToolsPanel.activeTab, "terminal");
+ assert.deepEqual(synced.customSettings.projectToolsPanel.activeTabs, {
+ "/desktop/project": "terminal",
+ "/web/project": "fileTree",
+ });
assert.deepEqual(synced.customSettings.projectToolsPanel.tabOrders, {
"/desktop/project": ["desktop-terminal", "__file_tree__"],
});
From 0ab55df7e6598d6c3965cff45849fc4b8dda4052 Mon Sep 17 00:00:00 2001
From: su-fen <715041@qq.com>
Date: Wed, 10 Jun 2026 10:37:42 +0800
Subject: [PATCH 3/9] feat(gateway): add status dashboard and asset serving
---
crates/agent-gateway/embed.go | 6 +-
crates/agent-gateway/embed_test.go | 56 +
crates/agent-gateway/internal/server/http.go | 17 +
.../internal/server/http_test.go | 27 +
crates/agent-gateway/web/src/main.tsx | 6 +-
.../web/src/pages/StatusDashboardPage.tsx | 1362 +++++++++++++++++
crates/agent-gateway/web/src/styles.css | 1130 ++++++++++++++
7 files changed, 2602 insertions(+), 2 deletions(-)
create mode 100644 crates/agent-gateway/embed_test.go
create mode 100644 crates/agent-gateway/web/src/pages/StatusDashboardPage.tsx
diff --git a/crates/agent-gateway/embed.go b/crates/agent-gateway/embed.go
index 93eb21e1b..fe1d98ef4 100644
--- a/crates/agent-gateway/embed.go
+++ b/crates/agent-gateway/embed.go
@@ -4,5 +4,9 @@ import "embed"
// WebUIAssets contains the embedded WebUI build output served by the HTTP server.
//
-//go:embed web/dist
+// The all: prefix is required because Vite may emit chunks whose names begin
+// with "_" (for example, lodash's _baseFor chunk). Plain directory embeds
+// silently exclude files and directories beginning with "." or "_".
+//
+//go:embed all:web/dist
var WebUIAssets embed.FS
diff --git a/crates/agent-gateway/embed_test.go b/crates/agent-gateway/embed_test.go
new file mode 100644
index 000000000..1596c6788
--- /dev/null
+++ b/crates/agent-gateway/embed_test.go
@@ -0,0 +1,56 @@
+package gateway
+
+import (
+ "io/fs"
+ "os"
+ "sort"
+ "testing"
+)
+
+func TestWebUIAssetsIncludeEntireDistTree(t *testing.T) {
+ diskFiles := regularFileSizes(t, os.DirFS("."), "web/dist")
+ embeddedFiles := regularFileSizes(t, WebUIAssets, "web/dist")
+
+ var missing []string
+ for file, size := range diskFiles {
+ embeddedSize, ok := embeddedFiles[file]
+ if !ok {
+ missing = append(missing, file)
+ continue
+ }
+ if embeddedSize != size {
+ t.Fatalf("embedded WebUI asset %q size = %d, want %d", file, embeddedSize, size)
+ }
+ }
+
+ if len(missing) > 0 {
+ sort.Strings(missing)
+ t.Fatalf("embedded WebUI assets are missing files from web/dist: %v", missing)
+ }
+}
+
+func regularFileSizes(t *testing.T, fileSystem fs.FS, root string) map[string]int64 {
+ t.Helper()
+
+ files := make(map[string]int64)
+ err := fs.WalkDir(fileSystem, root, func(path string, entry fs.DirEntry, walkErr error) error {
+ if walkErr != nil {
+ return walkErr
+ }
+ if entry.IsDir() {
+ return nil
+ }
+ info, err := entry.Info()
+ if err != nil {
+ return err
+ }
+ if info.Mode().IsRegular() {
+ files[path] = info.Size()
+ }
+ return nil
+ })
+ if err != nil {
+ t.Fatalf("walk %q: %v", root, err)
+ }
+ return files
+}
diff --git a/crates/agent-gateway/internal/server/http.go b/crates/agent-gateway/internal/server/http.go
index db8124167..446e0481b 100644
--- a/crates/agent-gateway/internal/server/http.go
+++ b/crates/agent-gateway/internal/server/http.go
@@ -43,6 +43,7 @@ func NewHTTPServer(cfg *config.Config, sm *session.Manager) http.Handler {
}
fileServer := http.FileServer(http.FS(webFS))
serveIndex := func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Content-Type", "text/html; charset=utf-8")
http.ServeContent(w, r, "index.html", time.Time{}, bytes.NewReader(indexHTML))
}
@@ -58,18 +59,34 @@ func NewHTTPServer(cfg *config.Config, sm *session.Manager) http.Handler {
if err == nil {
if stat, statErr := file.Stat(); statErr == nil && !stat.IsDir() {
_ = file.Close()
+ if strings.HasPrefix(cleanPath, "assets/") {
+ w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
+ }
fileServer.ServeHTTP(w, r)
return
}
_ = file.Close()
}
+ if isWebUIStaticAssetPath(cleanPath) {
+ http.NotFound(w, r)
+ return
+ }
+
serveIndex(w, r)
})
return rootMux
}
+func isWebUIStaticAssetPath(cleanPath string) bool {
+ cleanPath = strings.TrimSpace(cleanPath)
+ if cleanPath == "" || cleanPath == "." || cleanPath == "index.html" {
+ return false
+ }
+ return strings.HasPrefix(cleanPath, "assets/") || path.Ext(cleanPath) != ""
+}
+
func publicHistoryShare(cfg *config.Config, sm *session.Manager) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
token := strings.TrimSpace(r.PathValue("token"))
diff --git a/crates/agent-gateway/internal/server/http_test.go b/crates/agent-gateway/internal/server/http_test.go
index 0e28201a6..7909fc6d8 100644
--- a/crates/agent-gateway/internal/server/http_test.go
+++ b/crates/agent-gateway/internal/server/http_test.go
@@ -29,6 +29,9 @@ func TestNewHTTPServerServesRootWithoutRedirect(t *testing.T) {
if !strings.Contains(rec.Body.String(), "LiveAgent Gateway") {
t.Fatalf("expected WebUI index.html, got body %q", rec.Body.String())
}
+ if cacheControl := rec.Header().Get("Cache-Control"); !strings.Contains(cacheControl, "no-store") {
+ t.Fatalf("Cache-Control = %q, want no-store for index.html", cacheControl)
+ }
}
func TestNewHTTPServerServesSpaFallbackWithoutRedirect(t *testing.T) {
@@ -49,6 +52,30 @@ func TestNewHTTPServerServesSpaFallbackWithoutRedirect(t *testing.T) {
}
}
+func TestNewHTTPServerDoesNotFallbackMissingStaticAssetsToIndex(t *testing.T) {
+ handler := NewHTTPServer(&config.Config{Token: "dev-token"}, session.NewManager())
+
+ for _, target := range []string{
+ "http://gateway.test/assets/missing-module.js",
+ "http://gateway.test/assets/missing-style.css",
+ "http://gateway.test/missing-icon.svg",
+ } {
+ req := httptest.NewRequest(http.MethodGet, target, nil)
+ rec := httptest.NewRecorder()
+ handler.ServeHTTP(rec, req)
+
+ if rec.Code != http.StatusNotFound {
+ t.Fatalf("%s status = %d, want %d", target, rec.Code, http.StatusNotFound)
+ }
+ if strings.Contains(rec.Body.String(), "LiveAgent Gateway") {
+ t.Fatalf("%s returned SPA index fallback for a missing static asset", target)
+ }
+ if contentType := rec.Header().Get("Content-Type"); strings.Contains(contentType, "text/html") {
+ t.Fatalf("%s Content-Type = %q, want non-html 404", target, contentType)
+ }
+ }
+}
+
func TestPublicHistoryShareResolvesWithoutAuthorization(t *testing.T) {
sm := session.NewManager()
sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1")
diff --git a/crates/agent-gateway/web/src/main.tsx b/crates/agent-gateway/web/src/main.tsx
index 4cbece945..ab3c36d5a 100644
--- a/crates/agent-gateway/web/src/main.tsx
+++ b/crates/agent-gateway/web/src/main.tsx
@@ -2,6 +2,7 @@ import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
+import { StatusDashboardPage } from "./pages/StatusDashboardPage";
import "./index.css";
import "react-complex-tree/lib/style-modern.css";
import "streamdown/styles.css";
@@ -9,8 +10,11 @@ import "./styles.css";
document.documentElement.dataset.liveagentWebui = "gateway";
+const dashboardPaths = new Set(["/dashboard", "/status-board", "/observatory"]);
+const Root = dashboardPaths.has(window.location.pathname) ? StatusDashboardPage : App;
+
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
-
+
,
);
diff --git a/crates/agent-gateway/web/src/pages/StatusDashboardPage.tsx b/crates/agent-gateway/web/src/pages/StatusDashboardPage.tsx
new file mode 100644
index 000000000..6d8ddc9f9
--- /dev/null
+++ b/crates/agent-gateway/web/src/pages/StatusDashboardPage.tsx
@@ -0,0 +1,1362 @@
+import { useEffect, useMemo, useRef, useState } from "react";
+
+import {
+ AlertCircle,
+ Bot,
+ Brain,
+ CheckCircle2,
+ Cloud,
+ ExternalLink,
+ Globe2,
+ HardDrive,
+ History,
+ Loader2,
+ LogOut,
+ MessageSquareText,
+ Plug,
+ Radio,
+ RefreshCw,
+ Server,
+ Shield,
+ Sparkles,
+ Terminal,
+ Timer,
+ Wifi,
+ WifiOff,
+ Wrench,
+ Zap,
+ type IconComponent,
+} from "@/components/icons";
+import { Button } from "@/components/ui/button";
+import { normalizeGatewayAccessToken, verifyGatewayAccessToken } from "@/lib/gatewayAuth";
+import {
+ getGatewayWebSocketClient,
+ resetGatewayWebSocketClient,
+ type GatewayWebSocketClientLike,
+ type TunnelSummary,
+} from "@/lib/gatewaySocket";
+import type {
+ AgentStatus,
+ ChatEvent,
+ ConversationSummary,
+ GatewayProviderSummary,
+ HistoryList,
+ HistoryWorkdirSummary,
+} from "@/lib/gatewayTypes";
+import { cn } from "@/lib/shared/utils";
+import type { GatewaySettingsSyncPayload } from "@/lib/settings/sync";
+import { clearToken, loadToken, saveToken } from "@/lib/storage";
+import type { TerminalSession } from "@/lib/terminal/types";
+import { LoginPage } from "./LoginPage";
+
+type DashboardTone = "cyan" | "violet" | "rose" | "amber" | "emerald" | "slate";
+
+type DashboardEvent = {
+ id: string;
+ at: number;
+ title: string;
+ detail: string;
+ tone: DashboardTone;
+ conversationId?: string;
+ workdir?: string;
+};
+
+type LiveCounters = {
+ events: number;
+ tokenChunks: number;
+ tokenChars: number;
+ thinking: number;
+ toolCalls: number;
+ toolResults: number;
+ searches: number;
+ completions: number;
+ errors: number;
+ startedAt: number;
+};
+
+type PendingCounters = Omit;
+
+type SnapshotState = {
+ loading: boolean;
+ error: string | null;
+ lastRefreshAt: number;
+};
+
+type MetricCard = {
+ label: string;
+ value: string;
+ unit: string;
+ detail: string;
+ tone: DashboardTone;
+ icon: IconComponent;
+};
+
+type FactItem = {
+ label: string;
+ value: string;
+ unit?: string;
+ note?: string;
+ tone?: DashboardTone;
+};
+
+type LoadSegment = {
+ label: string;
+ value: number;
+ unit: string;
+ width: number;
+ tone: DashboardTone;
+};
+
+const HISTORY_PAGE_SIZE = 80;
+const SNAPSHOT_REFRESH_MS = 10_000;
+const LIVE_FLUSH_MS = 500;
+const TOKEN_EVENT_MIN_INTERVAL_MS = 1_200;
+const MAX_RECENT_EVENTS = 12;
+
+const initialCounters = (): LiveCounters => ({
+ events: 0,
+ tokenChunks: 0,
+ tokenChars: 0,
+ thinking: 0,
+ toolCalls: 0,
+ toolResults: 0,
+ searches: 0,
+ completions: 0,
+ errors: 0,
+ startedAt: Date.now(),
+});
+
+const initialPendingCounters = (): PendingCounters => ({
+ events: 0,
+ tokenChunks: 0,
+ tokenChars: 0,
+ thinking: 0,
+ toolCalls: 0,
+ toolResults: 0,
+ searches: 0,
+ completions: 0,
+ errors: 0,
+});
+
+function readDashboardTokenSeed() {
+ const params = new URLSearchParams(window.location.search);
+ const queryToken = params.get("token") ?? params.get("access_token") ?? "";
+ return normalizeGatewayAccessToken(queryToken || loadToken());
+}
+
+function stripDashboardTokenFromUrl() {
+ const url = new URL(window.location.href);
+ if (!url.searchParams.has("token") && !url.searchParams.has("access_token")) {
+ return;
+ }
+ url.searchParams.delete("token");
+ url.searchParams.delete("access_token");
+ window.history.replaceState({}, document.title, `${url.pathname}${url.search}${url.hash}`);
+}
+
+function asErrorMessage(error: unknown, fallback: string) {
+ if (error instanceof Error && error.message.trim()) {
+ return error.message.trim();
+ }
+ if (typeof error === "string" && error.trim()) {
+ return error.trim();
+ }
+ return fallback;
+}
+
+function normalizeEpochMs(value: number | null | undefined) {
+ if (!value || !Number.isFinite(value)) {
+ return 0;
+ }
+ return value > 10_000_000_000 ? value : value * 1000;
+}
+
+function formatDuration(ms: number) {
+ if (!Number.isFinite(ms) || ms <= 0) {
+ return "0 s";
+ }
+ const seconds = Math.floor(ms / 1000);
+ if (seconds < 60) {
+ return `${seconds} s`;
+ }
+ const minutes = Math.floor(seconds / 60);
+ if (minutes < 60) {
+ return `${minutes} min ${seconds % 60} s`;
+ }
+ const hours = Math.floor(minutes / 60);
+ if (hours < 24) {
+ return `${hours} h ${minutes % 60} min`;
+ }
+ const days = Math.floor(hours / 24);
+ return `${days} d ${hours % 24} h`;
+}
+
+function formatClock(ms: number) {
+ if (!ms) {
+ return "--:--:--";
+ }
+ return new Intl.DateTimeFormat("zh-CN", {
+ hour: "2-digit",
+ minute: "2-digit",
+ second: "2-digit",
+ hour12: false,
+ }).format(new Date(ms));
+}
+
+function formatDateTime(ms: number) {
+ if (!ms) {
+ return "未知";
+ }
+ return new Intl.DateTimeFormat("zh-CN", {
+ month: "2-digit",
+ day: "2-digit",
+ hour: "2-digit",
+ minute: "2-digit",
+ second: "2-digit",
+ hour12: false,
+ }).format(new Date(ms));
+}
+
+function compactNumber(value: number) {
+ return new Intl.NumberFormat("zh-CN", { notation: "compact", maximumFractionDigits: 1 }).format(
+ Math.max(0, value),
+ );
+}
+
+function percentage(value: number) {
+ return `${Math.round(Math.max(0, Math.min(100, value)))}%`;
+}
+
+function formatRuntimeState(status: AgentStatus | null) {
+ const explicit = status?.runtime_state?.trim();
+ if (explicit) {
+ return explicit;
+ }
+ if (status?.online) {
+ return status.chat_runtime_ready ? "ready" : "connected";
+ }
+ return "offline";
+}
+
+function formatBooleanFlag(enabled: boolean | undefined) {
+ if (typeof enabled !== "boolean") {
+ return "--";
+ }
+ return enabled ? "ON" : "OFF";
+}
+
+function truncateMiddle(value: string, maxLength = 34) {
+ const text = value.trim();
+ if (text.length <= maxLength) {
+ return text;
+ }
+ const head = Math.ceil((maxLength - 1) * 0.56);
+ const tail = Math.floor((maxLength - 1) * 0.44);
+ return `${text.slice(0, head)}…${text.slice(-tail)}`;
+}
+
+function basename(path: string) {
+ const normalized = path.trim().replace(/\\/g, "/").replace(/\/+$/, "");
+ if (!normalized) {
+ return "未命名项目";
+ }
+ return normalized.split("/").filter(Boolean).pop() ?? normalized;
+}
+
+function getConversationTitle(conversation: ConversationSummary | undefined, fallback: string) {
+ const title = conversation?.title?.trim();
+ return title || fallback;
+}
+
+function buildRunningConversations(history: HistoryList | null) {
+ if (!history) {
+ return [];
+ }
+ const byId = new Map(history.conversations.map((item) => [item.id, item]));
+ const runningIds = new Set();
+ for (const id of history.running_conversation_ids ?? []) {
+ if (id.trim()) {
+ runningIds.add(id.trim());
+ }
+ }
+ for (const item of history.running_conversations ?? []) {
+ if (item.conversation_id.trim()) {
+ runningIds.add(item.conversation_id.trim());
+ }
+ }
+ return Array.from(runningIds).map((id) => {
+ const conversation = byId.get(id);
+ const runtime = history.running_conversations?.find((item) => item.conversation_id === id);
+ return {
+ id,
+ title: getConversationTitle(conversation, `会话 ${truncateMiddle(id, 12)}`),
+ cwd: runtime?.cwd?.trim() || conversation?.cwd?.trim() || "",
+ updatedAt: normalizeEpochMs(runtime?.updated_at || conversation?.updated_at),
+ messageCount: conversation?.message_count ?? 0,
+ provider: conversation?.provider_id?.trim() || "",
+ model: conversation?.model?.trim() || "",
+ };
+ });
+}
+
+function updateHistoryListWithEvent(history: HistoryList | null, event: any): HistoryList | null {
+ if (!history) {
+ return history;
+ }
+ const conversationId = typeof event.conversation_id === "string" ? event.conversation_id.trim() : "";
+ if (!conversationId) {
+ return history;
+ }
+
+ if (event.kind === "delete") {
+ return {
+ ...history,
+ total_count: Math.max(0, history.total_count - 1),
+ conversations: history.conversations.filter((item) => item.id !== conversationId),
+ running_conversation_ids: (history.running_conversation_ids ?? []).filter((id) => id !== conversationId),
+ running_conversations: (history.running_conversations ?? []).filter(
+ (item) => item.conversation_id !== conversationId,
+ ),
+ };
+ }
+
+ if (event.kind === "running" || event.kind === "idle") {
+ const currentIds = new Set(history.running_conversation_ids ?? []);
+ if (event.kind === "running") {
+ currentIds.add(conversationId);
+ } else {
+ currentIds.delete(conversationId);
+ }
+ return {
+ ...history,
+ running_conversation_ids: Array.from(currentIds),
+ running_conversations: (history.running_conversations ?? []).filter((item) => {
+ if (item.conversation_id !== conversationId) {
+ return true;
+ }
+ return event.kind === "running";
+ }),
+ };
+ }
+
+ const conversation = event.conversation as ConversationSummary | undefined;
+ if (event.kind !== "upsert" || !conversation?.id) {
+ return history;
+ }
+
+ const without = history.conversations.filter((item) => item.id !== conversation.id);
+ const conversations = [conversation, ...without]
+ .sort((a, b) => (b.updated_at ?? 0) - (a.updated_at ?? 0))
+ .slice(0, HISTORY_PAGE_SIZE);
+ return {
+ ...history,
+ total_count: Math.max(history.total_count, conversations.length),
+ conversations,
+ };
+}
+
+function classifyChatEvent(event: ChatEvent) {
+ const type = String(event.type);
+ switch (type) {
+ case "token":
+ return { tone: "cyan" as const, title: "Token 流" };
+ case "thinking":
+ return { tone: "violet" as const, title: "推理脉冲" };
+ case "tool_call":
+ return { tone: "amber" as const, title: "工具调用" };
+ case "tool_result": {
+ const isError = "isError" in event && event.isError === true;
+ return { tone: isError ? ("rose" as const) : ("emerald" as const), title: "工具回传" };
+ }
+ case "hosted_search":
+ return { tone: "cyan" as const, title: "联网检索" };
+ case "tool_status":
+ return { tone: "slate" as const, title: "工具状态" };
+ case "done":
+ case "completed":
+ return { tone: "emerald" as const, title: "会话完成" };
+ case "error":
+ case "failed":
+ return { tone: "rose" as const, title: "异常事件" };
+ case "accepted":
+ case "delivered":
+ case "claimed":
+ case "starting":
+ case "started":
+ case "progress":
+ return { tone: "violet" as const, title: "调度推进" };
+ case "cancelled":
+ return { tone: "amber" as const, title: "任务取消" };
+ default:
+ return { tone: "slate" as const, title: "实时事件" };
+ }
+}
+
+function getToolName(event: ChatEvent) {
+ if ("name" in event && typeof event.name === "string" && event.name.trim()) {
+ return event.name.trim();
+ }
+ if ("id" in event && typeof event.id === "string" && event.id.trim()) {
+ return event.id.trim();
+ }
+ return "system tool";
+}
+
+function summarizeChatEvent(event: ChatEvent): DashboardEvent | null {
+ const type = String(event.type);
+ const classified = classifyChatEvent(event);
+ let detail = "";
+
+ if (type === "token") {
+ detail = "模型正在输出 token chunk。";
+ } else if (type === "thinking") {
+ const text = "text" in event && typeof event.text === "string" ? event.text.trim() : "";
+ detail = text ? truncateMiddle(text.replace(/\s+/g, " "), 80) : "模型正在整理推理上下文。";
+ } else if (type === "tool_call") {
+ detail = `${getToolName(event)} 已发起调用。`;
+ } else if (type === "tool_result") {
+ const isError = "isError" in event && event.isError === true;
+ detail = `${getToolName(event)} ${isError ? "返回异常" : "返回结果"}。`;
+ } else if (type === "hosted_search") {
+ const queries = "queries" in event && Array.isArray(event.queries) ? event.queries : [];
+ detail = queries.length ? truncateMiddle(queries.join(" / "), 80) : "联网检索通道产生事件。";
+ } else if (type === "tool_status") {
+ const statusText = "status" in event && typeof event.status === "string" ? event.status.trim() : "";
+ detail = statusText || "工具链状态更新。";
+ } else if (type === "error" || type === "failed") {
+ detail = "message" in event && typeof event.message === "string" ? event.message : "执行出现异常。";
+ } else if (type === "done" || type === "completed") {
+ detail = "一段会话工作流完成。";
+ } else if ("message" in event && typeof event.message === "string" && event.message.trim()) {
+ detail = event.message.trim();
+ } else {
+ detail = `事件类型:${type}`;
+ }
+
+ return {
+ id: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
+ at: Date.now(),
+ title: classified.title,
+ detail: truncateMiddle(detail, 112),
+ tone: classified.tone,
+ conversationId: event.conversation_id,
+ workdir: event.workdir,
+ };
+}
+
+function addPendingCounters(target: PendingCounters, event: ChatEvent) {
+ const type = String(event.type);
+ target.events += 1;
+ if (type === "token") {
+ target.tokenChunks += 1;
+ const text = "text" in event && typeof event.text === "string" ? event.text : "";
+ target.tokenChars += text.length;
+ } else if (type === "thinking") {
+ target.thinking += 1;
+ } else if (type === "tool_call") {
+ target.toolCalls += 1;
+ } else if (type === "tool_result") {
+ target.toolResults += 1;
+ } else if (type === "hosted_search") {
+ target.searches += 1;
+ } else if (type === "done" || type === "completed") {
+ target.completions += 1;
+ } else if (type === "error" || type === "failed") {
+ target.errors += 1;
+ }
+}
+
+function useNow(tickMs = 1000) {
+ const [now, setNow] = useState(() => Date.now());
+ useEffect(() => {
+ const timer = window.setInterval(() => setNow(Date.now()), tickMs);
+ return () => window.clearInterval(timer);
+ }, [tickMs]);
+ return now;
+}
+
+function StatusPill({ online, label }: { online: boolean; label: string }) {
+ return (
+
+
+ {label}
+
+ );
+}
+
+function MetricTile({ metric }: { metric: MetricCard }) {
+ const Icon = metric.icon;
+ return (
+
+
+
+
+
+
{metric.label}
+
{metric.value}
+
{metric.unit}
+
{metric.detail}
+
+
+ );
+}
+
+function EmptyState({ children }: { children: string }) {
+ return {children}
;
+}
+
+function FactList({ items }: { items: FactItem[] }) {
+ return (
+
+ {items.map((item) => (
+
+ {item.label}
+ {item.value}
+ {item.unit && {item.unit}}
+ {item.note && {item.note}}
+
+ ))}
+
+ );
+}
+
+function runSnapshotRequest(promise: Promise) {
+ return promise.then(
+ (value) => ({ ok: true as const, value }),
+ (error) => ({ ok: false as const, error }),
+ );
+}
+
+function useDashboardAuth() {
+ const initialTokenRef = useRef(readDashboardTokenSeed());
+ const [token, setToken] = useState("");
+ const [loginToken, setLoginToken] = useState(initialTokenRef.current);
+ const [authSubmitting, setAuthSubmitting] = useState(() => initialTokenRef.current !== "");
+ const [authError, setAuthError] = useState(null);
+
+ useEffect(() => {
+ const seed = initialTokenRef.current;
+ if (!seed) {
+ return;
+ }
+ let cancelled = false;
+ setAuthSubmitting(true);
+ verifyGatewayAccessToken(seed)
+ .then((verifiedToken) => {
+ if (cancelled) {
+ return;
+ }
+ saveToken(verifiedToken);
+ stripDashboardTokenFromUrl();
+ setToken(verifiedToken);
+ setLoginToken(verifiedToken);
+ setAuthError(null);
+ })
+ .catch((error) => {
+ if (cancelled) {
+ return;
+ }
+ clearToken();
+ stripDashboardTokenFromUrl();
+ setAuthError(asErrorMessage(error, "Access Token 验证失败。"));
+ })
+ .finally(() => {
+ if (!cancelled) {
+ setAuthSubmitting(false);
+ }
+ });
+ return () => {
+ cancelled = true;
+ };
+ }, []);
+
+ const submit = () => {
+ setAuthSubmitting(true);
+ setAuthError(null);
+ verifyGatewayAccessToken(loginToken)
+ .then((verifiedToken) => {
+ saveToken(verifiedToken);
+ setToken(verifiedToken);
+ setLoginToken(verifiedToken);
+ })
+ .catch((error) => {
+ setAuthError(asErrorMessage(error, "Access Token 验证失败。"));
+ })
+ .finally(() => setAuthSubmitting(false));
+ };
+
+ const logout = () => {
+ clearToken();
+ resetGatewayWebSocketClient();
+ setToken("");
+ setLoginToken("");
+ setAuthError(null);
+ setAuthSubmitting(false);
+ };
+
+ return {
+ token,
+ loginToken,
+ authSubmitting,
+ authError,
+ setLoginToken,
+ setAuthError,
+ submit,
+ logout,
+ };
+}
+
+export function StatusDashboardPage() {
+ const now = useNow();
+ const { token, loginToken, authSubmitting, authError, setLoginToken, setAuthError, submit, logout } =
+ useDashboardAuth();
+ const api = useMemo(() => (token ? getGatewayWebSocketClient(token) : null), [token]);
+ const pendingEventsRef = useRef([]);
+ const pendingCountersRef = useRef(initialPendingCounters());
+ const lastTokenEventAtRef = useRef(0);
+
+ const [status, setStatus] = useState(null);
+ const [statusError, setStatusError] = useState(null);
+ const [history, setHistory] = useState(null);
+ const [workdirs, setWorkdirs] = useState([]);
+ const [tunnels, setTunnels] = useState([]);
+ const [terminals, setTerminals] = useState([]);
+ const [providers, setProviders] = useState([]);
+ const [settingsSnapshot, setSettingsSnapshot] = useState(null);
+ const [recentEvents, setRecentEvents] = useState([]);
+ const [liveCounters, setLiveCounters] = useState(() => initialCounters());
+ const [snapshot, setSnapshot] = useState({
+ loading: false,
+ error: null,
+ lastRefreshAt: 0,
+ });
+ const [refreshVersion, setRefreshVersion] = useState(0);
+
+ useEffect(() => {
+ const timer = window.setInterval(() => {
+ const nextEvents = pendingEventsRef.current.splice(0, pendingEventsRef.current.length);
+ const pendingCounters = pendingCountersRef.current;
+ pendingCountersRef.current = initialPendingCounters();
+
+ if (nextEvents.length > 0) {
+ setRecentEvents((current) => [...nextEvents.reverse(), ...current].slice(0, MAX_RECENT_EVENTS));
+ }
+ if (pendingCounters.events > 0) {
+ setLiveCounters((current) => ({
+ ...current,
+ events: current.events + pendingCounters.events,
+ tokenChunks: current.tokenChunks + pendingCounters.tokenChunks,
+ tokenChars: current.tokenChars + pendingCounters.tokenChars,
+ thinking: current.thinking + pendingCounters.thinking,
+ toolCalls: current.toolCalls + pendingCounters.toolCalls,
+ toolResults: current.toolResults + pendingCounters.toolResults,
+ searches: current.searches + pendingCounters.searches,
+ completions: current.completions + pendingCounters.completions,
+ errors: current.errors + pendingCounters.errors,
+ }));
+ }
+ }, LIVE_FLUSH_MS);
+ return () => window.clearInterval(timer);
+ }, []);
+
+ useEffect(() => {
+ if (!api) {
+ return;
+ }
+ const unsubscribeStatus = api.subscribeStatus((nextStatus, error) => {
+ setStatus(nextStatus);
+ setStatusError(error);
+ });
+ const unsubscribeHistory = api.subscribeHistory((event) => {
+ setHistory((current) => updateHistoryListWithEvent(current, event));
+ });
+ const unsubscribeConversation = api.subscribeConversation((event) => {
+ addPendingCounters(pendingCountersRef.current, event);
+ if (event.type === "token") {
+ const currentAt = Date.now();
+ if (currentAt - lastTokenEventAtRef.current < TOKEN_EVENT_MIN_INTERVAL_MS) {
+ return;
+ }
+ lastTokenEventAtRef.current = currentAt;
+ }
+ const summary = summarizeChatEvent(event);
+ if (summary) {
+ pendingEventsRef.current.push(summary);
+ }
+ });
+ const unsubscribeTerminal = api.subscribeTerminal((event) => {
+ if (event.session) {
+ const session = event.session;
+ setTerminals((current) => {
+ const without = current.filter((item) => item.id !== session.id);
+ return [session, ...without].sort((a, b) => b.updatedAt - a.updatedAt);
+ });
+ }
+ });
+ const unsubscribeSettings = api.subscribeSettings((payload) => {
+ setSettingsSnapshot(payload);
+ });
+
+ return () => {
+ unsubscribeStatus();
+ unsubscribeHistory();
+ unsubscribeConversation();
+ unsubscribeTerminal();
+ unsubscribeSettings();
+ };
+ }, [api]);
+
+ useEffect(() => {
+ if (!api) {
+ return;
+ }
+ let cancelled = false;
+ async function refresh(currentApi: GatewayWebSocketClientLike) {
+ setSnapshot((current) => ({ ...current, loading: true, error: null }));
+ const [
+ statusResult,
+ historyResult,
+ workdirsResult,
+ tunnelsResult,
+ terminalsResult,
+ providersResult,
+ settingsResult,
+ ] = await Promise.all([
+ runSnapshotRequest(currentApi.getStatus()),
+ runSnapshotRequest(currentApi.listHistory(1, HISTORY_PAGE_SIZE)),
+ runSnapshotRequest(currentApi.listHistoryWorkdirs()),
+ runSnapshotRequest(currentApi.listTunnels()),
+ runSnapshotRequest(currentApi.listTerminals()),
+ runSnapshotRequest(currentApi.listProviders()),
+ runSnapshotRequest(currentApi.getSettings()),
+ ]);
+ if (cancelled) {
+ return;
+ }
+ const errors: string[] = [];
+ if (statusResult.ok) {
+ setStatus(statusResult.value);
+ setStatusError(null);
+ } else {
+ errors.push(asErrorMessage(statusResult.error, "状态读取失败"));
+ }
+ if (historyResult.ok) {
+ setHistory(historyResult.value);
+ } else {
+ errors.push(asErrorMessage(historyResult.error, "历史读取失败"));
+ }
+ if (workdirsResult.ok) {
+ setWorkdirs(workdirsResult.value.workdirs);
+ } else {
+ errors.push(asErrorMessage(workdirsResult.error, "项目活动读取失败"));
+ }
+ if (tunnelsResult.ok) {
+ setTunnels(tunnelsResult.value);
+ } else {
+ errors.push(asErrorMessage(tunnelsResult.error, "公网通道读取失败"));
+ }
+ if (terminalsResult.ok) {
+ setTerminals(terminalsResult.value);
+ } else {
+ errors.push(asErrorMessage(terminalsResult.error, "终端读取失败"));
+ }
+ if (providersResult.ok) {
+ setProviders(providersResult.value);
+ } else {
+ errors.push(asErrorMessage(providersResult.error, "模型源读取失败"));
+ }
+ if (settingsResult.ok) {
+ setSettingsSnapshot(settingsResult.value);
+ } else {
+ errors.push(asErrorMessage(settingsResult.error, "设置读取失败"));
+ }
+ setSnapshot({
+ loading: false,
+ error: errors.length > 0 ? Array.from(new Set(errors)).slice(0, 2).join(" / ") : null,
+ lastRefreshAt: Date.now(),
+ });
+ }
+
+ void refresh(api);
+ const timer = window.setInterval(() => void refresh(api), SNAPSHOT_REFRESH_MS);
+ return () => {
+ cancelled = true;
+ window.clearInterval(timer);
+ };
+ }, [api, refreshVersion]);
+
+ const runningConversations = useMemo(() => buildRunningConversations(history), [history]);
+ const activeTunnels = useMemo(
+ () => tunnels.filter((item) => item.status === "active" && (!item.expiresAt || item.expiresAt > now)),
+ [now, tunnels],
+ );
+ const runningTerminals = useMemo(() => terminals.filter((item) => item.running), [terminals]);
+ const activeProviders = useMemo(
+ () => providers.filter((provider) => provider.activeModels.length > 0),
+ [providers],
+ );
+ const activeModelCount = useMemo(
+ () => activeProviders.reduce((total, provider) => total + provider.activeModels.length, 0),
+ [activeProviders],
+ );
+ const uptimeMs = status?.online ? now - normalizeEpochMs(status.connected_since) : 0;
+ const heartbeatAgeMs = status?.last_heartbeat ? now - normalizeEpochMs(status.last_heartbeat) : 0;
+ const isFreshHeartbeat = status?.online === true && heartbeatAgeMs < 20_000;
+ const observedMinutes = Math.max(1, (now - liveCounters.startedAt) / 60_000);
+ const eventsPerMinute = liveCounters.events / observedMinutes;
+ const messageSampleCount = useMemo(
+ () => (history?.conversations ?? []).reduce((total, item) => total + (item.message_count || 0), 0),
+ [history],
+ );
+ const todayConversationCount = useMemo(() => {
+ const start = new Date(now);
+ start.setHours(0, 0, 0, 0);
+ return (history?.conversations ?? []).filter((item) => normalizeEpochMs(item.created_at) >= start.getTime()).length;
+ }, [history, now]);
+ const runtimeState = formatRuntimeState(status);
+ const runtimeHeartbeatAgeMs = status?.runtime_last_heartbeat ? now - normalizeEpochMs(status.runtime_last_heartbeat) : 0;
+ const runtimeActiveRunCount = status?.runtime_active_run_count ?? runningConversations.length;
+ const totalTunnelConnections = activeTunnels.reduce((sum, item) => sum + item.activeConnections, 0);
+ const activeWorkspaceProjects = settingsSnapshot?.system.workspaceProjects ?? [];
+ const activeWorkspaceProject =
+ activeWorkspaceProjects.find((project) => project.id === settingsSnapshot?.system.activeWorkspaceProjectId) ??
+ activeWorkspaceProjects[0];
+ const selectedModel = settingsSnapshot?.selectedModel ?? null;
+ const selectedProvider = selectedModel
+ ? providers.find((provider) => provider.id === selectedModel.customProviderId) ??
+ settingsSnapshot?.customProviders.find((provider) => provider.id === selectedModel.customProviderId)
+ : undefined;
+ const selectedProviderName = selectedProvider?.name?.trim() || selectedModel?.customProviderId || "--";
+ const selectedProviderType = selectedProvider?.type || "--";
+ const selectedModelConfig = selectedProvider?.models?.find((model) => model.id === selectedModel?.model);
+ const enabledCronCount = settingsSnapshot?.cron.filter((task) => task.enabled).length ?? 0;
+ const enabledHookCount = settingsSnapshot?.hooks.filter((hook) => hook.enabled).length ?? 0;
+ const enabledMcpCount = settingsSnapshot?.mcp.servers.filter((server) => server.enabled).length ?? 0;
+ const configuredProviderCount = settingsSnapshot?.customProviders.filter((provider) => provider.apiKeyConfigured).length ?? 0;
+ const selectedSkillCount = settingsSnapshot?.skills.enabled ? settingsSnapshot.skills.selected.length : 0;
+ const remoteFeatureCount = settingsSnapshot?.remote
+ ? [
+ settingsSnapshot.remote.enableWebTerminal,
+ settingsSnapshot.remote.enableWebGit,
+ settingsSnapshot.remote.enableWebTunnels,
+ ].filter(Boolean).length
+ : 0;
+ const latestTerminal = runningTerminals[0] ?? terminals[0];
+ const activeWorkspaceName =
+ activeWorkspaceProject?.name?.trim() || (activeWorkspaceProject?.path ? basename(activeWorkspaceProject.path) : "--");
+ const activeWorkspaceHint = activeWorkspaceProject?.path
+ ? truncateMiddle(activeWorkspaceProject.path, 48)
+ : settingsSnapshot
+ ? "未配置工作区"
+ : "等待 settings.get";
+ const loadedConversationRows = history?.conversations.length ?? 0;
+ const maxWorkdirCount = Math.max(1, ...workdirs.map((item) => item.conversationCount || 0));
+ const activeSubsystemCount =
+ Number(status?.online === true) +
+ Number(status?.chat_runtime_ready === true) +
+ Number(activeProviders.length > 0) +
+ Number(runningTerminals.length > 0) +
+ Number(activeTunnels.length > 0) +
+ Number(enabledMcpCount > 0) +
+ Number(enabledCronCount > 0) +
+ Number(settingsSnapshot?.skills.enabled === true);
+ const integrityScore = Math.min(
+ 100,
+ (status?.online ? 34 : 0) +
+ (isFreshHeartbeat ? 18 : 0) +
+ (status?.chat_runtime_ready ? 18 : 0) +
+ (settingsSnapshot ? 12 : 0) +
+ (activeProviders.length > 0 ? 10 : 0) +
+ (snapshot.error ? 0 : 8),
+ );
+ const activeLoadValues = [
+ liveCounters.tokenChunks,
+ liveCounters.thinking,
+ liveCounters.toolCalls + liveCounters.toolResults,
+ liveCounters.searches,
+ liveCounters.errors,
+ ];
+ const maxLoadValue = Math.max(1, ...activeLoadValues);
+ const toLoadWidth = (value: number) => Math.max(value > 0 ? 12 : 4, Math.round((value / maxLoadValue) * 100));
+ const throughputSegments: LoadSegment[] = [
+ { label: "Token Chunks", value: liveCounters.tokenChunks, unit: "chunks", width: toLoadWidth(liveCounters.tokenChunks), tone: "cyan" },
+ { label: "Reasoning", value: liveCounters.thinking, unit: "events", width: toLoadWidth(liveCounters.thinking), tone: "violet" },
+ { label: "Tool I/O", value: liveCounters.toolCalls + liveCounters.toolResults, unit: "events", width: toLoadWidth(liveCounters.toolCalls + liveCounters.toolResults), tone: "amber" },
+ { label: "Web Search", value: liveCounters.searches, unit: "events", width: toLoadWidth(liveCounters.searches), tone: "emerald" },
+ { label: "Errors", value: liveCounters.errors, unit: "events", width: toLoadWidth(liveCounters.errors), tone: "rose" },
+ ];
+
+ const runtimeFacts: FactItem[] = [
+ {
+ label: "Runtime State",
+ value: runtimeState,
+ note: `chat_runtime_ready=${formatBooleanFlag(status?.chat_runtime_ready)}`,
+ tone: status?.online ? "emerald" : "rose",
+ },
+ {
+ label: "Active Runs",
+ value: String(runtimeActiveRunCount),
+ unit: "runs",
+ note: status?.runtime_active_run_count !== undefined ? "runtime_active_run_count" : "history running fallback",
+ tone: runtimeActiveRunCount > 0 ? "violet" : "slate",
+ },
+ {
+ label: "Worker ID",
+ value: status?.runtime_worker_id ? truncateMiddle(status.runtime_worker_id, 22) : "--",
+ note: `runtime_visible=${formatBooleanFlag(status?.runtime_visible)}`,
+ },
+ {
+ label: "Runtime Heartbeat Age",
+ value: status?.runtime_last_heartbeat ? formatDuration(runtimeHeartbeatAgeMs) : "--",
+ unit: "age",
+ note: "runtime_last_heartbeat",
+ },
+ ];
+
+ const modelFacts: FactItem[] = [
+ {
+ label: "Selected Model",
+ value: selectedModel?.model ? truncateMiddle(selectedModel.model, 30) : "--",
+ note: selectedProviderName,
+ tone: selectedModel ? "violet" : "slate",
+ },
+ {
+ label: "Provider",
+ value: truncateMiddle(selectedProviderName, 24),
+ note: selectedProviderType,
+ },
+ {
+ label: "Context Window",
+ value: selectedModelConfig?.contextWindow ? compactNumber(selectedModelConfig.contextWindow) : "--",
+ unit: "tokens",
+ note: selectedModelConfig?.maxOutputToken ? `${compactNumber(selectedModelConfig.maxOutputToken)} max output tokens` : "model config",
+ },
+ {
+ label: "Reasoning Mode",
+ value: settingsSnapshot?.chatRuntimeControls.reasoning ?? "--",
+ note: `thinking=${formatBooleanFlag(settingsSnapshot?.chatRuntimeControls.thinkingEnabled)} · web_search=${formatBooleanFlag(settingsSnapshot?.chatRuntimeControls.nativeWebSearchEnabled)}`,
+ },
+ ];
+
+ const fabricFacts: FactItem[] = [
+ {
+ label: "MCP Servers",
+ value: settingsSnapshot ? `${enabledMcpCount}/${settingsSnapshot.mcp.servers.length}` : "--",
+ unit: "enabled/total",
+ note: `selected ${settingsSnapshot?.mcp.selected.length ?? "--"}`,
+ tone: enabledMcpCount > 0 ? "cyan" : "slate",
+ },
+ {
+ label: "Cron Tasks",
+ value: settingsSnapshot ? `${enabledCronCount}/${settingsSnapshot.cron.length}` : "--",
+ unit: "enabled/total",
+ tone: enabledCronCount > 0 ? "amber" : "slate",
+ },
+ {
+ label: "Hooks",
+ value: settingsSnapshot ? `${enabledHookCount}/${settingsSnapshot.hooks.length}` : "--",
+ unit: "enabled/total",
+ },
+ {
+ label: "Skills",
+ value: settingsSnapshot?.skills.enabled ? String(selectedSkillCount) : "OFF",
+ unit: settingsSnapshot?.skills.enabled ? "selected" : undefined,
+ note: `skills.enabled=${formatBooleanFlag(settingsSnapshot?.skills.enabled)}`,
+ },
+ ];
+
+ const telemetryFacts: FactItem[] = [
+ {
+ label: "Derived Integrity",
+ value: String(integrityScore),
+ unit: "%",
+ note: `${activeSubsystemCount}/8 observed subsystems`,
+ tone: integrityScore >= 70 ? "emerald" : integrityScore >= 40 ? "amber" : "rose",
+ },
+ {
+ label: "Event Rate",
+ value: eventsPerMinute.toFixed(1),
+ unit: "events/min",
+ note: "live WebSocket events since page open",
+ tone: liveCounters.events > 0 ? "cyan" : "slate",
+ },
+ {
+ label: "Text Output",
+ value: compactNumber(liveCounters.tokenChars),
+ unit: "chars",
+ note: `${compactNumber(liveCounters.tokenChunks)} token chunks`,
+ tone: "violet",
+ },
+ {
+ label: "Tool Traffic",
+ value: compactNumber(liveCounters.toolCalls + liveCounters.toolResults),
+ unit: "events",
+ note: `${compactNumber(liveCounters.errors)} error events`,
+ tone: liveCounters.errors > 0 ? "rose" : "amber",
+ },
+ ];
+
+ const metrics: MetricCard[] = [
+ {
+ label: "Agent Link",
+ value: status?.online ? (isFreshHeartbeat ? "LIVE" : "WARM") : "OFFLINE",
+ unit: "state",
+ detail: status?.online ? `uptime ${formatDuration(uptimeMs)} · heartbeat age ${formatDuration(heartbeatAgeMs)}` : statusError || "desktop agent not connected",
+ tone: status?.online ? "emerald" : "rose",
+ icon: status?.online ? Wifi : WifiOff,
+ },
+ {
+ label: "Runtime Runs",
+ value: String(runtimeActiveRunCount),
+ unit: "runs",
+ detail: `${runningConversations.length} running conversations in history snapshot`,
+ tone: runtimeActiveRunCount > 0 ? "violet" : "slate",
+ icon: Radio,
+ },
+ {
+ label: "History Index",
+ value: compactNumber(history?.total_count ?? 0),
+ unit: "conversations",
+ detail: `loaded ${loadedConversationRows} rows · today sample ${todayConversationCount} · ${compactNumber(messageSampleCount)} msgs in loaded rows`,
+ tone: "cyan",
+ icon: History,
+ },
+ {
+ label: "Public Tunnels",
+ value: String(activeTunnels.length),
+ unit: "active",
+ detail: `${tunnels.length} tunnel records · ${totalTunnelConnections} active connections`,
+ tone: activeTunnels.length > 0 ? "amber" : "slate",
+ icon: Cloud,
+ },
+ {
+ label: "Web Terminals",
+ value: String(runningTerminals.length),
+ unit: "running sessions",
+ detail: `${terminals.length} total sessions · ${latestTerminal ? basename(latestTerminal.cwd) : "no cwd"}`,
+ tone: runningTerminals.length > 0 ? "emerald" : "slate",
+ icon: Terminal,
+ },
+ {
+ label: "Active Models",
+ value: String(activeModelCount),
+ unit: "models",
+ detail: `${activeProviders.length} active providers · ${configuredProviderCount} provider keys configured`,
+ tone: "violet",
+ icon: Sparkles,
+ },
+ ];
+
+ if (!token) {
+ return (
+ {
+ setLoginToken(nextToken);
+ if (authError) {
+ setAuthError(null);
+ }
+ }}
+ onSubmit={submit}
+ />
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {snapshot.error && (
+
+ )}
+
+
+
+
+
+
+
+
+
Live Telemetry
+
系统数据雷达
+
+
{eventsPerMinute.toFixed(1)} events/min
+
+
+
+ {metrics.map((metric) => (
+
+ ))}
+
+
+
+
+
+
+
+
+ {runtimeActiveRunCount}
+ active runs
+
+ {throughputSegments.map((segment, index) => (
+
+ ))}
+
+
+
+
+ Stream Load
+ {compactNumber(liveCounters.events)} events
+
+ {throughputSegments.map((segment) => (
+
+
{segment.label}
+
+
+
+
+ {compactNumber(segment.value)} {segment.unit}
+
+
+ ))}
+
+
+
+
+
+
+
+
+ {recentEvents.length === 0 ? (
+
我还没收到实时事件;当 token、thinking 或 tool_call 抵达时,这里会亮起来。
+ ) : (
+ recentEvents.slice(0, 6).map((event) => (
+
+
+
+
+ {event.title}
+
+
+
{event.detail}
+ {(event.conversationId || event.workdir) && (
+
+ {event.workdir ? basename(event.workdir) : truncateMiddle(event.conversationId ?? "", 18)}
+
+ )}
+
+
+ ))
+ )}
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/crates/agent-gateway/web/src/styles.css b/crates/agent-gateway/web/src/styles.css
index 9534497c7..c4f6054de 100644
--- a/crates/agent-gateway/web/src/styles.css
+++ b/crates/agent-gateway/web/src/styles.css
@@ -2967,3 +2967,1133 @@ html[data-liveagent-webui="gateway"] .external-link-modal-overlay[data-state="cl
margin-top: 12px;
}
}
+
+.status-board-shell {
+ --status-bg: #030712;
+ --status-panel: rgba(6, 14, 31, 0.72);
+ --status-panel-strong: rgba(8, 20, 42, 0.88);
+ --status-line: rgba(125, 249, 255, 0.2);
+ --status-text: rgba(241, 248, 255, 0.94);
+ --status-muted: rgba(178, 202, 230, 0.62);
+ --status-cyan: 72, 235, 255;
+ --status-violet: 169, 119, 255;
+ --status-rose: 255, 78, 142;
+ --status-amber: 255, 196, 87;
+ --status-emerald: 68, 255, 196;
+ position: relative;
+ display: grid;
+ width: 100vw;
+ height: 100dvh;
+ min-height: 0;
+ place-items: center;
+ overflow: hidden;
+ background:
+ radial-gradient(circle at 16% 15%, rgba(var(--status-cyan), 0.18), transparent 25%),
+ radial-gradient(circle at 76% 18%, rgba(var(--status-violet), 0.18), transparent 27%),
+ radial-gradient(circle at 56% 85%, rgba(var(--status-emerald), 0.1), transparent 30%),
+ linear-gradient(135deg, #02040c 0%, #06101f 48%, #030712 100%);
+ color: var(--status-text);
+ font-family:
+ "OpenAI Sans Semibold Web",
+ "PingFang SC",
+ "Microsoft YaHei",
+ sans-serif;
+}
+
+.status-board-shell::before,
+.status-board-shell::after {
+ content: "";
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+}
+
+.status-board-shell::before {
+ background-image:
+ linear-gradient(rgba(var(--status-cyan), 0.08) 1px, transparent 1px),
+ linear-gradient(90deg, rgba(var(--status-cyan), 0.06) 1px, transparent 1px),
+ radial-gradient(circle at 50% 50%, transparent 0 44%, rgba(var(--status-cyan), 0.08) 45%, transparent 46%);
+ background-size: 48px 48px, 48px 48px, 620px 620px;
+ mask-image: radial-gradient(circle at 50% 50%, rgba(0, 0, 0, 0.94), transparent 78%);
+ opacity: 0.9;
+}
+
+.status-board-shell::after {
+ background: linear-gradient(180deg, transparent 0, rgba(255, 255, 255, 0.055) 50%, transparent 100%);
+ background-size: 100% 7px;
+ mix-blend-mode: screen;
+ opacity: 0.22;
+ animation: status-board-scanline 6s linear infinite;
+}
+
+.status-board-aurora,
+.status-board-noise,
+.status-board-orb {
+ position: absolute;
+ pointer-events: none;
+}
+
+.status-board-aurora {
+ inset: -30% -18%;
+ background:
+ conic-gradient(from 90deg at 50% 50%, transparent, rgba(var(--status-cyan), 0.16), transparent, rgba(var(--status-violet), 0.16), transparent, rgba(var(--status-emerald), 0.12), transparent),
+ radial-gradient(circle at 48% 46%, rgba(255, 255, 255, 0.08), transparent 28%);
+ filter: blur(28px);
+ opacity: 0.95;
+ animation: status-board-aurora-drift 22s ease-in-out infinite alternate;
+}
+
+.status-board-noise {
+ inset: 0;
+ opacity: 0.26;
+ background-image:
+ radial-gradient(circle at 12% 26%, rgba(255, 255, 255, 0.2) 0 1px, transparent 1px),
+ radial-gradient(circle at 72% 62%, rgba(var(--status-cyan), 0.18) 0 1px, transparent 1px),
+ radial-gradient(circle at 46% 82%, rgba(var(--status-violet), 0.16) 0 1px, transparent 1px);
+ background-size: 37px 37px, 61px 61px, 89px 89px;
+ mix-blend-mode: screen;
+}
+
+.status-board-orb {
+ border-radius: 999px;
+ filter: blur(2px);
+ opacity: 0.8;
+ animation: status-board-float 12s ease-in-out infinite alternate;
+}
+
+.status-board-orb--a {
+ top: 9%;
+ left: 4%;
+ width: 220px;
+ height: 220px;
+ background: radial-gradient(circle, rgba(var(--status-cyan), 0.3), transparent 64%);
+}
+
+.status-board-orb--b {
+ right: 8%;
+ top: 11%;
+ width: 280px;
+ height: 280px;
+ background: radial-gradient(circle, rgba(var(--status-violet), 0.26), transparent 66%);
+ animation-delay: -4s;
+}
+
+.status-board-orb--c {
+ right: 18%;
+ bottom: -4%;
+ width: 340px;
+ height: 340px;
+ background: radial-gradient(circle, rgba(var(--status-emerald), 0.16), transparent 68%);
+ animation-delay: -7s;
+}
+
+.status-board-stage {
+ position: relative;
+ z-index: 1;
+ box-sizing: border-box;
+ display: grid;
+ grid-template-rows: 58px minmax(0, 1fr) 30px;
+ gap: 12px;
+ width: min(100vw, 1912px);
+ height: min(100dvh, 948px);
+ min-height: 0;
+ padding: 14px 18px 12px;
+}
+
+.status-board-header,
+.status-board-actions,
+.status-board-brand,
+.status-board-brand > div,
+.status-board-section-head,
+.status-board-footer,
+.status-board-pill,
+.status-board-action-button,
+.status-board-link-button {
+ display: flex;
+ align-items: center;
+}
+
+.status-board-command {
+ position: relative;
+ justify-content: space-between;
+ gap: 16px;
+ border: 1px solid rgba(var(--status-cyan), 0.2);
+ border-radius: 20px;
+ padding: 9px 12px;
+ background:
+ linear-gradient(90deg, rgba(var(--status-cyan), 0.09), transparent 28%, rgba(var(--status-violet), 0.09)),
+ rgba(4, 11, 24, 0.72);
+ box-shadow:
+ 0 18px 60px rgba(0, 0, 0, 0.26),
+ inset 0 1px 0 rgba(255, 255, 255, 0.08),
+ inset 0 0 48px rgba(var(--status-cyan), 0.05);
+ backdrop-filter: blur(22px);
+}
+
+.status-board-command::before {
+ content: "";
+ position: absolute;
+ right: 22%;
+ bottom: -1px;
+ left: 22%;
+ height: 1px;
+ background: linear-gradient(90deg, transparent, rgba(var(--status-cyan), 0.82), transparent);
+ box-shadow: 0 0 18px rgba(var(--status-cyan), 0.72);
+}
+
+.status-board-brand {
+ min-width: 0;
+ gap: 12px;
+}
+
+.status-board-brand > div {
+ align-items: flex-start;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.status-board-logo,
+.status-board-metric-icon {
+ display: grid;
+ place-items: center;
+ border: 1px solid rgba(var(--status-cyan), 0.3);
+ background:
+ linear-gradient(135deg, rgba(var(--status-cyan), 0.18), rgba(var(--status-violet), 0.14)),
+ rgba(255, 255, 255, 0.06);
+ box-shadow: 0 0 28px rgba(var(--status-cyan), 0.2), inset 0 0 22px rgba(255, 255, 255, 0.08);
+ backdrop-filter: blur(18px);
+}
+
+.status-board-logo {
+ width: 38px;
+ height: 38px;
+ border-radius: 14px;
+ color: rgb(var(--status-cyan));
+}
+
+.status-board-logo--hot {
+ animation: status-board-logo-pulse 2.8s ease-in-out infinite;
+}
+
+.status-board-label,
+.status-board-brand p {
+ margin: 0;
+ color: rgba(186, 216, 246, 0.6);
+ font-size: 10px;
+ letter-spacing: 0.18em;
+ text-transform: uppercase;
+}
+
+.status-board-brand h1,
+.status-board-section-head h3 {
+ margin: 0;
+ color: rgba(255, 255, 255, 0.96);
+ letter-spacing: -0.035em;
+}
+
+.status-board-brand h1 {
+ overflow: hidden;
+ max-width: 360px;
+ font-size: 23px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.status-board-command-center {
+ position: absolute;
+ left: 50%;
+ display: grid;
+ min-width: 360px;
+ justify-items: center;
+ transform: translateX(-50%);
+ text-align: center;
+}
+
+.status-board-command-center span,
+.status-board-command-center em {
+ color: rgba(181, 213, 244, 0.58);
+ font-size: 10px;
+ font-style: normal;
+ letter-spacing: 0.2em;
+ text-transform: uppercase;
+}
+
+.status-board-command-center strong {
+ color: rgba(255, 255, 255, 0.98);
+ font-size: 25px;
+ letter-spacing: 0.05em;
+ line-height: 1;
+ text-shadow: 0 0 24px rgba(var(--status-cyan), 0.42);
+}
+
+.status-board-actions {
+ flex-wrap: nowrap;
+ justify-content: flex-end;
+ gap: 8px;
+}
+
+.status-board-pill,
+.status-board-action-button,
+.status-board-link-button {
+ height: 33px;
+ gap: 7px;
+ border: 1px solid rgba(var(--status-cyan), 0.2);
+ border-radius: 999px;
+ padding: 0 12px;
+ background: rgba(255, 255, 255, 0.055);
+ color: rgba(233, 245, 255, 0.84);
+ font-size: 11px;
+ text-decoration: none;
+ text-transform: uppercase;
+ white-space: nowrap;
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
+ backdrop-filter: blur(16px);
+ transition: transform 160ms ease, border-color 160ms ease, background-color 160ms ease;
+}
+
+.status-board-action-button:hover,
+.status-board-link-button:hover {
+ transform: translateY(-1px);
+ border-color: rgba(var(--status-cyan), 0.48);
+ background: rgba(var(--status-cyan), 0.1);
+ color: #ffffff;
+}
+
+.status-board-pill-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 999px;
+ background: rgb(var(--status-rose));
+ box-shadow: 0 0 18px rgba(var(--status-rose), 0.82);
+}
+
+.status-board-pill--online .status-board-pill-dot {
+ background: rgb(var(--status-emerald));
+ box-shadow: 0 0 20px rgba(var(--status-emerald), 0.86);
+ animation: status-board-pulse 1.5s ease-in-out infinite;
+}
+
+.status-board-cockpit {
+ display: grid;
+ grid-template-columns: 360px minmax(0, 1fr) 390px;
+ gap: 12px;
+ min-height: 0;
+}
+
+.status-board-left-rail,
+.status-board-center-stack,
+.status-board-right-rail {
+ display: grid;
+ min-height: 0;
+ gap: 12px;
+}
+
+.status-board-left-rail {
+ grid-template-rows: 1.04fr 0.96fr;
+}
+
+.status-board-center-stack {
+ grid-template-rows: minmax(0, 1fr) 206px;
+}
+
+.status-board-right-rail {
+ grid-template-rows: 0.96fr 1.04fr;
+}
+
+.status-board-card {
+ position: relative;
+ overflow: hidden;
+ border: 1px solid rgba(var(--status-cyan), 0.16);
+ border-radius: 22px;
+ background:
+ linear-gradient(135deg, rgba(255, 255, 255, 0.09), rgba(255, 255, 255, 0.026)),
+ var(--status-panel);
+ box-shadow:
+ 0 24px 70px rgba(0, 0, 0, 0.32),
+ inset 0 1px 0 rgba(255, 255, 255, 0.08),
+ inset 0 0 56px rgba(var(--status-cyan), 0.035);
+ backdrop-filter: blur(22px);
+}
+
+.status-board-card::before,
+.status-board-card::after {
+ content: "";
+ position: absolute;
+ pointer-events: none;
+}
+
+.status-board-card::before {
+ inset: 0;
+ background:
+ linear-gradient(125deg, rgba(255, 255, 255, 0.13), transparent 30%, rgba(var(--status-cyan), 0.045)),
+ linear-gradient(90deg, transparent, rgba(var(--status-cyan), 0.1), transparent);
+ opacity: 0.72;
+}
+
+.status-board-card::after {
+ top: 0;
+ right: 18px;
+ left: 18px;
+ height: 1px;
+ background: linear-gradient(90deg, transparent, rgba(var(--status-cyan), 0.62), transparent);
+ box-shadow: 0 0 18px rgba(var(--status-cyan), 0.48);
+}
+
+.status-board-card > * {
+ position: relative;
+ z-index: 1;
+}
+
+.status-board-panel {
+ display: flex;
+ min-height: 0;
+ flex-direction: column;
+ padding: 14px;
+}
+
+.status-board-section-head {
+ flex: 0 0 auto;
+ justify-content: space-between;
+ gap: 10px;
+ margin-bottom: 10px;
+}
+
+.status-board-section-head h3 {
+ margin-top: 2px;
+ font-size: 18px;
+}
+
+.status-board-section-head > span,
+.status-board-section-head > svg {
+ color: rgba(191, 225, 255, 0.68);
+ font-size: 11px;
+}
+
+.status-board-reactor-panel,
+.status-board-fabric-panel,
+.status-board-model-panel,
+.status-board-workspace-panel,
+.status-board-stream-panel {
+ min-height: 0;
+}
+
+.status-board-reactor-core {
+ display: grid;
+ grid-template-columns: 154px minmax(0, 1fr);
+ align-items: center;
+ gap: 14px;
+ margin-bottom: 12px;
+}
+
+.status-board-reactor {
+ position: relative;
+ display: grid;
+ width: 154px;
+ height: 154px;
+ place-items: center;
+ border-radius: 999px;
+ box-shadow: 0 0 46px rgba(var(--status-cyan), 0.22), inset 0 0 34px rgba(0, 0, 0, 0.58);
+}
+
+.status-board-reactor::before {
+ content: "";
+ position: absolute;
+ inset: 12px;
+ border-radius: inherit;
+ background:
+ radial-gradient(circle, rgba(255, 255, 255, 0.12), transparent 42%),
+ #071226;
+ box-shadow: inset 0 0 28px rgba(var(--status-cyan), 0.12);
+}
+
+.status-board-reactor-ring {
+ position: absolute;
+ border-radius: inherit;
+ border: 1px solid rgba(var(--status-cyan), 0.24);
+}
+
+.status-board-reactor-ring--a {
+ inset: -8px;
+ animation: status-board-ring-spin 9s linear infinite;
+}
+
+.status-board-reactor-ring--b {
+ inset: 28px;
+ border-color: rgba(var(--status-violet), 0.28);
+ animation: status-board-ring-spin 7s linear infinite reverse;
+}
+
+.status-board-reactor-number {
+ position: relative;
+ z-index: 1;
+ display: grid;
+ justify-items: center;
+}
+
+.status-board-reactor-number strong {
+ color: #ffffff;
+ font-size: 46px;
+ line-height: 0.92;
+ letter-spacing: -0.06em;
+ text-shadow: 0 0 30px rgba(var(--status-cyan), 0.5);
+}
+
+.status-board-reactor-number span,
+.status-board-reactor-copy span,
+.status-board-reactor-copy em {
+ color: var(--status-muted);
+ font-size: 10px;
+ font-style: normal;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+}
+
+.status-board-reactor-copy {
+ min-width: 0;
+}
+
+.status-board-reactor-copy strong {
+ display: block;
+ overflow: hidden;
+ margin: 7px 0;
+ color: rgba(255, 255, 255, 0.94);
+ font-size: 17px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.status-board-fact-list {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 7px;
+}
+
+.status-board-radar-panel .status-board-fact-list {
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ margin-top: 10px;
+}
+
+.status-board-fact {
+ --status-board-tone: 191, 210, 232;
+ min-width: 0;
+ border: 1px solid rgba(var(--status-board-tone), 0.15);
+ border-radius: 13px;
+ padding: 8px;
+ background:
+ linear-gradient(135deg, rgba(var(--status-board-tone), 0.08), transparent),
+ rgba(255, 255, 255, 0.045);
+}
+
+.status-board-fact--cyan,
+.status-board-tone-cyan {
+ --status-board-tone: var(--status-cyan);
+}
+
+.status-board-fact--violet,
+.status-board-tone-violet {
+ --status-board-tone: var(--status-violet);
+}
+
+.status-board-fact--rose,
+.status-board-tone-rose {
+ --status-board-tone: var(--status-rose);
+}
+
+.status-board-fact--amber,
+.status-board-tone-amber {
+ --status-board-tone: var(--status-amber);
+}
+
+.status-board-fact--emerald,
+.status-board-tone-emerald {
+ --status-board-tone: var(--status-emerald);
+}
+
+.status-board-fact--slate,
+.status-board-tone-slate {
+ --status-board-tone: 191, 210, 232;
+}
+
+.status-board-fact span,
+.status-board-active-workspace span {
+ display: block;
+ color: rgba(192, 220, 248, 0.56);
+ font-size: 9px;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+}
+
+.status-board-fact strong,
+.status-board-active-workspace strong {
+ display: inline-block;
+ overflow: hidden;
+ max-width: 100%;
+ margin-top: 3px;
+ color: rgba(255, 255, 255, 0.94);
+ font-size: 13px;
+ line-height: 1.12;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.status-board-fact b {
+ display: inline-block;
+ margin-left: 5px;
+ color: rgba(var(--status-board-tone), 0.9);
+ font-size: 9px;
+ font-weight: 500;
+ text-transform: uppercase;
+}
+
+.status-board-fact em,
+.status-board-active-workspace em {
+ display: block;
+ overflow: hidden;
+ margin-top: 3px;
+ color: rgba(186, 213, 242, 0.58);
+ font-size: 10px;
+ font-style: normal;
+ line-height: 1.22;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.status-board-mini-grid,
+.status-board-metrics-grid {
+ display: grid;
+ gap: 8px;
+}
+
+.status-board-mini-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ margin-top: 10px;
+}
+
+.status-board-mini-card,
+.status-board-metric {
+ min-width: 0;
+ border: 1px solid rgba(var(--status-cyan), 0.12);
+ border-radius: 14px;
+ background: rgba(255, 255, 255, 0.045);
+}
+
+.status-board-mini-card {
+ padding: 10px;
+ color: rgba(226, 242, 255, 0.7);
+}
+
+.status-board-mini-card svg {
+ color: rgba(var(--status-cyan), 0.84);
+}
+
+.status-board-mini-card span,
+.status-board-metric em,
+.status-board-metric span {
+ color: rgba(190, 218, 246, 0.58);
+ font-size: 10px;
+ font-style: normal;
+}
+
+.status-board-mini-card strong {
+ display: block;
+ margin-top: 5px;
+ color: #ffffff;
+ font-size: 23px;
+ line-height: 1;
+}
+
+.status-board-metrics-grid {
+ grid-template-columns: repeat(6, minmax(0, 1fr));
+ flex: 0 0 auto;
+}
+
+.status-board-metric {
+ display: grid;
+ grid-template-columns: 34px minmax(0, 1fr);
+ align-items: center;
+ gap: 9px;
+ min-height: 82px;
+ padding: 10px;
+}
+
+.status-board-metric-icon {
+ width: 34px;
+ height: 34px;
+ border-radius: 12px;
+ color: rgb(var(--status-board-tone));
+ background: rgba(var(--status-board-tone), 0.11);
+}
+
+.status-board-metric strong {
+ display: inline-block;
+ margin-right: 5px;
+ color: #ffffff;
+ font-size: 22px;
+ line-height: 1;
+ letter-spacing: -0.04em;
+}
+
+.status-board-metric em {
+ text-transform: uppercase;
+}
+
+.status-board-metric span {
+ display: block;
+ overflow: hidden;
+ margin-top: 4px;
+ line-height: 1.25;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.status-board-radar-deck {
+ display: grid;
+ grid-template-columns: minmax(410px, 0.9fr) minmax(0, 1.1fr);
+ align-items: center;
+ gap: 14px;
+ min-height: 0;
+ flex: 1 1 auto;
+ margin-top: 12px;
+}
+
+.status-board-radar-screen {
+ position: relative;
+ display: grid;
+ width: min(42vh, 410px);
+ height: min(42vh, 410px);
+ place-items: center;
+ justify-self: center;
+ overflow: hidden;
+ border: 1px solid rgba(var(--status-cyan), 0.22);
+ border-radius: 999px;
+ background:
+ radial-gradient(circle, rgba(var(--status-cyan), 0.13), transparent 7%),
+ repeating-radial-gradient(circle, transparent 0 54px, rgba(var(--status-cyan), 0.13) 55px 56px),
+ radial-gradient(circle at center, rgba(7, 17, 33, 0.4), rgba(2, 7, 15, 0.92));
+ box-shadow:
+ 0 0 70px rgba(var(--status-cyan), 0.16),
+ inset 0 0 70px rgba(var(--status-cyan), 0.08);
+}
+
+.status-board-radar-grid,
+.status-board-radar-sweep {
+ position: absolute;
+ inset: 0;
+ border-radius: inherit;
+}
+
+.status-board-radar-grid {
+ background:
+ linear-gradient(rgba(var(--status-cyan), 0.12) 1px, transparent 1px),
+ linear-gradient(90deg, rgba(var(--status-cyan), 0.12) 1px, transparent 1px);
+ background-size: 46px 46px;
+ mask-image: radial-gradient(circle, black 0 68%, transparent 69%);
+ opacity: 0.42;
+}
+
+.status-board-radar-sweep {
+ background: conic-gradient(from 0deg, rgba(var(--status-cyan), 0.45), rgba(var(--status-cyan), 0.06) 42deg, transparent 72deg 360deg);
+ mix-blend-mode: screen;
+ opacity: 0.82;
+ animation: status-board-radar-sweep 3.4s linear infinite;
+}
+
+.status-board-radar-core {
+ position: relative;
+ z-index: 2;
+ display: grid;
+ width: 126px;
+ height: 126px;
+ place-items: center;
+ border: 1px solid rgba(var(--status-cyan), 0.26);
+ border-radius: 999px;
+ background: rgba(4, 13, 28, 0.84);
+ box-shadow: 0 0 40px rgba(var(--status-cyan), 0.23), inset 0 0 26px rgba(var(--status-cyan), 0.08);
+}
+
+.status-board-radar-core svg {
+ color: rgba(var(--status-cyan), 0.9);
+}
+
+.status-board-radar-core strong {
+ color: #ffffff;
+ font-size: 36px;
+ line-height: 0.8;
+}
+
+.status-board-radar-core span {
+ color: var(--status-muted);
+ font-size: 9px;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+}
+
+.status-board-radar-node {
+ --status-board-tone: var(--status-cyan);
+ position: absolute;
+ z-index: 3;
+ width: 10px;
+ height: 10px;
+ border-radius: 999px;
+ background: rgb(var(--status-board-tone));
+ box-shadow: 0 0 20px rgba(var(--status-board-tone), 0.82), 0 0 46px rgba(var(--status-board-tone), 0.38);
+ animation: status-board-node-pulse 1.8s ease-in-out infinite;
+}
+
+.status-board-throughput {
+ min-width: 0;
+}
+
+.status-board-throughput-head {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ margin-bottom: 10px;
+}
+
+.status-board-throughput-head span {
+ color: var(--status-muted);
+ font-size: 11px;
+ letter-spacing: 0.18em;
+ text-transform: uppercase;
+}
+
+.status-board-throughput-head strong {
+ color: rgba(255, 255, 255, 0.95);
+ font-size: 18px;
+}
+
+.status-board-load-row {
+ --status-board-tone: var(--status-cyan);
+ display: grid;
+ grid-template-columns: 112px minmax(0, 1fr) 92px;
+ align-items: center;
+ gap: 9px;
+ margin-bottom: 8px;
+}
+
+.status-board-load-row span,
+.status-board-load-row em {
+ color: rgba(211, 232, 255, 0.66);
+ font-size: 11px;
+ font-style: normal;
+}
+
+.status-board-load-row em {
+ text-align: right;
+}
+
+.status-board-load-row div {
+ height: 10px;
+ overflow: hidden;
+ border: 1px solid rgba(var(--status-board-tone), 0.14);
+ border-radius: 999px;
+ background: rgba(255, 255, 255, 0.05);
+}
+
+.status-board-load-row i {
+ display: block;
+ height: 100%;
+ border-radius: inherit;
+ background: linear-gradient(90deg, rgba(var(--status-board-tone), 0.25), rgba(var(--status-board-tone), 0.95));
+ box-shadow: 0 0 16px rgba(var(--status-board-tone), 0.44);
+ animation: status-board-load-breathe 1.8s ease-in-out infinite alternate;
+}
+
+.status-board-stream-panel {
+ padding-bottom: 12px;
+}
+
+.status-board-event-list,
+.status-board-running-list,
+.status-board-workdir-list {
+ display: flex;
+ min-height: 0;
+ flex: 1 1 auto;
+ flex-direction: column;
+ gap: 7px;
+ overflow: hidden;
+}
+
+.status-board-event,
+.status-board-running-item,
+.status-board-workdir,
+.status-board-empty,
+.status-board-active-workspace {
+ border: 1px solid rgba(var(--status-board-tone, 191, 210, 232), 0.14);
+ border-radius: 14px;
+ padding: 8px 9px;
+ background: rgba(var(--status-board-tone, 191, 210, 232), 0.055);
+}
+
+.status-board-event {
+ --status-board-tone: 191, 210, 232;
+ display: grid;
+ grid-template-columns: auto minmax(0, 1fr);
+ gap: 9px;
+}
+
+.status-board-event-dot,
+.status-board-running-dot {
+ width: 8px;
+ height: 8px;
+ margin-top: 5px;
+ border-radius: 999px;
+ background: rgb(var(--status-board-tone));
+ box-shadow: 0 0 16px rgba(var(--status-board-tone), 0.76);
+ animation: status-board-pulse 1.8s ease-in-out infinite;
+}
+
+.status-board-running-dot {
+ --status-board-tone: var(--status-violet);
+ flex: 0 0 auto;
+}
+
+.status-board-event-title-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+}
+
+.status-board-event-title-row strong,
+.status-board-running-item strong,
+.status-board-workdir strong {
+ display: block;
+ overflow: hidden;
+ color: rgba(255, 255, 255, 0.92);
+ font-size: 12px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.status-board-event-title-row time,
+.status-board-event p,
+.status-board-event-meta,
+.status-board-running-item span,
+.status-board-workdir span,
+.status-board-empty,
+.status-board-workdir em {
+ color: rgba(190, 219, 248, 0.58);
+ font-size: 10px;
+ font-style: normal;
+ line-height: 1.25;
+}
+
+.status-board-event p {
+ display: -webkit-box;
+ overflow: hidden;
+ margin: 3px 0 0;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 1;
+}
+
+.status-board-event-meta {
+ display: inline-flex;
+ margin-top: 4px;
+ border-radius: 999px;
+ padding: 2px 6px;
+ background: rgba(255, 255, 255, 0.06);
+}
+
+.status-board-running-item {
+ display: grid;
+ grid-template-columns: auto minmax(0, 1fr);
+ gap: 9px;
+}
+
+.status-board-active-workspace {
+ --status-board-tone: var(--status-cyan);
+ flex: 0 0 auto;
+ margin-bottom: 8px;
+}
+
+.status-board-workdir {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) 86px 82px;
+ align-items: center;
+ gap: 9px;
+}
+
+.status-board-workdir-meter {
+ height: 8px;
+ overflow: hidden;
+ border: 1px solid rgba(var(--status-cyan), 0.12);
+ border-radius: 999px;
+ background: rgba(255, 255, 255, 0.05);
+}
+
+.status-board-workdir-meter span {
+ display: block;
+ height: 100%;
+ border-radius: inherit;
+ background: linear-gradient(90deg, rgb(var(--status-cyan)), rgb(var(--status-violet)));
+ box-shadow: 0 0 16px rgba(var(--status-cyan), 0.34);
+}
+
+.status-board-workdir em {
+ text-align: right;
+}
+
+.status-board-warning {
+ position: absolute;
+ top: 82px;
+ right: 24px;
+ z-index: 6;
+ display: flex;
+ max-width: 540px;
+ align-items: center;
+ gap: 9px;
+ border: 1px solid rgba(var(--status-amber), 0.3);
+ border-radius: 16px;
+ padding: 9px 12px;
+ background: rgba(56, 35, 4, 0.5);
+ color: rgba(255, 232, 190, 0.92);
+ font-size: 12px;
+ box-shadow: 0 0 32px rgba(var(--status-amber), 0.16);
+ backdrop-filter: blur(18px);
+}
+
+.status-board-footer {
+ flex-wrap: nowrap;
+ justify-content: space-between;
+ gap: 10px;
+ overflow: hidden;
+ border: 1px solid rgba(var(--status-cyan), 0.14);
+ border-radius: 14px;
+ padding: 6px 10px;
+ background: rgba(4, 11, 24, 0.64);
+ color: rgba(198, 225, 250, 0.62);
+ font-size: 10px;
+ backdrop-filter: blur(16px);
+}
+
+.status-board-footer span {
+ display: inline-flex;
+ min-width: 0;
+ align-items: center;
+ gap: 7px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+@keyframes status-board-aurora-drift {
+ from {
+ transform: rotate(-3deg) scale(1);
+ }
+ to {
+ transform: rotate(4deg) scale(1.07);
+ }
+}
+
+@keyframes status-board-float {
+ from {
+ transform: translate3d(0, 0, 0) scale(1);
+ }
+ to {
+ transform: translate3d(20px, -24px, 0) scale(1.08);
+ }
+}
+
+@keyframes status-board-scanline {
+ from {
+ transform: translateY(-18px);
+ }
+ to {
+ transform: translateY(18px);
+ }
+}
+
+@keyframes status-board-pulse {
+ 0%,
+ 100% {
+ opacity: 0.54;
+ transform: scale(0.92);
+ }
+ 50% {
+ opacity: 1;
+ transform: scale(1.08);
+ }
+}
+
+@keyframes status-board-logo-pulse {
+ 0%,
+ 100% {
+ box-shadow: 0 0 22px rgba(var(--status-cyan), 0.18), inset 0 0 22px rgba(255, 255, 255, 0.08);
+ }
+ 50% {
+ box-shadow: 0 0 34px rgba(var(--status-cyan), 0.42), inset 0 0 28px rgba(var(--status-cyan), 0.12);
+ }
+}
+
+@keyframes status-board-ring-spin {
+ from {
+ transform: rotate(0deg) scale(1);
+ }
+ to {
+ transform: rotate(360deg) scale(1.015);
+ }
+}
+
+@keyframes status-board-radar-sweep {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@keyframes status-board-node-pulse {
+ 0%,
+ 100% {
+ opacity: 0.72;
+ filter: saturate(0.86);
+ }
+ 50% {
+ opacity: 1;
+ filter: saturate(1.35);
+ }
+}
+
+@keyframes status-board-load-breathe {
+ from {
+ filter: brightness(0.82);
+ }
+ to {
+ filter: brightness(1.24);
+ }
+}
+
+@media (max-width: 1400px), (max-height: 760px) {
+ .status-board-stage {
+ width: 100vw;
+ height: 100dvh;
+ padding: 10px;
+ }
+
+ .status-board-cockpit {
+ grid-template-columns: 310px minmax(0, 1fr) 330px;
+ gap: 8px;
+ }
+
+ .status-board-left-rail,
+ .status-board-center-stack,
+ .status-board-right-rail {
+ gap: 8px;
+ }
+
+ .status-board-panel {
+ padding: 10px;
+ }
+
+ .status-board-radar-screen {
+ width: min(38vh, 320px);
+ height: min(38vh, 320px);
+ }
+
+ .status-board-metrics-grid {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ }
+}
From 3e26f7f8d4f9e23be252e563156829c29a158f1b Mon Sep 17 00:00:00 2001
From: su-fen <715041@qq.com>
Date: Wed, 10 Jun 2026 10:37:42 +0800
Subject: [PATCH 4/9] fix(gateway): track remote chat runtime state
---
.../agent-gateway/internal/config/config.go | 10 +
.../internal/proto/v1/gateway.pb.go | 1014 ++++++++++-------
crates/agent-gateway/internal/server/grpc.go | 14 +-
.../internal/server/websocket.go | 40 +-
.../server/websocket_chat_handlers.go | 156 ++-
.../agent-gateway/internal/session/manager.go | 44 +-
.../internal/session/manager_chat_runs.go | 540 ++++++++-
.../internal/session/manager_registry.go | 136 ++-
.../internal/session/manager_state.go | 8 +
crates/agent-gateway/proto/v1/gateway.proto | 22 +
.../test/session/manager_test.go | 364 ++++++
.../test/websocket/chat_bridge_test.go | 353 ++++++
12 files changed, 2266 insertions(+), 435 deletions(-)
diff --git a/crates/agent-gateway/internal/config/config.go b/crates/agent-gateway/internal/config/config.go
index ad699a7de..eedf50bbb 100644
--- a/crates/agent-gateway/internal/config/config.go
+++ b/crates/agent-gateway/internal/config/config.go
@@ -17,6 +17,8 @@ type Config struct {
TLSCert string
TLSKey string
RequestTimeout time.Duration
+ ChatStartTimeout time.Duration
+ ChatRenderStartTimeout time.Duration
HeartbeatPeriod time.Duration
WebSocketHeartbeatPeriod time.Duration
WebSocketWriteTimeout time.Duration
@@ -32,6 +34,8 @@ func Load() *Config {
flag.StringVar(&cfg.TLSCert, "tls-cert", getenv("LIVEAGENT_GATEWAY_TLS_CERT", ""), "TLS certificate path")
flag.StringVar(&cfg.TLSKey, "tls-key", getenv("LIVEAGENT_GATEWAY_TLS_KEY", ""), "TLS private key path")
flag.DurationVar(&cfg.RequestTimeout, "request-timeout", getenvDuration("LIVEAGENT_GATEWAY_REQUEST_TIMEOUT", 2*time.Minute), "request timeout for non-streaming API calls")
+ flag.DurationVar(&cfg.ChatStartTimeout, "chat-start-timeout", getenvDuration("LIVEAGENT_GATEWAY_CHAT_START_TIMEOUT", 15*time.Second), "timeout waiting for the desktop backend to accept a remote chat request")
+ flag.DurationVar(&cfg.ChatRenderStartTimeout, "chat-render-start-timeout", getenvDuration("LIVEAGENT_GATEWAY_CHAT_RENDER_START_TIMEOUT", 45*time.Second), "timeout waiting for the desktop app to start an accepted remote chat request")
flag.DurationVar(&cfg.HeartbeatPeriod, "heartbeat-period", getenvDuration("LIVEAGENT_GATEWAY_HEARTBEAT_PERIOD", 30*time.Second), "ping interval for agent connection")
flag.DurationVar(&cfg.WebSocketHeartbeatPeriod, "websocket-heartbeat-period", getenvDuration("LIVEAGENT_GATEWAY_WS_HEARTBEAT_PERIOD", 15*time.Second), "ping interval for browser WebSocket connections")
flag.DurationVar(&cfg.WebSocketWriteTimeout, "websocket-write-timeout", getenvDuration("LIVEAGENT_GATEWAY_WS_WRITE_TIMEOUT", 10*time.Second), "write timeout for browser WebSocket connections")
@@ -49,6 +53,12 @@ func Load() *Config {
if cfg.GRPCMaxMessageBytes <= 0 {
cfg.GRPCMaxMessageBytes = DefaultGRPCMaxMessageBytes
}
+ if cfg.ChatStartTimeout <= 0 {
+ cfg.ChatStartTimeout = 15 * time.Second
+ }
+ if cfg.ChatRenderStartTimeout <= 0 {
+ cfg.ChatRenderStartTimeout = 45 * time.Second
+ }
if cfg.WebSocketHeartbeatPeriod <= 0 {
cfg.WebSocketHeartbeatPeriod = 15 * time.Second
}
diff --git a/crates/agent-gateway/internal/proto/v1/gateway.pb.go b/crates/agent-gateway/internal/proto/v1/gateway.pb.go
index 019569c1e..7c159763e 100644
--- a/crates/agent-gateway/internal/proto/v1/gateway.pb.go
+++ b/crates/agent-gateway/internal/proto/v1/gateway.pb.go
@@ -1035,6 +1035,8 @@ type AgentEnvelope struct {
// *AgentEnvelope_TunnelControl
// *AgentEnvelope_TunnelControlResp
// *AgentEnvelope_TunnelFrame
+ // *AgentEnvelope_ChatControl
+ // *AgentEnvelope_RuntimeStatus
// *AgentEnvelope_Error
Payload isAgentEnvelope_Payload `protobuf_oneof:"payload"`
unknownFields protoimpl.UnknownFields
@@ -1470,6 +1472,24 @@ func (x *AgentEnvelope) GetTunnelFrame() *TunnelFrame {
return nil
}
+func (x *AgentEnvelope) GetChatControl() *ChatControlEvent {
+ if x != nil {
+ if x, ok := x.Payload.(*AgentEnvelope_ChatControl); ok {
+ return x.ChatControl
+ }
+ }
+ return nil
+}
+
+func (x *AgentEnvelope) GetRuntimeStatus() *RuntimeStatusEvent {
+ if x != nil {
+ if x, ok := x.Payload.(*AgentEnvelope_RuntimeStatus); ok {
+ return x.RuntimeStatus
+ }
+ }
+ return nil
+}
+
func (x *AgentEnvelope) GetError() *ErrorResponse {
if x != nil {
if x, ok := x.Payload.(*AgentEnvelope_Error); ok {
@@ -1651,6 +1671,14 @@ type AgentEnvelope_TunnelFrame struct {
TunnelFrame *TunnelFrame `protobuf:"bytes,69,opt,name=tunnel_frame,json=tunnelFrame,proto3,oneof"`
}
+type AgentEnvelope_ChatControl struct {
+ ChatControl *ChatControlEvent `protobuf:"bytes,70,opt,name=chat_control,json=chatControl,proto3,oneof"`
+}
+
+type AgentEnvelope_RuntimeStatus struct {
+ RuntimeStatus *RuntimeStatusEvent `protobuf:"bytes,71,opt,name=runtime_status,json=runtimeStatus,proto3,oneof"`
+}
+
type AgentEnvelope_Error struct {
Error *ErrorResponse `protobuf:"bytes,99,opt,name=error,proto3,oneof"`
}
@@ -1739,6 +1767,10 @@ func (*AgentEnvelope_TunnelControlResp) isAgentEnvelope_Payload() {}
func (*AgentEnvelope_TunnelFrame) isAgentEnvelope_Payload() {}
+func (*AgentEnvelope_ChatControl) isAgentEnvelope_Payload() {}
+
+func (*AgentEnvelope_RuntimeStatus) isAgentEnvelope_Payload() {}
+
func (*AgentEnvelope_Error) isAgentEnvelope_Payload() {}
type ChatSelectedModel struct {
@@ -3633,6 +3665,190 @@ func (x *ChatEvent) GetData() string {
return ""
}
+type ChatControlEvent struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"`
+ ClientRequestId string `protobuf:"bytes,2,opt,name=client_request_id,json=clientRequestId,proto3" json:"client_request_id,omitempty"`
+ ConversationId string `protobuf:"bytes,3,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"`
+ RunEpoch int64 `protobuf:"varint,4,opt,name=run_epoch,json=runEpoch,proto3" json:"run_epoch,omitempty"`
+ Type string `protobuf:"bytes,5,opt,name=type,proto3" json:"type,omitempty"`
+ State string `protobuf:"bytes,6,opt,name=state,proto3" json:"state,omitempty"`
+ ErrorCode string `protobuf:"bytes,7,opt,name=error_code,json=errorCode,proto3" json:"error_code,omitempty"`
+ Message string `protobuf:"bytes,8,opt,name=message,proto3" json:"message,omitempty"`
+ Seq int64 `protobuf:"varint,9,opt,name=seq,proto3" json:"seq,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *ChatControlEvent) Reset() {
+ *x = ChatControlEvent{}
+ mi := &file_proto_v1_gateway_proto_msgTypes[29]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *ChatControlEvent) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ChatControlEvent) ProtoMessage() {}
+
+func (x *ChatControlEvent) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_v1_gateway_proto_msgTypes[29]
+ 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 ChatControlEvent.ProtoReflect.Descriptor instead.
+func (*ChatControlEvent) Descriptor() ([]byte, []int) {
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{29}
+}
+
+func (x *ChatControlEvent) GetRequestId() string {
+ if x != nil {
+ return x.RequestId
+ }
+ return ""
+}
+
+func (x *ChatControlEvent) GetClientRequestId() string {
+ if x != nil {
+ return x.ClientRequestId
+ }
+ return ""
+}
+
+func (x *ChatControlEvent) GetConversationId() string {
+ if x != nil {
+ return x.ConversationId
+ }
+ return ""
+}
+
+func (x *ChatControlEvent) GetRunEpoch() int64 {
+ if x != nil {
+ return x.RunEpoch
+ }
+ return 0
+}
+
+func (x *ChatControlEvent) GetType() string {
+ if x != nil {
+ return x.Type
+ }
+ return ""
+}
+
+func (x *ChatControlEvent) GetState() string {
+ if x != nil {
+ return x.State
+ }
+ return ""
+}
+
+func (x *ChatControlEvent) GetErrorCode() string {
+ if x != nil {
+ return x.ErrorCode
+ }
+ return ""
+}
+
+func (x *ChatControlEvent) GetMessage() string {
+ if x != nil {
+ return x.Message
+ }
+ return ""
+}
+
+func (x *ChatControlEvent) GetSeq() int64 {
+ if x != nil {
+ return x.Seq
+ }
+ return 0
+}
+
+type RuntimeStatusEvent struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ WorkerId string `protobuf:"bytes,1,opt,name=worker_id,json=workerId,proto3" json:"worker_id,omitempty"`
+ State string `protobuf:"bytes,2,opt,name=state,proto3" json:"state,omitempty"`
+ Visible bool `protobuf:"varint,3,opt,name=visible,proto3" json:"visible,omitempty"`
+ ActiveRunCount uint32 `protobuf:"varint,4,opt,name=active_run_count,json=activeRunCount,proto3" json:"active_run_count,omitempty"`
+ Timestamp int64 `protobuf:"varint,5,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *RuntimeStatusEvent) Reset() {
+ *x = RuntimeStatusEvent{}
+ mi := &file_proto_v1_gateway_proto_msgTypes[30]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *RuntimeStatusEvent) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RuntimeStatusEvent) ProtoMessage() {}
+
+func (x *RuntimeStatusEvent) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_v1_gateway_proto_msgTypes[30]
+ 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 RuntimeStatusEvent.ProtoReflect.Descriptor instead.
+func (*RuntimeStatusEvent) Descriptor() ([]byte, []int) {
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{30}
+}
+
+func (x *RuntimeStatusEvent) GetWorkerId() string {
+ if x != nil {
+ return x.WorkerId
+ }
+ return ""
+}
+
+func (x *RuntimeStatusEvent) GetState() string {
+ if x != nil {
+ return x.State
+ }
+ return ""
+}
+
+func (x *RuntimeStatusEvent) GetVisible() bool {
+ if x != nil {
+ return x.Visible
+ }
+ return false
+}
+
+func (x *RuntimeStatusEvent) GetActiveRunCount() uint32 {
+ if x != nil {
+ return x.ActiveRunCount
+ }
+ return 0
+}
+
+func (x *RuntimeStatusEvent) GetTimestamp() int64 {
+ if x != nil {
+ return x.Timestamp
+ }
+ return 0
+}
+
type CronManageRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Action string `protobuf:"bytes,1,opt,name=action,proto3" json:"action,omitempty"`
@@ -3644,7 +3860,7 @@ type CronManageRequest struct {
func (x *CronManageRequest) Reset() {
*x = CronManageRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[29]
+ mi := &file_proto_v1_gateway_proto_msgTypes[31]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3656,7 +3872,7 @@ func (x *CronManageRequest) String() string {
func (*CronManageRequest) ProtoMessage() {}
func (x *CronManageRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[29]
+ mi := &file_proto_v1_gateway_proto_msgTypes[31]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3669,7 +3885,7 @@ func (x *CronManageRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use CronManageRequest.ProtoReflect.Descriptor instead.
func (*CronManageRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{29}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{31}
}
func (x *CronManageRequest) GetAction() string {
@@ -3703,7 +3919,7 @@ type CronManageResponse struct {
func (x *CronManageResponse) Reset() {
*x = CronManageResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[30]
+ mi := &file_proto_v1_gateway_proto_msgTypes[32]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3715,7 +3931,7 @@ func (x *CronManageResponse) String() string {
func (*CronManageResponse) ProtoMessage() {}
func (x *CronManageResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[30]
+ mi := &file_proto_v1_gateway_proto_msgTypes[32]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3728,7 +3944,7 @@ func (x *CronManageResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use CronManageResponse.ProtoReflect.Descriptor instead.
func (*CronManageResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{30}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{32}
}
func (x *CronManageResponse) GetAction() string {
@@ -3757,7 +3973,7 @@ type HistoryListRequest struct {
func (x *HistoryListRequest) Reset() {
*x = HistoryListRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[31]
+ mi := &file_proto_v1_gateway_proto_msgTypes[33]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3769,7 +3985,7 @@ func (x *HistoryListRequest) String() string {
func (*HistoryListRequest) ProtoMessage() {}
func (x *HistoryListRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[31]
+ mi := &file_proto_v1_gateway_proto_msgTypes[33]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3782,7 +3998,7 @@ func (x *HistoryListRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryListRequest.ProtoReflect.Descriptor instead.
func (*HistoryListRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{31}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{33}
}
func (x *HistoryListRequest) GetPage() int32 {
@@ -3823,7 +4039,7 @@ type HistoryListResponse struct {
func (x *HistoryListResponse) Reset() {
*x = HistoryListResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[32]
+ mi := &file_proto_v1_gateway_proto_msgTypes[34]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3835,7 +4051,7 @@ func (x *HistoryListResponse) String() string {
func (*HistoryListResponse) ProtoMessage() {}
func (x *HistoryListResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[32]
+ mi := &file_proto_v1_gateway_proto_msgTypes[34]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3848,7 +4064,7 @@ func (x *HistoryListResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryListResponse.ProtoReflect.Descriptor instead.
func (*HistoryListResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{32}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{34}
}
func (x *HistoryListResponse) GetConversations() []*ConversationSummary {
@@ -3885,7 +4101,7 @@ type ConversationSummary struct {
func (x *ConversationSummary) Reset() {
*x = ConversationSummary{}
- mi := &file_proto_v1_gateway_proto_msgTypes[33]
+ mi := &file_proto_v1_gateway_proto_msgTypes[35]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3897,7 +4113,7 @@ func (x *ConversationSummary) String() string {
func (*ConversationSummary) ProtoMessage() {}
func (x *ConversationSummary) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[33]
+ mi := &file_proto_v1_gateway_proto_msgTypes[35]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3910,7 +4126,7 @@ func (x *ConversationSummary) ProtoReflect() protoreflect.Message {
// Deprecated: Use ConversationSummary.ProtoReflect.Descriptor instead.
func (*ConversationSummary) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{33}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{35}
}
func (x *ConversationSummary) GetId() string {
@@ -4007,7 +4223,7 @@ type HistoryGetRequest struct {
func (x *HistoryGetRequest) Reset() {
*x = HistoryGetRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[34]
+ mi := &file_proto_v1_gateway_proto_msgTypes[36]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4019,7 +4235,7 @@ func (x *HistoryGetRequest) String() string {
func (*HistoryGetRequest) ProtoMessage() {}
func (x *HistoryGetRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[34]
+ mi := &file_proto_v1_gateway_proto_msgTypes[36]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4032,7 +4248,7 @@ func (x *HistoryGetRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryGetRequest.ProtoReflect.Descriptor instead.
func (*HistoryGetRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{34}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{36}
}
func (x *HistoryGetRequest) GetConversationId() string {
@@ -4063,7 +4279,7 @@ type HistoryGetResponse struct {
func (x *HistoryGetResponse) Reset() {
*x = HistoryGetResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[35]
+ mi := &file_proto_v1_gateway_proto_msgTypes[37]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4075,7 +4291,7 @@ func (x *HistoryGetResponse) String() string {
func (*HistoryGetResponse) ProtoMessage() {}
func (x *HistoryGetResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[35]
+ mi := &file_proto_v1_gateway_proto_msgTypes[37]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4088,7 +4304,7 @@ func (x *HistoryGetResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryGetResponse.ProtoReflect.Descriptor instead.
func (*HistoryGetResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{35}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{37}
}
func (x *HistoryGetResponse) GetConversationId() string {
@@ -4143,7 +4359,7 @@ type HistoryRenameRequest struct {
func (x *HistoryRenameRequest) Reset() {
*x = HistoryRenameRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[36]
+ mi := &file_proto_v1_gateway_proto_msgTypes[38]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4155,7 +4371,7 @@ func (x *HistoryRenameRequest) String() string {
func (*HistoryRenameRequest) ProtoMessage() {}
func (x *HistoryRenameRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[36]
+ mi := &file_proto_v1_gateway_proto_msgTypes[38]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4168,7 +4384,7 @@ func (x *HistoryRenameRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryRenameRequest.ProtoReflect.Descriptor instead.
func (*HistoryRenameRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{36}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{38}
}
func (x *HistoryRenameRequest) GetConversationId() string {
@@ -4194,7 +4410,7 @@ type HistoryRenameResponse struct {
func (x *HistoryRenameResponse) Reset() {
*x = HistoryRenameResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[37]
+ mi := &file_proto_v1_gateway_proto_msgTypes[39]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4206,7 +4422,7 @@ func (x *HistoryRenameResponse) String() string {
func (*HistoryRenameResponse) ProtoMessage() {}
func (x *HistoryRenameResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[37]
+ mi := &file_proto_v1_gateway_proto_msgTypes[39]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4219,7 +4435,7 @@ func (x *HistoryRenameResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryRenameResponse.ProtoReflect.Descriptor instead.
func (*HistoryRenameResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{37}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{39}
}
func (x *HistoryRenameResponse) GetConversation() *ConversationSummary {
@@ -4239,7 +4455,7 @@ type HistoryPinRequest struct {
func (x *HistoryPinRequest) Reset() {
*x = HistoryPinRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[38]
+ mi := &file_proto_v1_gateway_proto_msgTypes[40]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4251,7 +4467,7 @@ func (x *HistoryPinRequest) String() string {
func (*HistoryPinRequest) ProtoMessage() {}
func (x *HistoryPinRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[38]
+ mi := &file_proto_v1_gateway_proto_msgTypes[40]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4264,7 +4480,7 @@ func (x *HistoryPinRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryPinRequest.ProtoReflect.Descriptor instead.
func (*HistoryPinRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{38}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{40}
}
func (x *HistoryPinRequest) GetConversationId() string {
@@ -4290,7 +4506,7 @@ type HistoryPinResponse struct {
func (x *HistoryPinResponse) Reset() {
*x = HistoryPinResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[39]
+ mi := &file_proto_v1_gateway_proto_msgTypes[41]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4302,7 +4518,7 @@ func (x *HistoryPinResponse) String() string {
func (*HistoryPinResponse) ProtoMessage() {}
func (x *HistoryPinResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[39]
+ mi := &file_proto_v1_gateway_proto_msgTypes[41]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4315,7 +4531,7 @@ func (x *HistoryPinResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryPinResponse.ProtoReflect.Descriptor instead.
func (*HistoryPinResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{39}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{41}
}
func (x *HistoryPinResponse) GetConversation() *ConversationSummary {
@@ -4339,7 +4555,7 @@ type HistoryShareStatus struct {
func (x *HistoryShareStatus) Reset() {
*x = HistoryShareStatus{}
- mi := &file_proto_v1_gateway_proto_msgTypes[40]
+ mi := &file_proto_v1_gateway_proto_msgTypes[42]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4351,7 +4567,7 @@ func (x *HistoryShareStatus) String() string {
func (*HistoryShareStatus) ProtoMessage() {}
func (x *HistoryShareStatus) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[40]
+ mi := &file_proto_v1_gateway_proto_msgTypes[42]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4364,7 +4580,7 @@ func (x *HistoryShareStatus) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryShareStatus.ProtoReflect.Descriptor instead.
func (*HistoryShareStatus) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{40}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{42}
}
func (x *HistoryShareStatus) GetConversationId() string {
@@ -4418,7 +4634,7 @@ type HistoryShareGetRequest struct {
func (x *HistoryShareGetRequest) Reset() {
*x = HistoryShareGetRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[41]
+ mi := &file_proto_v1_gateway_proto_msgTypes[43]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4430,7 +4646,7 @@ func (x *HistoryShareGetRequest) String() string {
func (*HistoryShareGetRequest) ProtoMessage() {}
func (x *HistoryShareGetRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[41]
+ mi := &file_proto_v1_gateway_proto_msgTypes[43]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4443,7 +4659,7 @@ func (x *HistoryShareGetRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryShareGetRequest.ProtoReflect.Descriptor instead.
func (*HistoryShareGetRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{41}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{43}
}
func (x *HistoryShareGetRequest) GetConversationId() string {
@@ -4462,7 +4678,7 @@ type HistoryShareGetResponse struct {
func (x *HistoryShareGetResponse) Reset() {
*x = HistoryShareGetResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[42]
+ mi := &file_proto_v1_gateway_proto_msgTypes[44]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4474,7 +4690,7 @@ func (x *HistoryShareGetResponse) String() string {
func (*HistoryShareGetResponse) ProtoMessage() {}
func (x *HistoryShareGetResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[42]
+ mi := &file_proto_v1_gateway_proto_msgTypes[44]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4487,7 +4703,7 @@ func (x *HistoryShareGetResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryShareGetResponse.ProtoReflect.Descriptor instead.
func (*HistoryShareGetResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{42}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{44}
}
func (x *HistoryShareGetResponse) GetShare() *HistoryShareStatus {
@@ -4508,7 +4724,7 @@ type HistoryShareSetRequest struct {
func (x *HistoryShareSetRequest) Reset() {
*x = HistoryShareSetRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[43]
+ mi := &file_proto_v1_gateway_proto_msgTypes[45]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4520,7 +4736,7 @@ func (x *HistoryShareSetRequest) String() string {
func (*HistoryShareSetRequest) ProtoMessage() {}
func (x *HistoryShareSetRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[43]
+ mi := &file_proto_v1_gateway_proto_msgTypes[45]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4533,7 +4749,7 @@ func (x *HistoryShareSetRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryShareSetRequest.ProtoReflect.Descriptor instead.
func (*HistoryShareSetRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{43}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{45}
}
func (x *HistoryShareSetRequest) GetConversationId() string {
@@ -4566,7 +4782,7 @@ type HistoryShareSetResponse struct {
func (x *HistoryShareSetResponse) Reset() {
*x = HistoryShareSetResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[44]
+ mi := &file_proto_v1_gateway_proto_msgTypes[46]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4578,7 +4794,7 @@ func (x *HistoryShareSetResponse) String() string {
func (*HistoryShareSetResponse) ProtoMessage() {}
func (x *HistoryShareSetResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[44]
+ mi := &file_proto_v1_gateway_proto_msgTypes[46]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4591,7 +4807,7 @@ func (x *HistoryShareSetResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryShareSetResponse.ProtoReflect.Descriptor instead.
func (*HistoryShareSetResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{44}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{46}
}
func (x *HistoryShareSetResponse) GetShare() *HistoryShareStatus {
@@ -4610,7 +4826,7 @@ type HistoryShareResolveRequest struct {
func (x *HistoryShareResolveRequest) Reset() {
*x = HistoryShareResolveRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[45]
+ mi := &file_proto_v1_gateway_proto_msgTypes[47]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4622,7 +4838,7 @@ func (x *HistoryShareResolveRequest) String() string {
func (*HistoryShareResolveRequest) ProtoMessage() {}
func (x *HistoryShareResolveRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[45]
+ mi := &file_proto_v1_gateway_proto_msgTypes[47]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4635,7 +4851,7 @@ func (x *HistoryShareResolveRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryShareResolveRequest.ProtoReflect.Descriptor instead.
func (*HistoryShareResolveRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{45}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{47}
}
func (x *HistoryShareResolveRequest) GetToken() string {
@@ -4658,7 +4874,7 @@ type HistoryShareResolveResponse struct {
func (x *HistoryShareResolveResponse) Reset() {
*x = HistoryShareResolveResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[46]
+ mi := &file_proto_v1_gateway_proto_msgTypes[48]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4670,7 +4886,7 @@ func (x *HistoryShareResolveResponse) String() string {
func (*HistoryShareResolveResponse) ProtoMessage() {}
func (x *HistoryShareResolveResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[46]
+ mi := &file_proto_v1_gateway_proto_msgTypes[48]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4683,7 +4899,7 @@ func (x *HistoryShareResolveResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryShareResolveResponse.ProtoReflect.Descriptor instead.
func (*HistoryShareResolveResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{46}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{48}
}
func (x *HistoryShareResolveResponse) GetConversationId() string {
@@ -4729,7 +4945,7 @@ type HistoryWorkdirsRequest struct {
func (x *HistoryWorkdirsRequest) Reset() {
*x = HistoryWorkdirsRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[47]
+ mi := &file_proto_v1_gateway_proto_msgTypes[49]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4741,7 +4957,7 @@ func (x *HistoryWorkdirsRequest) String() string {
func (*HistoryWorkdirsRequest) ProtoMessage() {}
func (x *HistoryWorkdirsRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[47]
+ mi := &file_proto_v1_gateway_proto_msgTypes[49]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4754,7 +4970,7 @@ func (x *HistoryWorkdirsRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryWorkdirsRequest.ProtoReflect.Descriptor instead.
func (*HistoryWorkdirsRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{47}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{49}
}
type HistoryWorkdirSummary struct {
@@ -4768,7 +4984,7 @@ type HistoryWorkdirSummary struct {
func (x *HistoryWorkdirSummary) Reset() {
*x = HistoryWorkdirSummary{}
- mi := &file_proto_v1_gateway_proto_msgTypes[48]
+ mi := &file_proto_v1_gateway_proto_msgTypes[50]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4780,7 +4996,7 @@ func (x *HistoryWorkdirSummary) String() string {
func (*HistoryWorkdirSummary) ProtoMessage() {}
func (x *HistoryWorkdirSummary) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[48]
+ mi := &file_proto_v1_gateway_proto_msgTypes[50]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4793,7 +5009,7 @@ func (x *HistoryWorkdirSummary) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryWorkdirSummary.ProtoReflect.Descriptor instead.
func (*HistoryWorkdirSummary) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{48}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{50}
}
func (x *HistoryWorkdirSummary) GetPath() string {
@@ -4826,7 +5042,7 @@ type HistoryWorkdirsResponse struct {
func (x *HistoryWorkdirsResponse) Reset() {
*x = HistoryWorkdirsResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[49]
+ mi := &file_proto_v1_gateway_proto_msgTypes[51]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4838,7 +5054,7 @@ func (x *HistoryWorkdirsResponse) String() string {
func (*HistoryWorkdirsResponse) ProtoMessage() {}
func (x *HistoryWorkdirsResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[49]
+ mi := &file_proto_v1_gateway_proto_msgTypes[51]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4851,7 +5067,7 @@ func (x *HistoryWorkdirsResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryWorkdirsResponse.ProtoReflect.Descriptor instead.
func (*HistoryWorkdirsResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{49}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{51}
}
func (x *HistoryWorkdirsResponse) GetWorkdirs() []*HistoryWorkdirSummary {
@@ -4870,7 +5086,7 @@ type HistoryDeleteRequest struct {
func (x *HistoryDeleteRequest) Reset() {
*x = HistoryDeleteRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[50]
+ mi := &file_proto_v1_gateway_proto_msgTypes[52]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4882,7 +5098,7 @@ func (x *HistoryDeleteRequest) String() string {
func (*HistoryDeleteRequest) ProtoMessage() {}
func (x *HistoryDeleteRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[50]
+ mi := &file_proto_v1_gateway_proto_msgTypes[52]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4895,7 +5111,7 @@ func (x *HistoryDeleteRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryDeleteRequest.ProtoReflect.Descriptor instead.
func (*HistoryDeleteRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{50}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{52}
}
func (x *HistoryDeleteRequest) GetConversationId() string {
@@ -4913,7 +5129,7 @@ type HistoryDeleteResponse struct {
func (x *HistoryDeleteResponse) Reset() {
*x = HistoryDeleteResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[51]
+ mi := &file_proto_v1_gateway_proto_msgTypes[53]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4925,7 +5141,7 @@ func (x *HistoryDeleteResponse) String() string {
func (*HistoryDeleteResponse) ProtoMessage() {}
func (x *HistoryDeleteResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[51]
+ mi := &file_proto_v1_gateway_proto_msgTypes[53]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4938,7 +5154,7 @@ func (x *HistoryDeleteResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryDeleteResponse.ProtoReflect.Descriptor instead.
func (*HistoryDeleteResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{51}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{53}
}
type HistoryTruncateRequest struct {
@@ -4953,7 +5169,7 @@ type HistoryTruncateRequest struct {
func (x *HistoryTruncateRequest) Reset() {
*x = HistoryTruncateRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[52]
+ mi := &file_proto_v1_gateway_proto_msgTypes[54]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4965,7 +5181,7 @@ func (x *HistoryTruncateRequest) String() string {
func (*HistoryTruncateRequest) ProtoMessage() {}
func (x *HistoryTruncateRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[52]
+ mi := &file_proto_v1_gateway_proto_msgTypes[54]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4978,7 +5194,7 @@ func (x *HistoryTruncateRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryTruncateRequest.ProtoReflect.Descriptor instead.
func (*HistoryTruncateRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{52}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{54}
}
func (x *HistoryTruncateRequest) GetConversationId() string {
@@ -5020,7 +5236,7 @@ type HistoryTruncateResponse struct {
func (x *HistoryTruncateResponse) Reset() {
*x = HistoryTruncateResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[53]
+ mi := &file_proto_v1_gateway_proto_msgTypes[55]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5032,7 +5248,7 @@ func (x *HistoryTruncateResponse) String() string {
func (*HistoryTruncateResponse) ProtoMessage() {}
func (x *HistoryTruncateResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[53]
+ mi := &file_proto_v1_gateway_proto_msgTypes[55]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5045,7 +5261,7 @@ func (x *HistoryTruncateResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryTruncateResponse.ProtoReflect.Descriptor instead.
func (*HistoryTruncateResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{53}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{55}
}
func (x *HistoryTruncateResponse) GetConversationId() string {
@@ -5080,7 +5296,7 @@ type HistorySyncEvent struct {
func (x *HistorySyncEvent) Reset() {
*x = HistorySyncEvent{}
- mi := &file_proto_v1_gateway_proto_msgTypes[54]
+ mi := &file_proto_v1_gateway_proto_msgTypes[56]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5092,7 +5308,7 @@ func (x *HistorySyncEvent) String() string {
func (*HistorySyncEvent) ProtoMessage() {}
func (x *HistorySyncEvent) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[54]
+ mi := &file_proto_v1_gateway_proto_msgTypes[56]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5105,7 +5321,7 @@ func (x *HistorySyncEvent) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistorySyncEvent.ProtoReflect.Descriptor instead.
func (*HistorySyncEvent) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{54}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{56}
}
func (x *HistorySyncEvent) GetKind() string {
@@ -5137,7 +5353,7 @@ type ProviderListRequest struct {
func (x *ProviderListRequest) Reset() {
*x = ProviderListRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[55]
+ mi := &file_proto_v1_gateway_proto_msgTypes[57]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5149,7 +5365,7 @@ func (x *ProviderListRequest) String() string {
func (*ProviderListRequest) ProtoMessage() {}
func (x *ProviderListRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[55]
+ mi := &file_proto_v1_gateway_proto_msgTypes[57]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5162,7 +5378,7 @@ func (x *ProviderListRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ProviderListRequest.ProtoReflect.Descriptor instead.
func (*ProviderListRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{55}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{57}
}
type ProviderListResponse struct {
@@ -5174,7 +5390,7 @@ type ProviderListResponse struct {
func (x *ProviderListResponse) Reset() {
*x = ProviderListResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[56]
+ mi := &file_proto_v1_gateway_proto_msgTypes[58]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5186,7 +5402,7 @@ func (x *ProviderListResponse) String() string {
func (*ProviderListResponse) ProtoMessage() {}
func (x *ProviderListResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[56]
+ mi := &file_proto_v1_gateway_proto_msgTypes[58]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5199,7 +5415,7 @@ func (x *ProviderListResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ProviderListResponse.ProtoReflect.Descriptor instead.
func (*ProviderListResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{56}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{58}
}
func (x *ProviderListResponse) GetProvidersJson() string {
@@ -5217,7 +5433,7 @@ type SettingsGetRequest struct {
func (x *SettingsGetRequest) Reset() {
*x = SettingsGetRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[57]
+ mi := &file_proto_v1_gateway_proto_msgTypes[59]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5229,7 +5445,7 @@ func (x *SettingsGetRequest) String() string {
func (*SettingsGetRequest) ProtoMessage() {}
func (x *SettingsGetRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[57]
+ mi := &file_proto_v1_gateway_proto_msgTypes[59]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5242,7 +5458,7 @@ func (x *SettingsGetRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use SettingsGetRequest.ProtoReflect.Descriptor instead.
func (*SettingsGetRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{57}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{59}
}
type SettingsGetResponse struct {
@@ -5254,7 +5470,7 @@ type SettingsGetResponse struct {
func (x *SettingsGetResponse) Reset() {
*x = SettingsGetResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[58]
+ mi := &file_proto_v1_gateway_proto_msgTypes[60]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5266,7 +5482,7 @@ func (x *SettingsGetResponse) String() string {
func (*SettingsGetResponse) ProtoMessage() {}
func (x *SettingsGetResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[58]
+ mi := &file_proto_v1_gateway_proto_msgTypes[60]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5279,7 +5495,7 @@ func (x *SettingsGetResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use SettingsGetResponse.ProtoReflect.Descriptor instead.
func (*SettingsGetResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{58}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{60}
}
func (x *SettingsGetResponse) GetSettingsJson() string {
@@ -5298,7 +5514,7 @@ type SettingsUpdateRequest struct {
func (x *SettingsUpdateRequest) Reset() {
*x = SettingsUpdateRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[59]
+ mi := &file_proto_v1_gateway_proto_msgTypes[61]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5310,7 +5526,7 @@ func (x *SettingsUpdateRequest) String() string {
func (*SettingsUpdateRequest) ProtoMessage() {}
func (x *SettingsUpdateRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[59]
+ mi := &file_proto_v1_gateway_proto_msgTypes[61]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5323,7 +5539,7 @@ func (x *SettingsUpdateRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use SettingsUpdateRequest.ProtoReflect.Descriptor instead.
func (*SettingsUpdateRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{59}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{61}
}
func (x *SettingsUpdateRequest) GetSettingsJson() string {
@@ -5343,7 +5559,7 @@ type SettingsUpdateResponse struct {
func (x *SettingsUpdateResponse) Reset() {
*x = SettingsUpdateResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[60]
+ mi := &file_proto_v1_gateway_proto_msgTypes[62]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5355,7 +5571,7 @@ func (x *SettingsUpdateResponse) String() string {
func (*SettingsUpdateResponse) ProtoMessage() {}
func (x *SettingsUpdateResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[60]
+ mi := &file_proto_v1_gateway_proto_msgTypes[62]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5368,7 +5584,7 @@ func (x *SettingsUpdateResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use SettingsUpdateResponse.ProtoReflect.Descriptor instead.
func (*SettingsUpdateResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{60}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{62}
}
func (x *SettingsUpdateResponse) GetAccepted() bool {
@@ -5394,7 +5610,7 @@ type SettingsSyncEvent struct {
func (x *SettingsSyncEvent) Reset() {
*x = SettingsSyncEvent{}
- mi := &file_proto_v1_gateway_proto_msgTypes[61]
+ mi := &file_proto_v1_gateway_proto_msgTypes[63]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5406,7 +5622,7 @@ func (x *SettingsSyncEvent) String() string {
func (*SettingsSyncEvent) ProtoMessage() {}
func (x *SettingsSyncEvent) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[61]
+ mi := &file_proto_v1_gateway_proto_msgTypes[63]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5419,7 +5635,7 @@ func (x *SettingsSyncEvent) ProtoReflect() protoreflect.Message {
// Deprecated: Use SettingsSyncEvent.ProtoReflect.Descriptor instead.
func (*SettingsSyncEvent) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{61}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{63}
}
func (x *SettingsSyncEvent) GetSettingsJson() string {
@@ -5437,7 +5653,7 @@ type SkillFilesListRequest struct {
func (x *SkillFilesListRequest) Reset() {
*x = SkillFilesListRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[62]
+ mi := &file_proto_v1_gateway_proto_msgTypes[64]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5449,7 +5665,7 @@ func (x *SkillFilesListRequest) String() string {
func (*SkillFilesListRequest) ProtoMessage() {}
func (x *SkillFilesListRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[62]
+ mi := &file_proto_v1_gateway_proto_msgTypes[64]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5462,7 +5678,7 @@ func (x *SkillFilesListRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use SkillFilesListRequest.ProtoReflect.Descriptor instead.
func (*SkillFilesListRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{62}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{64}
}
type SkillFilesListResponse struct {
@@ -5476,7 +5692,7 @@ type SkillFilesListResponse struct {
func (x *SkillFilesListResponse) Reset() {
*x = SkillFilesListResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[63]
+ mi := &file_proto_v1_gateway_proto_msgTypes[65]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5488,7 +5704,7 @@ func (x *SkillFilesListResponse) String() string {
func (*SkillFilesListResponse) ProtoMessage() {}
func (x *SkillFilesListResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[63]
+ mi := &file_proto_v1_gateway_proto_msgTypes[65]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5501,7 +5717,7 @@ func (x *SkillFilesListResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use SkillFilesListResponse.ProtoReflect.Descriptor instead.
func (*SkillFilesListResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{63}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{65}
}
func (x *SkillFilesListResponse) GetRootDir() string {
@@ -5534,7 +5750,7 @@ type SkillMetadataReadRequest struct {
func (x *SkillMetadataReadRequest) Reset() {
*x = SkillMetadataReadRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[64]
+ mi := &file_proto_v1_gateway_proto_msgTypes[66]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5546,7 +5762,7 @@ func (x *SkillMetadataReadRequest) String() string {
func (*SkillMetadataReadRequest) ProtoMessage() {}
func (x *SkillMetadataReadRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[64]
+ mi := &file_proto_v1_gateway_proto_msgTypes[66]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5559,7 +5775,7 @@ func (x *SkillMetadataReadRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use SkillMetadataReadRequest.ProtoReflect.Descriptor instead.
func (*SkillMetadataReadRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{64}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{66}
}
func (x *SkillMetadataReadRequest) GetPath() string {
@@ -5579,7 +5795,7 @@ type SkillMetadataReadResponse struct {
func (x *SkillMetadataReadResponse) Reset() {
*x = SkillMetadataReadResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[65]
+ mi := &file_proto_v1_gateway_proto_msgTypes[67]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5591,7 +5807,7 @@ func (x *SkillMetadataReadResponse) String() string {
func (*SkillMetadataReadResponse) ProtoMessage() {}
func (x *SkillMetadataReadResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[65]
+ mi := &file_proto_v1_gateway_proto_msgTypes[67]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5604,7 +5820,7 @@ func (x *SkillMetadataReadResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use SkillMetadataReadResponse.ProtoReflect.Descriptor instead.
func (*SkillMetadataReadResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{65}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{67}
}
func (x *SkillMetadataReadResponse) GetName() string {
@@ -5632,7 +5848,7 @@ type SkillTextReadRequest struct {
func (x *SkillTextReadRequest) Reset() {
*x = SkillTextReadRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[66]
+ mi := &file_proto_v1_gateway_proto_msgTypes[68]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5644,7 +5860,7 @@ func (x *SkillTextReadRequest) String() string {
func (*SkillTextReadRequest) ProtoMessage() {}
func (x *SkillTextReadRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[66]
+ mi := &file_proto_v1_gateway_proto_msgTypes[68]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5657,7 +5873,7 @@ func (x *SkillTextReadRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use SkillTextReadRequest.ProtoReflect.Descriptor instead.
func (*SkillTextReadRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{66}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{68}
}
func (x *SkillTextReadRequest) GetPath() string {
@@ -5691,7 +5907,7 @@ type SkillTextReadResponse struct {
func (x *SkillTextReadResponse) Reset() {
*x = SkillTextReadResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[67]
+ mi := &file_proto_v1_gateway_proto_msgTypes[69]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5703,7 +5919,7 @@ func (x *SkillTextReadResponse) String() string {
func (*SkillTextReadResponse) ProtoMessage() {}
func (x *SkillTextReadResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[67]
+ mi := &file_proto_v1_gateway_proto_msgTypes[69]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5716,7 +5932,7 @@ func (x *SkillTextReadResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use SkillTextReadResponse.ProtoReflect.Descriptor instead.
func (*SkillTextReadResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{67}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{69}
}
func (x *SkillTextReadResponse) GetContent() string {
@@ -5742,7 +5958,7 @@ type SkillManageRequest struct {
func (x *SkillManageRequest) Reset() {
*x = SkillManageRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[68]
+ mi := &file_proto_v1_gateway_proto_msgTypes[70]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5754,7 +5970,7 @@ func (x *SkillManageRequest) String() string {
func (*SkillManageRequest) ProtoMessage() {}
func (x *SkillManageRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[68]
+ mi := &file_proto_v1_gateway_proto_msgTypes[70]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5767,7 +5983,7 @@ func (x *SkillManageRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use SkillManageRequest.ProtoReflect.Descriptor instead.
func (*SkillManageRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{68}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{70}
}
func (x *SkillManageRequest) GetPayloadJson() string {
@@ -5786,7 +6002,7 @@ type SkillManageResponse struct {
func (x *SkillManageResponse) Reset() {
*x = SkillManageResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[69]
+ mi := &file_proto_v1_gateway_proto_msgTypes[71]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5798,7 +6014,7 @@ func (x *SkillManageResponse) String() string {
func (*SkillManageResponse) ProtoMessage() {}
func (x *SkillManageResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[69]
+ mi := &file_proto_v1_gateway_proto_msgTypes[71]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5811,7 +6027,7 @@ func (x *SkillManageResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use SkillManageResponse.ProtoReflect.Descriptor instead.
func (*SkillManageResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{69}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{71}
}
func (x *SkillManageResponse) GetResultJson() string {
@@ -5832,7 +6048,7 @@ type FileMentionListRequest struct {
func (x *FileMentionListRequest) Reset() {
*x = FileMentionListRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[70]
+ mi := &file_proto_v1_gateway_proto_msgTypes[72]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5844,7 +6060,7 @@ func (x *FileMentionListRequest) String() string {
func (*FileMentionListRequest) ProtoMessage() {}
func (x *FileMentionListRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[70]
+ mi := &file_proto_v1_gateway_proto_msgTypes[72]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5857,7 +6073,7 @@ func (x *FileMentionListRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use FileMentionListRequest.ProtoReflect.Descriptor instead.
func (*FileMentionListRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{70}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{72}
}
func (x *FileMentionListRequest) GetWorkdir() string {
@@ -5891,7 +6107,7 @@ type FileMentionEntry struct {
func (x *FileMentionEntry) Reset() {
*x = FileMentionEntry{}
- mi := &file_proto_v1_gateway_proto_msgTypes[71]
+ mi := &file_proto_v1_gateway_proto_msgTypes[73]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5903,7 +6119,7 @@ func (x *FileMentionEntry) String() string {
func (*FileMentionEntry) ProtoMessage() {}
func (x *FileMentionEntry) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[71]
+ mi := &file_proto_v1_gateway_proto_msgTypes[73]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5916,7 +6132,7 @@ func (x *FileMentionEntry) ProtoReflect() protoreflect.Message {
// Deprecated: Use FileMentionEntry.ProtoReflect.Descriptor instead.
func (*FileMentionEntry) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{71}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{73}
}
func (x *FileMentionEntry) GetPath() string {
@@ -5943,7 +6159,7 @@ type FileMentionListResponse struct {
func (x *FileMentionListResponse) Reset() {
*x = FileMentionListResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[72]
+ mi := &file_proto_v1_gateway_proto_msgTypes[74]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5955,7 +6171,7 @@ func (x *FileMentionListResponse) String() string {
func (*FileMentionListResponse) ProtoMessage() {}
func (x *FileMentionListResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[72]
+ mi := &file_proto_v1_gateway_proto_msgTypes[74]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5968,7 +6184,7 @@ func (x *FileMentionListResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use FileMentionListResponse.ProtoReflect.Descriptor instead.
func (*FileMentionListResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{72}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{74}
}
func (x *FileMentionListResponse) GetEntries() []*FileMentionEntry {
@@ -5997,7 +6213,7 @@ type FsRoot struct {
func (x *FsRoot) Reset() {
*x = FsRoot{}
- mi := &file_proto_v1_gateway_proto_msgTypes[73]
+ mi := &file_proto_v1_gateway_proto_msgTypes[75]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6009,7 +6225,7 @@ func (x *FsRoot) String() string {
func (*FsRoot) ProtoMessage() {}
func (x *FsRoot) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[73]
+ mi := &file_proto_v1_gateway_proto_msgTypes[75]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6022,7 +6238,7 @@ func (x *FsRoot) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsRoot.ProtoReflect.Descriptor instead.
func (*FsRoot) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{73}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{75}
}
func (x *FsRoot) GetId() string {
@@ -6061,7 +6277,7 @@ type FsRootsRequest struct {
func (x *FsRootsRequest) Reset() {
*x = FsRootsRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[74]
+ mi := &file_proto_v1_gateway_proto_msgTypes[76]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6073,7 +6289,7 @@ func (x *FsRootsRequest) String() string {
func (*FsRootsRequest) ProtoMessage() {}
func (x *FsRootsRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[74]
+ mi := &file_proto_v1_gateway_proto_msgTypes[76]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6086,7 +6302,7 @@ func (x *FsRootsRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsRootsRequest.ProtoReflect.Descriptor instead.
func (*FsRootsRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{74}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{76}
}
type FsRootsResponse struct {
@@ -6098,7 +6314,7 @@ type FsRootsResponse struct {
func (x *FsRootsResponse) Reset() {
*x = FsRootsResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[75]
+ mi := &file_proto_v1_gateway_proto_msgTypes[77]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6110,7 +6326,7 @@ func (x *FsRootsResponse) String() string {
func (*FsRootsResponse) ProtoMessage() {}
func (x *FsRootsResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[75]
+ mi := &file_proto_v1_gateway_proto_msgTypes[77]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6123,7 +6339,7 @@ func (x *FsRootsResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsRootsResponse.ProtoReflect.Descriptor instead.
func (*FsRootsResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{75}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{77}
}
func (x *FsRootsResponse) GetRoots() []*FsRoot {
@@ -6143,7 +6359,7 @@ type FsListDirsRequest struct {
func (x *FsListDirsRequest) Reset() {
*x = FsListDirsRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[76]
+ mi := &file_proto_v1_gateway_proto_msgTypes[78]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6155,7 +6371,7 @@ func (x *FsListDirsRequest) String() string {
func (*FsListDirsRequest) ProtoMessage() {}
func (x *FsListDirsRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[76]
+ mi := &file_proto_v1_gateway_proto_msgTypes[78]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6168,7 +6384,7 @@ func (x *FsListDirsRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsListDirsRequest.ProtoReflect.Descriptor instead.
func (*FsListDirsRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{76}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{78}
}
func (x *FsListDirsRequest) GetPath() string {
@@ -6195,7 +6411,7 @@ type FsDirEntry struct {
func (x *FsDirEntry) Reset() {
*x = FsDirEntry{}
- mi := &file_proto_v1_gateway_proto_msgTypes[77]
+ mi := &file_proto_v1_gateway_proto_msgTypes[79]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6207,7 +6423,7 @@ func (x *FsDirEntry) String() string {
func (*FsDirEntry) ProtoMessage() {}
func (x *FsDirEntry) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[77]
+ mi := &file_proto_v1_gateway_proto_msgTypes[79]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6220,7 +6436,7 @@ func (x *FsDirEntry) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsDirEntry.ProtoReflect.Descriptor instead.
func (*FsDirEntry) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{77}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{79}
}
func (x *FsDirEntry) GetPath() string {
@@ -6248,7 +6464,7 @@ type FsListDirsResponse struct {
func (x *FsListDirsResponse) Reset() {
*x = FsListDirsResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[78]
+ mi := &file_proto_v1_gateway_proto_msgTypes[80]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6260,7 +6476,7 @@ func (x *FsListDirsResponse) String() string {
func (*FsListDirsResponse) ProtoMessage() {}
func (x *FsListDirsResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[78]
+ mi := &file_proto_v1_gateway_proto_msgTypes[80]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6273,7 +6489,7 @@ func (x *FsListDirsResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsListDirsResponse.ProtoReflect.Descriptor instead.
func (*FsListDirsResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{78}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{80}
}
func (x *FsListDirsResponse) GetPath() string {
@@ -6307,7 +6523,7 @@ type FsCreateProjectFolderRequest struct {
func (x *FsCreateProjectFolderRequest) Reset() {
*x = FsCreateProjectFolderRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[79]
+ mi := &file_proto_v1_gateway_proto_msgTypes[81]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6319,7 +6535,7 @@ func (x *FsCreateProjectFolderRequest) String() string {
func (*FsCreateProjectFolderRequest) ProtoMessage() {}
func (x *FsCreateProjectFolderRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[79]
+ mi := &file_proto_v1_gateway_proto_msgTypes[81]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6332,7 +6548,7 @@ func (x *FsCreateProjectFolderRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsCreateProjectFolderRequest.ProtoReflect.Descriptor instead.
func (*FsCreateProjectFolderRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{79}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{81}
}
func (x *FsCreateProjectFolderRequest) GetParent() string {
@@ -6358,7 +6574,7 @@ type FsCreateProjectFolderResponse struct {
func (x *FsCreateProjectFolderResponse) Reset() {
*x = FsCreateProjectFolderResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[80]
+ mi := &file_proto_v1_gateway_proto_msgTypes[82]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6370,7 +6586,7 @@ func (x *FsCreateProjectFolderResponse) String() string {
func (*FsCreateProjectFolderResponse) ProtoMessage() {}
func (x *FsCreateProjectFolderResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[80]
+ mi := &file_proto_v1_gateway_proto_msgTypes[82]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6383,7 +6599,7 @@ func (x *FsCreateProjectFolderResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsCreateProjectFolderResponse.ProtoReflect.Descriptor instead.
func (*FsCreateProjectFolderResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{80}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{82}
}
func (x *FsCreateProjectFolderResponse) GetPath() string {
@@ -6406,7 +6622,7 @@ type FsListRequest struct {
func (x *FsListRequest) Reset() {
*x = FsListRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[81]
+ mi := &file_proto_v1_gateway_proto_msgTypes[83]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6418,7 +6634,7 @@ func (x *FsListRequest) String() string {
func (*FsListRequest) ProtoMessage() {}
func (x *FsListRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[81]
+ mi := &file_proto_v1_gateway_proto_msgTypes[83]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6431,7 +6647,7 @@ func (x *FsListRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsListRequest.ProtoReflect.Descriptor instead.
func (*FsListRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{81}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{83}
}
func (x *FsListRequest) GetWorkdir() string {
@@ -6479,7 +6695,7 @@ type FsListEntry struct {
func (x *FsListEntry) Reset() {
*x = FsListEntry{}
- mi := &file_proto_v1_gateway_proto_msgTypes[82]
+ mi := &file_proto_v1_gateway_proto_msgTypes[84]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6491,7 +6707,7 @@ func (x *FsListEntry) String() string {
func (*FsListEntry) ProtoMessage() {}
func (x *FsListEntry) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[82]
+ mi := &file_proto_v1_gateway_proto_msgTypes[84]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6504,7 +6720,7 @@ func (x *FsListEntry) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsListEntry.ProtoReflect.Descriptor instead.
func (*FsListEntry) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{82}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{84}
}
func (x *FsListEntry) GetPath() string {
@@ -6537,7 +6753,7 @@ type FsListResponse struct {
func (x *FsListResponse) Reset() {
*x = FsListResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[83]
+ mi := &file_proto_v1_gateway_proto_msgTypes[85]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6549,7 +6765,7 @@ func (x *FsListResponse) String() string {
func (*FsListResponse) ProtoMessage() {}
func (x *FsListResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[83]
+ mi := &file_proto_v1_gateway_proto_msgTypes[85]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6562,7 +6778,7 @@ func (x *FsListResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsListResponse.ProtoReflect.Descriptor instead.
func (*FsListResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{83}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{85}
}
func (x *FsListResponse) GetPath() string {
@@ -6631,7 +6847,7 @@ type FsReadEditableTextRequest struct {
func (x *FsReadEditableTextRequest) Reset() {
*x = FsReadEditableTextRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[84]
+ mi := &file_proto_v1_gateway_proto_msgTypes[86]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6643,7 +6859,7 @@ func (x *FsReadEditableTextRequest) String() string {
func (*FsReadEditableTextRequest) ProtoMessage() {}
func (x *FsReadEditableTextRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[84]
+ mi := &file_proto_v1_gateway_proto_msgTypes[86]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6656,7 +6872,7 @@ func (x *FsReadEditableTextRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsReadEditableTextRequest.ProtoReflect.Descriptor instead.
func (*FsReadEditableTextRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{84}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{86}
}
func (x *FsReadEditableTextRequest) GetWorkdir() string {
@@ -6687,7 +6903,7 @@ type FsReadEditableTextResponse struct {
func (x *FsReadEditableTextResponse) Reset() {
*x = FsReadEditableTextResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[85]
+ mi := &file_proto_v1_gateway_proto_msgTypes[87]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6699,7 +6915,7 @@ func (x *FsReadEditableTextResponse) String() string {
func (*FsReadEditableTextResponse) ProtoMessage() {}
func (x *FsReadEditableTextResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[85]
+ mi := &file_proto_v1_gateway_proto_msgTypes[87]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6712,7 +6928,7 @@ func (x *FsReadEditableTextResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsReadEditableTextResponse.ProtoReflect.Descriptor instead.
func (*FsReadEditableTextResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{85}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{87}
}
func (x *FsReadEditableTextResponse) GetPath() string {
@@ -6767,7 +6983,7 @@ type FsReadWorkspaceImageRequest struct {
func (x *FsReadWorkspaceImageRequest) Reset() {
*x = FsReadWorkspaceImageRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[86]
+ mi := &file_proto_v1_gateway_proto_msgTypes[88]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6779,7 +6995,7 @@ func (x *FsReadWorkspaceImageRequest) String() string {
func (*FsReadWorkspaceImageRequest) ProtoMessage() {}
func (x *FsReadWorkspaceImageRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[86]
+ mi := &file_proto_v1_gateway_proto_msgTypes[88]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6792,7 +7008,7 @@ func (x *FsReadWorkspaceImageRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsReadWorkspaceImageRequest.ProtoReflect.Descriptor instead.
func (*FsReadWorkspaceImageRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{86}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{88}
}
func (x *FsReadWorkspaceImageRequest) GetWorkdir() string {
@@ -6823,7 +7039,7 @@ type FsReadWorkspaceImageResponse struct {
func (x *FsReadWorkspaceImageResponse) Reset() {
*x = FsReadWorkspaceImageResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[87]
+ mi := &file_proto_v1_gateway_proto_msgTypes[89]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6835,7 +7051,7 @@ func (x *FsReadWorkspaceImageResponse) String() string {
func (*FsReadWorkspaceImageResponse) ProtoMessage() {}
func (x *FsReadWorkspaceImageResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[87]
+ mi := &file_proto_v1_gateway_proto_msgTypes[89]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6848,7 +7064,7 @@ func (x *FsReadWorkspaceImageResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsReadWorkspaceImageResponse.ProtoReflect.Descriptor instead.
func (*FsReadWorkspaceImageResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{87}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{89}
}
func (x *FsReadWorkspaceImageResponse) GetPath() string {
@@ -6909,7 +7125,7 @@ type FsWriteTextRequest struct {
func (x *FsWriteTextRequest) Reset() {
*x = FsWriteTextRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[88]
+ mi := &file_proto_v1_gateway_proto_msgTypes[90]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6921,7 +7137,7 @@ func (x *FsWriteTextRequest) String() string {
func (*FsWriteTextRequest) ProtoMessage() {}
func (x *FsWriteTextRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[88]
+ mi := &file_proto_v1_gateway_proto_msgTypes[90]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6934,7 +7150,7 @@ func (x *FsWriteTextRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsWriteTextRequest.ProtoReflect.Descriptor instead.
func (*FsWriteTextRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{88}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{90}
}
func (x *FsWriteTextRequest) GetWorkdir() string {
@@ -7008,7 +7224,7 @@ type FsWriteTextResponse struct {
func (x *FsWriteTextResponse) Reset() {
*x = FsWriteTextResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[89]
+ mi := &file_proto_v1_gateway_proto_msgTypes[91]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -7020,7 +7236,7 @@ func (x *FsWriteTextResponse) String() string {
func (*FsWriteTextResponse) ProtoMessage() {}
func (x *FsWriteTextResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[89]
+ mi := &file_proto_v1_gateway_proto_msgTypes[91]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -7033,7 +7249,7 @@ func (x *FsWriteTextResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsWriteTextResponse.ProtoReflect.Descriptor instead.
func (*FsWriteTextResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{89}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{91}
}
func (x *FsWriteTextResponse) GetPath() string {
@@ -7095,7 +7311,7 @@ type FsCreateDirRequest struct {
func (x *FsCreateDirRequest) Reset() {
*x = FsCreateDirRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[90]
+ mi := &file_proto_v1_gateway_proto_msgTypes[92]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -7107,7 +7323,7 @@ func (x *FsCreateDirRequest) String() string {
func (*FsCreateDirRequest) ProtoMessage() {}
func (x *FsCreateDirRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[90]
+ mi := &file_proto_v1_gateway_proto_msgTypes[92]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -7120,7 +7336,7 @@ func (x *FsCreateDirRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsCreateDirRequest.ProtoReflect.Descriptor instead.
func (*FsCreateDirRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{90}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{92}
}
func (x *FsCreateDirRequest) GetWorkdir() string {
@@ -7147,7 +7363,7 @@ type FsCreateDirResponse struct {
func (x *FsCreateDirResponse) Reset() {
*x = FsCreateDirResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[91]
+ mi := &file_proto_v1_gateway_proto_msgTypes[93]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -7159,7 +7375,7 @@ func (x *FsCreateDirResponse) String() string {
func (*FsCreateDirResponse) ProtoMessage() {}
func (x *FsCreateDirResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[91]
+ mi := &file_proto_v1_gateway_proto_msgTypes[93]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -7172,7 +7388,7 @@ func (x *FsCreateDirResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsCreateDirResponse.ProtoReflect.Descriptor instead.
func (*FsCreateDirResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{91}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{93}
}
func (x *FsCreateDirResponse) GetPath() string {
@@ -7200,7 +7416,7 @@ type FsRenameRequest struct {
func (x *FsRenameRequest) Reset() {
*x = FsRenameRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[92]
+ mi := &file_proto_v1_gateway_proto_msgTypes[94]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -7212,7 +7428,7 @@ func (x *FsRenameRequest) String() string {
func (*FsRenameRequest) ProtoMessage() {}
func (x *FsRenameRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[92]
+ mi := &file_proto_v1_gateway_proto_msgTypes[94]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -7225,7 +7441,7 @@ func (x *FsRenameRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsRenameRequest.ProtoReflect.Descriptor instead.
func (*FsRenameRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{92}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{94}
}
func (x *FsRenameRequest) GetWorkdir() string {
@@ -7260,7 +7476,7 @@ type FsRenameResponse struct {
func (x *FsRenameResponse) Reset() {
*x = FsRenameResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[93]
+ mi := &file_proto_v1_gateway_proto_msgTypes[95]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -7272,7 +7488,7 @@ func (x *FsRenameResponse) String() string {
func (*FsRenameResponse) ProtoMessage() {}
func (x *FsRenameResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[93]
+ mi := &file_proto_v1_gateway_proto_msgTypes[95]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -7285,7 +7501,7 @@ func (x *FsRenameResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsRenameResponse.ProtoReflect.Descriptor instead.
func (*FsRenameResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{93}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{95}
}
func (x *FsRenameResponse) GetFromPath() string {
@@ -7319,7 +7535,7 @@ type FsDeleteRequest struct {
func (x *FsDeleteRequest) Reset() {
*x = FsDeleteRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[94]
+ mi := &file_proto_v1_gateway_proto_msgTypes[96]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -7331,7 +7547,7 @@ func (x *FsDeleteRequest) String() string {
func (*FsDeleteRequest) ProtoMessage() {}
func (x *FsDeleteRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[94]
+ mi := &file_proto_v1_gateway_proto_msgTypes[96]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -7344,7 +7560,7 @@ func (x *FsDeleteRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsDeleteRequest.ProtoReflect.Descriptor instead.
func (*FsDeleteRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{94}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{96}
}
func (x *FsDeleteRequest) GetWorkdir() string {
@@ -7371,7 +7587,7 @@ type FsDeleteResponse struct {
func (x *FsDeleteResponse) Reset() {
*x = FsDeleteResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[95]
+ mi := &file_proto_v1_gateway_proto_msgTypes[97]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -7383,7 +7599,7 @@ func (x *FsDeleteResponse) String() string {
func (*FsDeleteResponse) ProtoMessage() {}
func (x *FsDeleteResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[95]
+ mi := &file_proto_v1_gateway_proto_msgTypes[97]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -7396,7 +7612,7 @@ func (x *FsDeleteResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsDeleteResponse.ProtoReflect.Descriptor instead.
func (*FsDeleteResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{95}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{97}
}
func (x *FsDeleteResponse) GetPath() string {
@@ -7422,7 +7638,7 @@ type PingRequest struct {
func (x *PingRequest) Reset() {
*x = PingRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[96]
+ mi := &file_proto_v1_gateway_proto_msgTypes[98]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -7434,7 +7650,7 @@ func (x *PingRequest) String() string {
func (*PingRequest) ProtoMessage() {}
func (x *PingRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[96]
+ mi := &file_proto_v1_gateway_proto_msgTypes[98]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -7447,7 +7663,7 @@ func (x *PingRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use PingRequest.ProtoReflect.Descriptor instead.
func (*PingRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{96}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{98}
}
func (x *PingRequest) GetTimestamp() int64 {
@@ -7466,7 +7682,7 @@ type PongResponse struct {
func (x *PongResponse) Reset() {
*x = PongResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[97]
+ mi := &file_proto_v1_gateway_proto_msgTypes[99]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -7478,7 +7694,7 @@ func (x *PongResponse) String() string {
func (*PongResponse) ProtoMessage() {}
func (x *PongResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[97]
+ mi := &file_proto_v1_gateway_proto_msgTypes[99]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -7491,7 +7707,7 @@ func (x *PongResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use PongResponse.ProtoReflect.Descriptor instead.
func (*PongResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{97}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{99}
}
func (x *PongResponse) GetTimestamp() int64 {
@@ -7511,7 +7727,7 @@ type ErrorResponse struct {
func (x *ErrorResponse) Reset() {
*x = ErrorResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[98]
+ mi := &file_proto_v1_gateway_proto_msgTypes[100]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -7523,7 +7739,7 @@ func (x *ErrorResponse) String() string {
func (*ErrorResponse) ProtoMessage() {}
func (x *ErrorResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[98]
+ mi := &file_proto_v1_gateway_proto_msgTypes[100]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -7536,7 +7752,7 @@ func (x *ErrorResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ErrorResponse.ProtoReflect.Descriptor instead.
func (*ErrorResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{98}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{100}
}
func (x *ErrorResponse) GetCode() int32 {
@@ -7618,7 +7834,7 @@ const file_proto_v1_gateway_proto_rawDesc = "" +
"\x0etunnel_control\x18C \x01(\v2*.liveagent.gateway.v1.TunnelControlRequestH\x00R\rtunnelControl\x12]\n" +
"\x13tunnel_control_resp\x18D \x01(\v2+.liveagent.gateway.v1.TunnelControlResponseH\x00R\x11tunnelControlResp\x12F\n" +
"\ftunnel_frame\x18E \x01(\v2!.liveagent.gateway.v1.TunnelFrameH\x00R\vtunnelFrameB\t\n" +
- "\apayload\"\xa0\x1f\n" +
+ "\apayload\"\xc0 \n" +
"\rAgentEnvelope\x12\x1d\n" +
"\n" +
"request_id\x18\x01 \x01(\tR\trequestId\x12\x1c\n" +
@@ -7667,7 +7883,9 @@ const file_proto_v1_gateway_proto_rawDesc = "" +
"\x1cfs_read_workspace_image_resp\x18B \x01(\v22.liveagent.gateway.v1.FsReadWorkspaceImageResponseH\x00R\x18fsReadWorkspaceImageResp\x12S\n" +
"\x0etunnel_control\x18C \x01(\v2*.liveagent.gateway.v1.TunnelControlRequestH\x00R\rtunnelControl\x12]\n" +
"\x13tunnel_control_resp\x18D \x01(\v2+.liveagent.gateway.v1.TunnelControlResponseH\x00R\x11tunnelControlResp\x12F\n" +
- "\ftunnel_frame\x18E \x01(\v2!.liveagent.gateway.v1.TunnelFrameH\x00R\vtunnelFrame\x12;\n" +
+ "\ftunnel_frame\x18E \x01(\v2!.liveagent.gateway.v1.TunnelFrameH\x00R\vtunnelFrame\x12K\n" +
+ "\fchat_control\x18F \x01(\v2&.liveagent.gateway.v1.ChatControlEventH\x00R\vchatControl\x12Q\n" +
+ "\x0eruntime_status\x18G \x01(\v2(.liveagent.gateway.v1.RuntimeStatusEventH\x00R\rruntimeStatus\x12;\n" +
"\x05error\x18c \x01(\v2#.liveagent.gateway.v1.ErrorResponseH\x00R\x05errorB\t\n" +
"\apayload\"|\n" +
"\x11ChatSelectedModel\x12,\n" +
@@ -7852,7 +8070,25 @@ const file_proto_v1_gateway_proto_rawDesc = "" +
"\x04DONE\x10\x04\x12\t\n" +
"\x05ERROR\x10\x05\x12\x0f\n" +
"\vTOOL_STATUS\x10\x06\x12\x11\n" +
- "\rHOSTED_SEARCH\x10\a\"a\n" +
+ "\rHOSTED_SEARCH\x10\a\"\x98\x02\n" +
+ "\x10ChatControlEvent\x12\x1d\n" +
+ "\n" +
+ "request_id\x18\x01 \x01(\tR\trequestId\x12*\n" +
+ "\x11client_request_id\x18\x02 \x01(\tR\x0fclientRequestId\x12'\n" +
+ "\x0fconversation_id\x18\x03 \x01(\tR\x0econversationId\x12\x1b\n" +
+ "\trun_epoch\x18\x04 \x01(\x03R\brunEpoch\x12\x12\n" +
+ "\x04type\x18\x05 \x01(\tR\x04type\x12\x14\n" +
+ "\x05state\x18\x06 \x01(\tR\x05state\x12\x1d\n" +
+ "\n" +
+ "error_code\x18\a \x01(\tR\terrorCode\x12\x18\n" +
+ "\amessage\x18\b \x01(\tR\amessage\x12\x10\n" +
+ "\x03seq\x18\t \x01(\x03R\x03seq\"\xa9\x01\n" +
+ "\x12RuntimeStatusEvent\x12\x1b\n" +
+ "\tworker_id\x18\x01 \x01(\tR\bworkerId\x12\x14\n" +
+ "\x05state\x18\x02 \x01(\tR\x05state\x12\x18\n" +
+ "\avisible\x18\x03 \x01(\bR\avisible\x12(\n" +
+ "\x10active_run_count\x18\x04 \x01(\rR\x0eactiveRunCount\x12\x1c\n" +
+ "\ttimestamp\x18\x05 \x01(\x03R\ttimestamp\"a\n" +
"\x11CronManageRequest\x12\x16\n" +
"\x06action\x18\x01 \x01(\tR\x06action\x12\x17\n" +
"\atask_id\x18\x02 \x01(\tR\x06taskId\x12\x1b\n" +
@@ -8150,7 +8386,7 @@ func file_proto_v1_gateway_proto_rawDescGZIP() []byte {
}
var file_proto_v1_gateway_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
-var file_proto_v1_gateway_proto_msgTypes = make([]protoimpl.MessageInfo, 99)
+var file_proto_v1_gateway_proto_msgTypes = make([]protoimpl.MessageInfo, 101)
var file_proto_v1_gateway_proto_goTypes = []any{
(TunnelFrameKind)(0), // 0: liveagent.gateway.v1.TunnelFrameKind
(ChatEvent_ChatEventType)(0), // 1: liveagent.gateway.v1.ChatEvent.ChatEventType
@@ -8183,198 +8419,202 @@ var file_proto_v1_gateway_proto_goTypes = []any{
(*ChatRequest)(nil), // 28: liveagent.gateway.v1.ChatRequest
(*CancelChatRequest)(nil), // 29: liveagent.gateway.v1.CancelChatRequest
(*ChatEvent)(nil), // 30: liveagent.gateway.v1.ChatEvent
- (*CronManageRequest)(nil), // 31: liveagent.gateway.v1.CronManageRequest
- (*CronManageResponse)(nil), // 32: liveagent.gateway.v1.CronManageResponse
- (*HistoryListRequest)(nil), // 33: liveagent.gateway.v1.HistoryListRequest
- (*HistoryListResponse)(nil), // 34: liveagent.gateway.v1.HistoryListResponse
- (*ConversationSummary)(nil), // 35: liveagent.gateway.v1.ConversationSummary
- (*HistoryGetRequest)(nil), // 36: liveagent.gateway.v1.HistoryGetRequest
- (*HistoryGetResponse)(nil), // 37: liveagent.gateway.v1.HistoryGetResponse
- (*HistoryRenameRequest)(nil), // 38: liveagent.gateway.v1.HistoryRenameRequest
- (*HistoryRenameResponse)(nil), // 39: liveagent.gateway.v1.HistoryRenameResponse
- (*HistoryPinRequest)(nil), // 40: liveagent.gateway.v1.HistoryPinRequest
- (*HistoryPinResponse)(nil), // 41: liveagent.gateway.v1.HistoryPinResponse
- (*HistoryShareStatus)(nil), // 42: liveagent.gateway.v1.HistoryShareStatus
- (*HistoryShareGetRequest)(nil), // 43: liveagent.gateway.v1.HistoryShareGetRequest
- (*HistoryShareGetResponse)(nil), // 44: liveagent.gateway.v1.HistoryShareGetResponse
- (*HistoryShareSetRequest)(nil), // 45: liveagent.gateway.v1.HistoryShareSetRequest
- (*HistoryShareSetResponse)(nil), // 46: liveagent.gateway.v1.HistoryShareSetResponse
- (*HistoryShareResolveRequest)(nil), // 47: liveagent.gateway.v1.HistoryShareResolveRequest
- (*HistoryShareResolveResponse)(nil), // 48: liveagent.gateway.v1.HistoryShareResolveResponse
- (*HistoryWorkdirsRequest)(nil), // 49: liveagent.gateway.v1.HistoryWorkdirsRequest
- (*HistoryWorkdirSummary)(nil), // 50: liveagent.gateway.v1.HistoryWorkdirSummary
- (*HistoryWorkdirsResponse)(nil), // 51: liveagent.gateway.v1.HistoryWorkdirsResponse
- (*HistoryDeleteRequest)(nil), // 52: liveagent.gateway.v1.HistoryDeleteRequest
- (*HistoryDeleteResponse)(nil), // 53: liveagent.gateway.v1.HistoryDeleteResponse
- (*HistoryTruncateRequest)(nil), // 54: liveagent.gateway.v1.HistoryTruncateRequest
- (*HistoryTruncateResponse)(nil), // 55: liveagent.gateway.v1.HistoryTruncateResponse
- (*HistorySyncEvent)(nil), // 56: liveagent.gateway.v1.HistorySyncEvent
- (*ProviderListRequest)(nil), // 57: liveagent.gateway.v1.ProviderListRequest
- (*ProviderListResponse)(nil), // 58: liveagent.gateway.v1.ProviderListResponse
- (*SettingsGetRequest)(nil), // 59: liveagent.gateway.v1.SettingsGetRequest
- (*SettingsGetResponse)(nil), // 60: liveagent.gateway.v1.SettingsGetResponse
- (*SettingsUpdateRequest)(nil), // 61: liveagent.gateway.v1.SettingsUpdateRequest
- (*SettingsUpdateResponse)(nil), // 62: liveagent.gateway.v1.SettingsUpdateResponse
- (*SettingsSyncEvent)(nil), // 63: liveagent.gateway.v1.SettingsSyncEvent
- (*SkillFilesListRequest)(nil), // 64: liveagent.gateway.v1.SkillFilesListRequest
- (*SkillFilesListResponse)(nil), // 65: liveagent.gateway.v1.SkillFilesListResponse
- (*SkillMetadataReadRequest)(nil), // 66: liveagent.gateway.v1.SkillMetadataReadRequest
- (*SkillMetadataReadResponse)(nil), // 67: liveagent.gateway.v1.SkillMetadataReadResponse
- (*SkillTextReadRequest)(nil), // 68: liveagent.gateway.v1.SkillTextReadRequest
- (*SkillTextReadResponse)(nil), // 69: liveagent.gateway.v1.SkillTextReadResponse
- (*SkillManageRequest)(nil), // 70: liveagent.gateway.v1.SkillManageRequest
- (*SkillManageResponse)(nil), // 71: liveagent.gateway.v1.SkillManageResponse
- (*FileMentionListRequest)(nil), // 72: liveagent.gateway.v1.FileMentionListRequest
- (*FileMentionEntry)(nil), // 73: liveagent.gateway.v1.FileMentionEntry
- (*FileMentionListResponse)(nil), // 74: liveagent.gateway.v1.FileMentionListResponse
- (*FsRoot)(nil), // 75: liveagent.gateway.v1.FsRoot
- (*FsRootsRequest)(nil), // 76: liveagent.gateway.v1.FsRootsRequest
- (*FsRootsResponse)(nil), // 77: liveagent.gateway.v1.FsRootsResponse
- (*FsListDirsRequest)(nil), // 78: liveagent.gateway.v1.FsListDirsRequest
- (*FsDirEntry)(nil), // 79: liveagent.gateway.v1.FsDirEntry
- (*FsListDirsResponse)(nil), // 80: liveagent.gateway.v1.FsListDirsResponse
- (*FsCreateProjectFolderRequest)(nil), // 81: liveagent.gateway.v1.FsCreateProjectFolderRequest
- (*FsCreateProjectFolderResponse)(nil), // 82: liveagent.gateway.v1.FsCreateProjectFolderResponse
- (*FsListRequest)(nil), // 83: liveagent.gateway.v1.FsListRequest
- (*FsListEntry)(nil), // 84: liveagent.gateway.v1.FsListEntry
- (*FsListResponse)(nil), // 85: liveagent.gateway.v1.FsListResponse
- (*FsReadEditableTextRequest)(nil), // 86: liveagent.gateway.v1.FsReadEditableTextRequest
- (*FsReadEditableTextResponse)(nil), // 87: liveagent.gateway.v1.FsReadEditableTextResponse
- (*FsReadWorkspaceImageRequest)(nil), // 88: liveagent.gateway.v1.FsReadWorkspaceImageRequest
- (*FsReadWorkspaceImageResponse)(nil), // 89: liveagent.gateway.v1.FsReadWorkspaceImageResponse
- (*FsWriteTextRequest)(nil), // 90: liveagent.gateway.v1.FsWriteTextRequest
- (*FsWriteTextResponse)(nil), // 91: liveagent.gateway.v1.FsWriteTextResponse
- (*FsCreateDirRequest)(nil), // 92: liveagent.gateway.v1.FsCreateDirRequest
- (*FsCreateDirResponse)(nil), // 93: liveagent.gateway.v1.FsCreateDirResponse
- (*FsRenameRequest)(nil), // 94: liveagent.gateway.v1.FsRenameRequest
- (*FsRenameResponse)(nil), // 95: liveagent.gateway.v1.FsRenameResponse
- (*FsDeleteRequest)(nil), // 96: liveagent.gateway.v1.FsDeleteRequest
- (*FsDeleteResponse)(nil), // 97: liveagent.gateway.v1.FsDeleteResponse
- (*PingRequest)(nil), // 98: liveagent.gateway.v1.PingRequest
- (*PongResponse)(nil), // 99: liveagent.gateway.v1.PongResponse
- (*ErrorResponse)(nil), // 100: liveagent.gateway.v1.ErrorResponse
+ (*ChatControlEvent)(nil), // 31: liveagent.gateway.v1.ChatControlEvent
+ (*RuntimeStatusEvent)(nil), // 32: liveagent.gateway.v1.RuntimeStatusEvent
+ (*CronManageRequest)(nil), // 33: liveagent.gateway.v1.CronManageRequest
+ (*CronManageResponse)(nil), // 34: liveagent.gateway.v1.CronManageResponse
+ (*HistoryListRequest)(nil), // 35: liveagent.gateway.v1.HistoryListRequest
+ (*HistoryListResponse)(nil), // 36: liveagent.gateway.v1.HistoryListResponse
+ (*ConversationSummary)(nil), // 37: liveagent.gateway.v1.ConversationSummary
+ (*HistoryGetRequest)(nil), // 38: liveagent.gateway.v1.HistoryGetRequest
+ (*HistoryGetResponse)(nil), // 39: liveagent.gateway.v1.HistoryGetResponse
+ (*HistoryRenameRequest)(nil), // 40: liveagent.gateway.v1.HistoryRenameRequest
+ (*HistoryRenameResponse)(nil), // 41: liveagent.gateway.v1.HistoryRenameResponse
+ (*HistoryPinRequest)(nil), // 42: liveagent.gateway.v1.HistoryPinRequest
+ (*HistoryPinResponse)(nil), // 43: liveagent.gateway.v1.HistoryPinResponse
+ (*HistoryShareStatus)(nil), // 44: liveagent.gateway.v1.HistoryShareStatus
+ (*HistoryShareGetRequest)(nil), // 45: liveagent.gateway.v1.HistoryShareGetRequest
+ (*HistoryShareGetResponse)(nil), // 46: liveagent.gateway.v1.HistoryShareGetResponse
+ (*HistoryShareSetRequest)(nil), // 47: liveagent.gateway.v1.HistoryShareSetRequest
+ (*HistoryShareSetResponse)(nil), // 48: liveagent.gateway.v1.HistoryShareSetResponse
+ (*HistoryShareResolveRequest)(nil), // 49: liveagent.gateway.v1.HistoryShareResolveRequest
+ (*HistoryShareResolveResponse)(nil), // 50: liveagent.gateway.v1.HistoryShareResolveResponse
+ (*HistoryWorkdirsRequest)(nil), // 51: liveagent.gateway.v1.HistoryWorkdirsRequest
+ (*HistoryWorkdirSummary)(nil), // 52: liveagent.gateway.v1.HistoryWorkdirSummary
+ (*HistoryWorkdirsResponse)(nil), // 53: liveagent.gateway.v1.HistoryWorkdirsResponse
+ (*HistoryDeleteRequest)(nil), // 54: liveagent.gateway.v1.HistoryDeleteRequest
+ (*HistoryDeleteResponse)(nil), // 55: liveagent.gateway.v1.HistoryDeleteResponse
+ (*HistoryTruncateRequest)(nil), // 56: liveagent.gateway.v1.HistoryTruncateRequest
+ (*HistoryTruncateResponse)(nil), // 57: liveagent.gateway.v1.HistoryTruncateResponse
+ (*HistorySyncEvent)(nil), // 58: liveagent.gateway.v1.HistorySyncEvent
+ (*ProviderListRequest)(nil), // 59: liveagent.gateway.v1.ProviderListRequest
+ (*ProviderListResponse)(nil), // 60: liveagent.gateway.v1.ProviderListResponse
+ (*SettingsGetRequest)(nil), // 61: liveagent.gateway.v1.SettingsGetRequest
+ (*SettingsGetResponse)(nil), // 62: liveagent.gateway.v1.SettingsGetResponse
+ (*SettingsUpdateRequest)(nil), // 63: liveagent.gateway.v1.SettingsUpdateRequest
+ (*SettingsUpdateResponse)(nil), // 64: liveagent.gateway.v1.SettingsUpdateResponse
+ (*SettingsSyncEvent)(nil), // 65: liveagent.gateway.v1.SettingsSyncEvent
+ (*SkillFilesListRequest)(nil), // 66: liveagent.gateway.v1.SkillFilesListRequest
+ (*SkillFilesListResponse)(nil), // 67: liveagent.gateway.v1.SkillFilesListResponse
+ (*SkillMetadataReadRequest)(nil), // 68: liveagent.gateway.v1.SkillMetadataReadRequest
+ (*SkillMetadataReadResponse)(nil), // 69: liveagent.gateway.v1.SkillMetadataReadResponse
+ (*SkillTextReadRequest)(nil), // 70: liveagent.gateway.v1.SkillTextReadRequest
+ (*SkillTextReadResponse)(nil), // 71: liveagent.gateway.v1.SkillTextReadResponse
+ (*SkillManageRequest)(nil), // 72: liveagent.gateway.v1.SkillManageRequest
+ (*SkillManageResponse)(nil), // 73: liveagent.gateway.v1.SkillManageResponse
+ (*FileMentionListRequest)(nil), // 74: liveagent.gateway.v1.FileMentionListRequest
+ (*FileMentionEntry)(nil), // 75: liveagent.gateway.v1.FileMentionEntry
+ (*FileMentionListResponse)(nil), // 76: liveagent.gateway.v1.FileMentionListResponse
+ (*FsRoot)(nil), // 77: liveagent.gateway.v1.FsRoot
+ (*FsRootsRequest)(nil), // 78: liveagent.gateway.v1.FsRootsRequest
+ (*FsRootsResponse)(nil), // 79: liveagent.gateway.v1.FsRootsResponse
+ (*FsListDirsRequest)(nil), // 80: liveagent.gateway.v1.FsListDirsRequest
+ (*FsDirEntry)(nil), // 81: liveagent.gateway.v1.FsDirEntry
+ (*FsListDirsResponse)(nil), // 82: liveagent.gateway.v1.FsListDirsResponse
+ (*FsCreateProjectFolderRequest)(nil), // 83: liveagent.gateway.v1.FsCreateProjectFolderRequest
+ (*FsCreateProjectFolderResponse)(nil), // 84: liveagent.gateway.v1.FsCreateProjectFolderResponse
+ (*FsListRequest)(nil), // 85: liveagent.gateway.v1.FsListRequest
+ (*FsListEntry)(nil), // 86: liveagent.gateway.v1.FsListEntry
+ (*FsListResponse)(nil), // 87: liveagent.gateway.v1.FsListResponse
+ (*FsReadEditableTextRequest)(nil), // 88: liveagent.gateway.v1.FsReadEditableTextRequest
+ (*FsReadEditableTextResponse)(nil), // 89: liveagent.gateway.v1.FsReadEditableTextResponse
+ (*FsReadWorkspaceImageRequest)(nil), // 90: liveagent.gateway.v1.FsReadWorkspaceImageRequest
+ (*FsReadWorkspaceImageResponse)(nil), // 91: liveagent.gateway.v1.FsReadWorkspaceImageResponse
+ (*FsWriteTextRequest)(nil), // 92: liveagent.gateway.v1.FsWriteTextRequest
+ (*FsWriteTextResponse)(nil), // 93: liveagent.gateway.v1.FsWriteTextResponse
+ (*FsCreateDirRequest)(nil), // 94: liveagent.gateway.v1.FsCreateDirRequest
+ (*FsCreateDirResponse)(nil), // 95: liveagent.gateway.v1.FsCreateDirResponse
+ (*FsRenameRequest)(nil), // 96: liveagent.gateway.v1.FsRenameRequest
+ (*FsRenameResponse)(nil), // 97: liveagent.gateway.v1.FsRenameResponse
+ (*FsDeleteRequest)(nil), // 98: liveagent.gateway.v1.FsDeleteRequest
+ (*FsDeleteResponse)(nil), // 99: liveagent.gateway.v1.FsDeleteResponse
+ (*PingRequest)(nil), // 100: liveagent.gateway.v1.PingRequest
+ (*PongResponse)(nil), // 101: liveagent.gateway.v1.PongResponse
+ (*ErrorResponse)(nil), // 102: liveagent.gateway.v1.ErrorResponse
}
var file_proto_v1_gateway_proto_depIdxs = []int32{
28, // 0: liveagent.gateway.v1.GatewayEnvelope.chat_request:type_name -> liveagent.gateway.v1.ChatRequest
29, // 1: liveagent.gateway.v1.GatewayEnvelope.cancel_chat:type_name -> liveagent.gateway.v1.CancelChatRequest
- 31, // 2: liveagent.gateway.v1.GatewayEnvelope.cron_manage:type_name -> liveagent.gateway.v1.CronManageRequest
- 33, // 3: liveagent.gateway.v1.GatewayEnvelope.history_list:type_name -> liveagent.gateway.v1.HistoryListRequest
- 36, // 4: liveagent.gateway.v1.GatewayEnvelope.history_get:type_name -> liveagent.gateway.v1.HistoryGetRequest
- 38, // 5: liveagent.gateway.v1.GatewayEnvelope.history_rename:type_name -> liveagent.gateway.v1.HistoryRenameRequest
- 52, // 6: liveagent.gateway.v1.GatewayEnvelope.history_delete:type_name -> liveagent.gateway.v1.HistoryDeleteRequest
- 54, // 7: liveagent.gateway.v1.GatewayEnvelope.history_truncate:type_name -> liveagent.gateway.v1.HistoryTruncateRequest
- 40, // 8: liveagent.gateway.v1.GatewayEnvelope.history_pin:type_name -> liveagent.gateway.v1.HistoryPinRequest
- 43, // 9: liveagent.gateway.v1.GatewayEnvelope.history_share_get:type_name -> liveagent.gateway.v1.HistoryShareGetRequest
- 45, // 10: liveagent.gateway.v1.GatewayEnvelope.history_share_set:type_name -> liveagent.gateway.v1.HistoryShareSetRequest
- 47, // 11: liveagent.gateway.v1.GatewayEnvelope.history_share_resolve:type_name -> liveagent.gateway.v1.HistoryShareResolveRequest
- 49, // 12: liveagent.gateway.v1.GatewayEnvelope.history_workdirs:type_name -> liveagent.gateway.v1.HistoryWorkdirsRequest
- 57, // 13: liveagent.gateway.v1.GatewayEnvelope.provider_list:type_name -> liveagent.gateway.v1.ProviderListRequest
- 59, // 14: liveagent.gateway.v1.GatewayEnvelope.settings_get:type_name -> liveagent.gateway.v1.SettingsGetRequest
- 61, // 15: liveagent.gateway.v1.GatewayEnvelope.settings_update:type_name -> liveagent.gateway.v1.SettingsUpdateRequest
- 64, // 16: liveagent.gateway.v1.GatewayEnvelope.skill_files_list:type_name -> liveagent.gateway.v1.SkillFilesListRequest
- 66, // 17: liveagent.gateway.v1.GatewayEnvelope.skill_metadata_read:type_name -> liveagent.gateway.v1.SkillMetadataReadRequest
- 68, // 18: liveagent.gateway.v1.GatewayEnvelope.skill_text_read:type_name -> liveagent.gateway.v1.SkillTextReadRequest
- 72, // 19: liveagent.gateway.v1.GatewayEnvelope.file_mention_list:type_name -> liveagent.gateway.v1.FileMentionListRequest
+ 33, // 2: liveagent.gateway.v1.GatewayEnvelope.cron_manage:type_name -> liveagent.gateway.v1.CronManageRequest
+ 35, // 3: liveagent.gateway.v1.GatewayEnvelope.history_list:type_name -> liveagent.gateway.v1.HistoryListRequest
+ 38, // 4: liveagent.gateway.v1.GatewayEnvelope.history_get:type_name -> liveagent.gateway.v1.HistoryGetRequest
+ 40, // 5: liveagent.gateway.v1.GatewayEnvelope.history_rename:type_name -> liveagent.gateway.v1.HistoryRenameRequest
+ 54, // 6: liveagent.gateway.v1.GatewayEnvelope.history_delete:type_name -> liveagent.gateway.v1.HistoryDeleteRequest
+ 56, // 7: liveagent.gateway.v1.GatewayEnvelope.history_truncate:type_name -> liveagent.gateway.v1.HistoryTruncateRequest
+ 42, // 8: liveagent.gateway.v1.GatewayEnvelope.history_pin:type_name -> liveagent.gateway.v1.HistoryPinRequest
+ 45, // 9: liveagent.gateway.v1.GatewayEnvelope.history_share_get:type_name -> liveagent.gateway.v1.HistoryShareGetRequest
+ 47, // 10: liveagent.gateway.v1.GatewayEnvelope.history_share_set:type_name -> liveagent.gateway.v1.HistoryShareSetRequest
+ 49, // 11: liveagent.gateway.v1.GatewayEnvelope.history_share_resolve:type_name -> liveagent.gateway.v1.HistoryShareResolveRequest
+ 51, // 12: liveagent.gateway.v1.GatewayEnvelope.history_workdirs:type_name -> liveagent.gateway.v1.HistoryWorkdirsRequest
+ 59, // 13: liveagent.gateway.v1.GatewayEnvelope.provider_list:type_name -> liveagent.gateway.v1.ProviderListRequest
+ 61, // 14: liveagent.gateway.v1.GatewayEnvelope.settings_get:type_name -> liveagent.gateway.v1.SettingsGetRequest
+ 63, // 15: liveagent.gateway.v1.GatewayEnvelope.settings_update:type_name -> liveagent.gateway.v1.SettingsUpdateRequest
+ 66, // 16: liveagent.gateway.v1.GatewayEnvelope.skill_files_list:type_name -> liveagent.gateway.v1.SkillFilesListRequest
+ 68, // 17: liveagent.gateway.v1.GatewayEnvelope.skill_metadata_read:type_name -> liveagent.gateway.v1.SkillMetadataReadRequest
+ 70, // 18: liveagent.gateway.v1.GatewayEnvelope.skill_text_read:type_name -> liveagent.gateway.v1.SkillTextReadRequest
+ 74, // 19: liveagent.gateway.v1.GatewayEnvelope.file_mention_list:type_name -> liveagent.gateway.v1.FileMentionListRequest
10, // 20: liveagent.gateway.v1.GatewayEnvelope.upload_readable_files:type_name -> liveagent.gateway.v1.UploadReadableFilesRequest
- 76, // 21: liveagent.gateway.v1.GatewayEnvelope.fs_roots:type_name -> liveagent.gateway.v1.FsRootsRequest
- 78, // 22: liveagent.gateway.v1.GatewayEnvelope.fs_list_dirs:type_name -> liveagent.gateway.v1.FsListDirsRequest
- 98, // 23: liveagent.gateway.v1.GatewayEnvelope.ping:type_name -> liveagent.gateway.v1.PingRequest
+ 78, // 21: liveagent.gateway.v1.GatewayEnvelope.fs_roots:type_name -> liveagent.gateway.v1.FsRootsRequest
+ 80, // 22: liveagent.gateway.v1.GatewayEnvelope.fs_list_dirs:type_name -> liveagent.gateway.v1.FsListDirsRequest
+ 100, // 23: liveagent.gateway.v1.GatewayEnvelope.ping:type_name -> liveagent.gateway.v1.PingRequest
12, // 24: liveagent.gateway.v1.GatewayEnvelope.uploaded_image_preview:type_name -> liveagent.gateway.v1.UploadedImagePreviewRequest
19, // 25: liveagent.gateway.v1.GatewayEnvelope.memory_manage:type_name -> liveagent.gateway.v1.MemoryManageRequest
- 70, // 26: liveagent.gateway.v1.GatewayEnvelope.skill_manage:type_name -> liveagent.gateway.v1.SkillManageRequest
- 81, // 27: liveagent.gateway.v1.GatewayEnvelope.fs_create_project_folder:type_name -> liveagent.gateway.v1.FsCreateProjectFolderRequest
+ 72, // 26: liveagent.gateway.v1.GatewayEnvelope.skill_manage:type_name -> liveagent.gateway.v1.SkillManageRequest
+ 83, // 27: liveagent.gateway.v1.GatewayEnvelope.fs_create_project_folder:type_name -> liveagent.gateway.v1.FsCreateProjectFolderRequest
21, // 28: liveagent.gateway.v1.GatewayEnvelope.terminal_request:type_name -> liveagent.gateway.v1.TerminalRequest
- 83, // 29: liveagent.gateway.v1.GatewayEnvelope.fs_list:type_name -> liveagent.gateway.v1.FsListRequest
- 90, // 30: liveagent.gateway.v1.GatewayEnvelope.fs_write_text:type_name -> liveagent.gateway.v1.FsWriteTextRequest
- 92, // 31: liveagent.gateway.v1.GatewayEnvelope.fs_create_dir:type_name -> liveagent.gateway.v1.FsCreateDirRequest
- 94, // 32: liveagent.gateway.v1.GatewayEnvelope.fs_rename:type_name -> liveagent.gateway.v1.FsRenameRequest
- 96, // 33: liveagent.gateway.v1.GatewayEnvelope.fs_delete:type_name -> liveagent.gateway.v1.FsDeleteRequest
+ 85, // 29: liveagent.gateway.v1.GatewayEnvelope.fs_list:type_name -> liveagent.gateway.v1.FsListRequest
+ 92, // 30: liveagent.gateway.v1.GatewayEnvelope.fs_write_text:type_name -> liveagent.gateway.v1.FsWriteTextRequest
+ 94, // 31: liveagent.gateway.v1.GatewayEnvelope.fs_create_dir:type_name -> liveagent.gateway.v1.FsCreateDirRequest
+ 96, // 32: liveagent.gateway.v1.GatewayEnvelope.fs_rename:type_name -> liveagent.gateway.v1.FsRenameRequest
+ 98, // 33: liveagent.gateway.v1.GatewayEnvelope.fs_delete:type_name -> liveagent.gateway.v1.FsDeleteRequest
26, // 34: liveagent.gateway.v1.GatewayEnvelope.git_request:type_name -> liveagent.gateway.v1.GitRequest
- 86, // 35: liveagent.gateway.v1.GatewayEnvelope.fs_read_editable_text:type_name -> liveagent.gateway.v1.FsReadEditableTextRequest
- 88, // 36: liveagent.gateway.v1.GatewayEnvelope.fs_read_workspace_image:type_name -> liveagent.gateway.v1.FsReadWorkspaceImageRequest
+ 88, // 35: liveagent.gateway.v1.GatewayEnvelope.fs_read_editable_text:type_name -> liveagent.gateway.v1.FsReadEditableTextRequest
+ 90, // 36: liveagent.gateway.v1.GatewayEnvelope.fs_read_workspace_image:type_name -> liveagent.gateway.v1.FsReadWorkspaceImageRequest
14, // 37: liveagent.gateway.v1.GatewayEnvelope.tunnel_control:type_name -> liveagent.gateway.v1.TunnelControlRequest
15, // 38: liveagent.gateway.v1.GatewayEnvelope.tunnel_control_resp:type_name -> liveagent.gateway.v1.TunnelControlResponse
18, // 39: liveagent.gateway.v1.GatewayEnvelope.tunnel_frame:type_name -> liveagent.gateway.v1.TunnelFrame
30, // 40: liveagent.gateway.v1.AgentEnvelope.chat_event:type_name -> liveagent.gateway.v1.ChatEvent
- 32, // 41: liveagent.gateway.v1.AgentEnvelope.cron_manage_resp:type_name -> liveagent.gateway.v1.CronManageResponse
- 34, // 42: liveagent.gateway.v1.AgentEnvelope.history_list_resp:type_name -> liveagent.gateway.v1.HistoryListResponse
- 37, // 43: liveagent.gateway.v1.AgentEnvelope.history_get_resp:type_name -> liveagent.gateway.v1.HistoryGetResponse
- 39, // 44: liveagent.gateway.v1.AgentEnvelope.history_rename_resp:type_name -> liveagent.gateway.v1.HistoryRenameResponse
- 53, // 45: liveagent.gateway.v1.AgentEnvelope.history_delete_resp:type_name -> liveagent.gateway.v1.HistoryDeleteResponse
- 56, // 46: liveagent.gateway.v1.AgentEnvelope.history_sync:type_name -> liveagent.gateway.v1.HistorySyncEvent
- 55, // 47: liveagent.gateway.v1.AgentEnvelope.history_truncate_resp:type_name -> liveagent.gateway.v1.HistoryTruncateResponse
- 41, // 48: liveagent.gateway.v1.AgentEnvelope.history_pin_resp:type_name -> liveagent.gateway.v1.HistoryPinResponse
- 44, // 49: liveagent.gateway.v1.AgentEnvelope.history_share_get_resp:type_name -> liveagent.gateway.v1.HistoryShareGetResponse
- 46, // 50: liveagent.gateway.v1.AgentEnvelope.history_share_set_resp:type_name -> liveagent.gateway.v1.HistoryShareSetResponse
- 48, // 51: liveagent.gateway.v1.AgentEnvelope.history_share_resolve_resp:type_name -> liveagent.gateway.v1.HistoryShareResolveResponse
- 51, // 52: liveagent.gateway.v1.AgentEnvelope.history_workdirs_resp:type_name -> liveagent.gateway.v1.HistoryWorkdirsResponse
- 58, // 53: liveagent.gateway.v1.AgentEnvelope.provider_list_resp:type_name -> liveagent.gateway.v1.ProviderListResponse
- 60, // 54: liveagent.gateway.v1.AgentEnvelope.settings_get_resp:type_name -> liveagent.gateway.v1.SettingsGetResponse
- 62, // 55: liveagent.gateway.v1.AgentEnvelope.settings_update_resp:type_name -> liveagent.gateway.v1.SettingsUpdateResponse
- 63, // 56: liveagent.gateway.v1.AgentEnvelope.settings_sync:type_name -> liveagent.gateway.v1.SettingsSyncEvent
- 65, // 57: liveagent.gateway.v1.AgentEnvelope.skill_files_list_resp:type_name -> liveagent.gateway.v1.SkillFilesListResponse
- 67, // 58: liveagent.gateway.v1.AgentEnvelope.skill_metadata_read_resp:type_name -> liveagent.gateway.v1.SkillMetadataReadResponse
- 69, // 59: liveagent.gateway.v1.AgentEnvelope.skill_text_read_resp:type_name -> liveagent.gateway.v1.SkillTextReadResponse
- 74, // 60: liveagent.gateway.v1.AgentEnvelope.file_mention_list_resp:type_name -> liveagent.gateway.v1.FileMentionListResponse
+ 34, // 41: liveagent.gateway.v1.AgentEnvelope.cron_manage_resp:type_name -> liveagent.gateway.v1.CronManageResponse
+ 36, // 42: liveagent.gateway.v1.AgentEnvelope.history_list_resp:type_name -> liveagent.gateway.v1.HistoryListResponse
+ 39, // 43: liveagent.gateway.v1.AgentEnvelope.history_get_resp:type_name -> liveagent.gateway.v1.HistoryGetResponse
+ 41, // 44: liveagent.gateway.v1.AgentEnvelope.history_rename_resp:type_name -> liveagent.gateway.v1.HistoryRenameResponse
+ 55, // 45: liveagent.gateway.v1.AgentEnvelope.history_delete_resp:type_name -> liveagent.gateway.v1.HistoryDeleteResponse
+ 58, // 46: liveagent.gateway.v1.AgentEnvelope.history_sync:type_name -> liveagent.gateway.v1.HistorySyncEvent
+ 57, // 47: liveagent.gateway.v1.AgentEnvelope.history_truncate_resp:type_name -> liveagent.gateway.v1.HistoryTruncateResponse
+ 43, // 48: liveagent.gateway.v1.AgentEnvelope.history_pin_resp:type_name -> liveagent.gateway.v1.HistoryPinResponse
+ 46, // 49: liveagent.gateway.v1.AgentEnvelope.history_share_get_resp:type_name -> liveagent.gateway.v1.HistoryShareGetResponse
+ 48, // 50: liveagent.gateway.v1.AgentEnvelope.history_share_set_resp:type_name -> liveagent.gateway.v1.HistoryShareSetResponse
+ 50, // 51: liveagent.gateway.v1.AgentEnvelope.history_share_resolve_resp:type_name -> liveagent.gateway.v1.HistoryShareResolveResponse
+ 53, // 52: liveagent.gateway.v1.AgentEnvelope.history_workdirs_resp:type_name -> liveagent.gateway.v1.HistoryWorkdirsResponse
+ 60, // 53: liveagent.gateway.v1.AgentEnvelope.provider_list_resp:type_name -> liveagent.gateway.v1.ProviderListResponse
+ 62, // 54: liveagent.gateway.v1.AgentEnvelope.settings_get_resp:type_name -> liveagent.gateway.v1.SettingsGetResponse
+ 64, // 55: liveagent.gateway.v1.AgentEnvelope.settings_update_resp:type_name -> liveagent.gateway.v1.SettingsUpdateResponse
+ 65, // 56: liveagent.gateway.v1.AgentEnvelope.settings_sync:type_name -> liveagent.gateway.v1.SettingsSyncEvent
+ 67, // 57: liveagent.gateway.v1.AgentEnvelope.skill_files_list_resp:type_name -> liveagent.gateway.v1.SkillFilesListResponse
+ 69, // 58: liveagent.gateway.v1.AgentEnvelope.skill_metadata_read_resp:type_name -> liveagent.gateway.v1.SkillMetadataReadResponse
+ 71, // 59: liveagent.gateway.v1.AgentEnvelope.skill_text_read_resp:type_name -> liveagent.gateway.v1.SkillTextReadResponse
+ 76, // 60: liveagent.gateway.v1.AgentEnvelope.file_mention_list_resp:type_name -> liveagent.gateway.v1.FileMentionListResponse
11, // 61: liveagent.gateway.v1.AgentEnvelope.upload_readable_files_resp:type_name -> liveagent.gateway.v1.UploadReadableFilesResponse
- 77, // 62: liveagent.gateway.v1.AgentEnvelope.fs_roots_resp:type_name -> liveagent.gateway.v1.FsRootsResponse
- 99, // 63: liveagent.gateway.v1.AgentEnvelope.pong:type_name -> liveagent.gateway.v1.PongResponse
- 80, // 64: liveagent.gateway.v1.AgentEnvelope.fs_list_dirs_resp:type_name -> liveagent.gateway.v1.FsListDirsResponse
+ 79, // 62: liveagent.gateway.v1.AgentEnvelope.fs_roots_resp:type_name -> liveagent.gateway.v1.FsRootsResponse
+ 101, // 63: liveagent.gateway.v1.AgentEnvelope.pong:type_name -> liveagent.gateway.v1.PongResponse
+ 82, // 64: liveagent.gateway.v1.AgentEnvelope.fs_list_dirs_resp:type_name -> liveagent.gateway.v1.FsListDirsResponse
13, // 65: liveagent.gateway.v1.AgentEnvelope.uploaded_image_preview_resp:type_name -> liveagent.gateway.v1.UploadedImagePreviewResponse
20, // 66: liveagent.gateway.v1.AgentEnvelope.memory_manage_resp:type_name -> liveagent.gateway.v1.MemoryManageResponse
- 71, // 67: liveagent.gateway.v1.AgentEnvelope.skill_manage_resp:type_name -> liveagent.gateway.v1.SkillManageResponse
- 82, // 68: liveagent.gateway.v1.AgentEnvelope.fs_create_project_folder_resp:type_name -> liveagent.gateway.v1.FsCreateProjectFolderResponse
+ 73, // 67: liveagent.gateway.v1.AgentEnvelope.skill_manage_resp:type_name -> liveagent.gateway.v1.SkillManageResponse
+ 84, // 68: liveagent.gateway.v1.AgentEnvelope.fs_create_project_folder_resp:type_name -> liveagent.gateway.v1.FsCreateProjectFolderResponse
24, // 69: liveagent.gateway.v1.AgentEnvelope.terminal_response:type_name -> liveagent.gateway.v1.TerminalResponse
25, // 70: liveagent.gateway.v1.AgentEnvelope.terminal_event:type_name -> liveagent.gateway.v1.TerminalEvent
- 85, // 71: liveagent.gateway.v1.AgentEnvelope.fs_list_resp:type_name -> liveagent.gateway.v1.FsListResponse
- 91, // 72: liveagent.gateway.v1.AgentEnvelope.fs_write_text_resp:type_name -> liveagent.gateway.v1.FsWriteTextResponse
- 93, // 73: liveagent.gateway.v1.AgentEnvelope.fs_create_dir_resp:type_name -> liveagent.gateway.v1.FsCreateDirResponse
- 95, // 74: liveagent.gateway.v1.AgentEnvelope.fs_rename_resp:type_name -> liveagent.gateway.v1.FsRenameResponse
- 97, // 75: liveagent.gateway.v1.AgentEnvelope.fs_delete_resp:type_name -> liveagent.gateway.v1.FsDeleteResponse
+ 87, // 71: liveagent.gateway.v1.AgentEnvelope.fs_list_resp:type_name -> liveagent.gateway.v1.FsListResponse
+ 93, // 72: liveagent.gateway.v1.AgentEnvelope.fs_write_text_resp:type_name -> liveagent.gateway.v1.FsWriteTextResponse
+ 95, // 73: liveagent.gateway.v1.AgentEnvelope.fs_create_dir_resp:type_name -> liveagent.gateway.v1.FsCreateDirResponse
+ 97, // 74: liveagent.gateway.v1.AgentEnvelope.fs_rename_resp:type_name -> liveagent.gateway.v1.FsRenameResponse
+ 99, // 75: liveagent.gateway.v1.AgentEnvelope.fs_delete_resp:type_name -> liveagent.gateway.v1.FsDeleteResponse
27, // 76: liveagent.gateway.v1.AgentEnvelope.git_response:type_name -> liveagent.gateway.v1.GitResponse
- 87, // 77: liveagent.gateway.v1.AgentEnvelope.fs_read_editable_text_resp:type_name -> liveagent.gateway.v1.FsReadEditableTextResponse
- 89, // 78: liveagent.gateway.v1.AgentEnvelope.fs_read_workspace_image_resp:type_name -> liveagent.gateway.v1.FsReadWorkspaceImageResponse
+ 89, // 77: liveagent.gateway.v1.AgentEnvelope.fs_read_editable_text_resp:type_name -> liveagent.gateway.v1.FsReadEditableTextResponse
+ 91, // 78: liveagent.gateway.v1.AgentEnvelope.fs_read_workspace_image_resp:type_name -> liveagent.gateway.v1.FsReadWorkspaceImageResponse
14, // 79: liveagent.gateway.v1.AgentEnvelope.tunnel_control:type_name -> liveagent.gateway.v1.TunnelControlRequest
15, // 80: liveagent.gateway.v1.AgentEnvelope.tunnel_control_resp:type_name -> liveagent.gateway.v1.TunnelControlResponse
18, // 81: liveagent.gateway.v1.AgentEnvelope.tunnel_frame:type_name -> liveagent.gateway.v1.TunnelFrame
- 100, // 82: liveagent.gateway.v1.AgentEnvelope.error:type_name -> liveagent.gateway.v1.ErrorResponse
- 9, // 83: liveagent.gateway.v1.UploadReadableFilesRequest.files:type_name -> liveagent.gateway.v1.UploadReadableFile
- 8, // 84: liveagent.gateway.v1.UploadReadableFilesResponse.files:type_name -> liveagent.gateway.v1.ChatUploadedFile
- 16, // 85: liveagent.gateway.v1.TunnelControlResponse.tunnels:type_name -> liveagent.gateway.v1.TunnelSummary
- 16, // 86: liveagent.gateway.v1.TunnelControlResponse.tunnel:type_name -> liveagent.gateway.v1.TunnelSummary
- 0, // 87: liveagent.gateway.v1.TunnelFrame.kind:type_name -> liveagent.gateway.v1.TunnelFrameKind
- 17, // 88: liveagent.gateway.v1.TunnelFrame.headers:type_name -> liveagent.gateway.v1.TunnelHeader
- 22, // 89: liveagent.gateway.v1.TerminalResponse.sessions:type_name -> liveagent.gateway.v1.TerminalSession
- 22, // 90: liveagent.gateway.v1.TerminalResponse.session:type_name -> liveagent.gateway.v1.TerminalSession
- 23, // 91: liveagent.gateway.v1.TerminalResponse.shell_options:type_name -> liveagent.gateway.v1.TerminalShellOption
- 22, // 92: liveagent.gateway.v1.TerminalEvent.session:type_name -> liveagent.gateway.v1.TerminalSession
- 6, // 93: liveagent.gateway.v1.ChatRequest.selected_model:type_name -> liveagent.gateway.v1.ChatSelectedModel
- 8, // 94: liveagent.gateway.v1.ChatRequest.uploaded_files:type_name -> liveagent.gateway.v1.ChatUploadedFile
- 7, // 95: liveagent.gateway.v1.ChatRequest.runtime_controls:type_name -> liveagent.gateway.v1.ChatRuntimeControls
- 1, // 96: liveagent.gateway.v1.ChatEvent.type:type_name -> liveagent.gateway.v1.ChatEvent.ChatEventType
- 35, // 97: liveagent.gateway.v1.HistoryListResponse.conversations:type_name -> liveagent.gateway.v1.ConversationSummary
- 35, // 98: liveagent.gateway.v1.HistoryGetResponse.conversation:type_name -> liveagent.gateway.v1.ConversationSummary
- 35, // 99: liveagent.gateway.v1.HistoryRenameResponse.conversation:type_name -> liveagent.gateway.v1.ConversationSummary
- 35, // 100: liveagent.gateway.v1.HistoryPinResponse.conversation:type_name -> liveagent.gateway.v1.ConversationSummary
- 42, // 101: liveagent.gateway.v1.HistoryShareGetResponse.share:type_name -> liveagent.gateway.v1.HistoryShareStatus
- 42, // 102: liveagent.gateway.v1.HistoryShareSetResponse.share:type_name -> liveagent.gateway.v1.HistoryShareStatus
- 35, // 103: liveagent.gateway.v1.HistoryShareResolveResponse.conversation:type_name -> liveagent.gateway.v1.ConversationSummary
- 50, // 104: liveagent.gateway.v1.HistoryWorkdirsResponse.workdirs:type_name -> liveagent.gateway.v1.HistoryWorkdirSummary
- 35, // 105: liveagent.gateway.v1.HistoryTruncateResponse.conversation:type_name -> liveagent.gateway.v1.ConversationSummary
- 35, // 106: liveagent.gateway.v1.HistorySyncEvent.conversation:type_name -> liveagent.gateway.v1.ConversationSummary
- 73, // 107: liveagent.gateway.v1.FileMentionListResponse.entries:type_name -> liveagent.gateway.v1.FileMentionEntry
- 75, // 108: liveagent.gateway.v1.FsRootsResponse.roots:type_name -> liveagent.gateway.v1.FsRoot
- 79, // 109: liveagent.gateway.v1.FsListDirsResponse.entries:type_name -> liveagent.gateway.v1.FsDirEntry
- 84, // 110: liveagent.gateway.v1.FsListResponse.entries:type_name -> liveagent.gateway.v1.FsListEntry
- 5, // 111: liveagent.gateway.v1.AgentGateway.AgentConnect:input_type -> liveagent.gateway.v1.AgentEnvelope
- 2, // 112: liveagent.gateway.v1.AgentGateway.Authenticate:input_type -> liveagent.gateway.v1.AuthRequest
- 4, // 113: liveagent.gateway.v1.AgentGateway.AgentConnect:output_type -> liveagent.gateway.v1.GatewayEnvelope
- 3, // 114: liveagent.gateway.v1.AgentGateway.Authenticate:output_type -> liveagent.gateway.v1.AuthResponse
- 113, // [113:115] is the sub-list for method output_type
- 111, // [111:113] is the sub-list for method input_type
- 111, // [111:111] is the sub-list for extension type_name
- 111, // [111:111] is the sub-list for extension extendee
- 0, // [0:111] is the sub-list for field type_name
+ 31, // 82: liveagent.gateway.v1.AgentEnvelope.chat_control:type_name -> liveagent.gateway.v1.ChatControlEvent
+ 32, // 83: liveagent.gateway.v1.AgentEnvelope.runtime_status:type_name -> liveagent.gateway.v1.RuntimeStatusEvent
+ 102, // 84: liveagent.gateway.v1.AgentEnvelope.error:type_name -> liveagent.gateway.v1.ErrorResponse
+ 9, // 85: liveagent.gateway.v1.UploadReadableFilesRequest.files:type_name -> liveagent.gateway.v1.UploadReadableFile
+ 8, // 86: liveagent.gateway.v1.UploadReadableFilesResponse.files:type_name -> liveagent.gateway.v1.ChatUploadedFile
+ 16, // 87: liveagent.gateway.v1.TunnelControlResponse.tunnels:type_name -> liveagent.gateway.v1.TunnelSummary
+ 16, // 88: liveagent.gateway.v1.TunnelControlResponse.tunnel:type_name -> liveagent.gateway.v1.TunnelSummary
+ 0, // 89: liveagent.gateway.v1.TunnelFrame.kind:type_name -> liveagent.gateway.v1.TunnelFrameKind
+ 17, // 90: liveagent.gateway.v1.TunnelFrame.headers:type_name -> liveagent.gateway.v1.TunnelHeader
+ 22, // 91: liveagent.gateway.v1.TerminalResponse.sessions:type_name -> liveagent.gateway.v1.TerminalSession
+ 22, // 92: liveagent.gateway.v1.TerminalResponse.session:type_name -> liveagent.gateway.v1.TerminalSession
+ 23, // 93: liveagent.gateway.v1.TerminalResponse.shell_options:type_name -> liveagent.gateway.v1.TerminalShellOption
+ 22, // 94: liveagent.gateway.v1.TerminalEvent.session:type_name -> liveagent.gateway.v1.TerminalSession
+ 6, // 95: liveagent.gateway.v1.ChatRequest.selected_model:type_name -> liveagent.gateway.v1.ChatSelectedModel
+ 8, // 96: liveagent.gateway.v1.ChatRequest.uploaded_files:type_name -> liveagent.gateway.v1.ChatUploadedFile
+ 7, // 97: liveagent.gateway.v1.ChatRequest.runtime_controls:type_name -> liveagent.gateway.v1.ChatRuntimeControls
+ 1, // 98: liveagent.gateway.v1.ChatEvent.type:type_name -> liveagent.gateway.v1.ChatEvent.ChatEventType
+ 37, // 99: liveagent.gateway.v1.HistoryListResponse.conversations:type_name -> liveagent.gateway.v1.ConversationSummary
+ 37, // 100: liveagent.gateway.v1.HistoryGetResponse.conversation:type_name -> liveagent.gateway.v1.ConversationSummary
+ 37, // 101: liveagent.gateway.v1.HistoryRenameResponse.conversation:type_name -> liveagent.gateway.v1.ConversationSummary
+ 37, // 102: liveagent.gateway.v1.HistoryPinResponse.conversation:type_name -> liveagent.gateway.v1.ConversationSummary
+ 44, // 103: liveagent.gateway.v1.HistoryShareGetResponse.share:type_name -> liveagent.gateway.v1.HistoryShareStatus
+ 44, // 104: liveagent.gateway.v1.HistoryShareSetResponse.share:type_name -> liveagent.gateway.v1.HistoryShareStatus
+ 37, // 105: liveagent.gateway.v1.HistoryShareResolveResponse.conversation:type_name -> liveagent.gateway.v1.ConversationSummary
+ 52, // 106: liveagent.gateway.v1.HistoryWorkdirsResponse.workdirs:type_name -> liveagent.gateway.v1.HistoryWorkdirSummary
+ 37, // 107: liveagent.gateway.v1.HistoryTruncateResponse.conversation:type_name -> liveagent.gateway.v1.ConversationSummary
+ 37, // 108: liveagent.gateway.v1.HistorySyncEvent.conversation:type_name -> liveagent.gateway.v1.ConversationSummary
+ 75, // 109: liveagent.gateway.v1.FileMentionListResponse.entries:type_name -> liveagent.gateway.v1.FileMentionEntry
+ 77, // 110: liveagent.gateway.v1.FsRootsResponse.roots:type_name -> liveagent.gateway.v1.FsRoot
+ 81, // 111: liveagent.gateway.v1.FsListDirsResponse.entries:type_name -> liveagent.gateway.v1.FsDirEntry
+ 86, // 112: liveagent.gateway.v1.FsListResponse.entries:type_name -> liveagent.gateway.v1.FsListEntry
+ 5, // 113: liveagent.gateway.v1.AgentGateway.AgentConnect:input_type -> liveagent.gateway.v1.AgentEnvelope
+ 2, // 114: liveagent.gateway.v1.AgentGateway.Authenticate:input_type -> liveagent.gateway.v1.AuthRequest
+ 4, // 115: liveagent.gateway.v1.AgentGateway.AgentConnect:output_type -> liveagent.gateway.v1.GatewayEnvelope
+ 3, // 116: liveagent.gateway.v1.AgentGateway.Authenticate:output_type -> liveagent.gateway.v1.AuthResponse
+ 115, // [115:117] is the sub-list for method output_type
+ 113, // [113:115] is the sub-list for method input_type
+ 113, // [113:113] is the sub-list for extension type_name
+ 113, // [113:113] is the sub-list for extension extendee
+ 0, // [0:113] is the sub-list for field type_name
}
func init() { file_proto_v1_gateway_proto_init() }
@@ -8467,16 +8707,18 @@ func file_proto_v1_gateway_proto_init() {
(*AgentEnvelope_TunnelControl)(nil),
(*AgentEnvelope_TunnelControlResp)(nil),
(*AgentEnvelope_TunnelFrame)(nil),
+ (*AgentEnvelope_ChatControl)(nil),
+ (*AgentEnvelope_RuntimeStatus)(nil),
(*AgentEnvelope_Error)(nil),
}
- file_proto_v1_gateway_proto_msgTypes[43].OneofWrappers = []any{}
+ file_proto_v1_gateway_proto_msgTypes[45].OneofWrappers = []any{}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_v1_gateway_proto_rawDesc), len(file_proto_v1_gateway_proto_rawDesc)),
NumEnums: 2,
- NumMessages: 99,
+ NumMessages: 101,
NumExtensions: 0,
NumServices: 1,
},
diff --git a/crates/agent-gateway/internal/server/grpc.go b/crates/agent-gateway/internal/server/grpc.go
index 9234b0970..624ff2993 100644
--- a/crates/agent-gateway/internal/server/grpc.go
+++ b/crates/agent-gateway/internal/server/grpc.go
@@ -129,13 +129,15 @@ func (s *GRPCServer) AgentConnect(stream gatewayv1.AgentGateway_AgentConnectServ
}
func (s *GRPCServer) heartbeatLoop(ctx context.Context, sess *session.AgentSession) {
- ticker := time.NewTicker(s.cfg.HeartbeatPeriod)
+ period := s.heartbeatPeriod()
+ ticker := time.NewTicker(period)
defer ticker.Stop()
if !s.sendHeartbeat(sess) {
return
}
+ timeout := period * 3
for {
select {
case <-ctx.Done():
@@ -143,6 +145,9 @@ func (s *GRPCServer) heartbeatLoop(ctx context.Context, sess *session.AgentSessi
case <-sess.Done():
return
case <-ticker.C:
+ if s.sm.ClearSessionIfHeartbeatStale(sess, timeout) {
+ return
+ }
if !s.sendHeartbeat(sess) {
return
}
@@ -150,6 +155,13 @@ func (s *GRPCServer) heartbeatLoop(ctx context.Context, sess *session.AgentSessi
}
}
+func (s *GRPCServer) heartbeatPeriod() time.Duration {
+ if s.cfg == nil || s.cfg.HeartbeatPeriod <= 0 {
+ return 30 * time.Second
+ }
+ return s.cfg.HeartbeatPeriod
+}
+
func (s *GRPCServer) sendHeartbeat(sess *session.AgentSession) bool {
ok, err := sess.TrySendToAgent(&gatewayv1.GatewayEnvelope{
RequestId: "ping-" + uuid.NewString(),
diff --git a/crates/agent-gateway/internal/server/websocket.go b/crates/agent-gateway/internal/server/websocket.go
index 0a7e43d30..844e291ad 100644
--- a/crates/agent-gateway/internal/server/websocket.go
+++ b/crates/agent-gateway/internal/server/websocket.go
@@ -269,9 +269,19 @@ func (c *websocketConnection) startChatEventForwarder() {
if c.hasActiveChatRequest(event.RequestID) {
continue
}
- if err := c.writeConversationEvent(websocketChatEventPayload(event.Event, event.Seq, event.Workdir)); err != nil {
- c.close()
- return
+ if event.Control != nil {
+ if err := c.writeEnvelope(websocketEnvelope{
+ Type: "conversation.control",
+ Payload: websocketChatControlPayload(event.Control, event.Seq, event.Workdir),
+ }); err != nil {
+ c.close()
+ return
+ }
+ } else if event.Event != nil {
+ if err := c.writeConversationEvent(websocketChatEventPayload(event.Event, event.Seq, event.Workdir)); err != nil {
+ c.close()
+ return
+ }
}
}
}
@@ -450,6 +460,22 @@ func (c *websocketConnection) sendToAgent(envelope *gatewayv1.GatewayEnvelope) e
return c.sm.SendToAgentContext(ctx, envelope)
}
+func (c *websocketConnection) chatStartTimeout() time.Duration {
+ timeout := c.cfg.ChatStartTimeout
+ if timeout <= 0 {
+ timeout = 15 * time.Second
+ }
+ return timeout
+}
+
+func (c *websocketConnection) chatRenderStartTimeout() time.Duration {
+ timeout := c.cfg.ChatRenderStartTimeout
+ if timeout <= 0 {
+ timeout = 45 * time.Second
+ }
+ return timeout
+}
+
func (c *websocketConnection) writeResponse(requestID string, payload any) error {
return c.writeEnvelope(websocketEnvelope{
ID: requestID,
@@ -474,6 +500,14 @@ func (c *websocketConnection) writeChatEvent(requestID string, payload any) erro
})
}
+func (c *websocketConnection) writeChatControl(requestID string, payload any) error {
+ return c.writeEnvelope(websocketEnvelope{
+ ID: requestID,
+ Type: "chat.control",
+ Payload: payload,
+ })
+}
+
func (c *websocketConnection) writeHistoryEvent(payload any) error {
return c.writeEnvelope(websocketEnvelope{
Type: "history.event",
diff --git a/crates/agent-gateway/internal/server/websocket_chat_handlers.go b/crates/agent-gateway/internal/server/websocket_chat_handlers.go
index f647b32e6..63abbea85 100644
--- a/crates/agent-gateway/internal/server/websocket_chat_handlers.go
+++ b/crates/agent-gateway/internal/server/websocket_chat_handlers.go
@@ -8,6 +8,7 @@ import (
"github.com/liveagent/agent-gateway/internal/handler"
gatewayv1 "github.com/liveagent/agent-gateway/internal/proto/v1"
+ "github.com/liveagent/agent-gateway/internal/session"
)
func (c *websocketConnection) handleChatStart(req websocketRequest) {
@@ -34,12 +35,17 @@ func (c *websocketConnection) handleChatStart(req websocketRequest) {
_ = c.writeError(req.ID, "message is required")
return
}
- if !c.sm.IsOnline() {
+ status := c.sm.Status()
+ if !status.Online {
_ = c.writeError(req.ID, "agent offline")
return
}
+ if !status.ChatRuntimeReady {
+ _ = c.writeError(req.ID, "Desktop chat runtime is not ready. Please retry.")
+ return
+ }
- snapshot, created, err := c.sm.StartChatRunWithClientRequest(
+ snapshot, created, err := c.sm.StartPendingChatRunWithClientRequest(
req.ID,
body.ConversationID,
body.ClientRequestID,
@@ -99,6 +105,11 @@ func (c *websocketConnection) handleChatStart(req websocketRequest) {
}
}
+ startTimer := time.NewTimer(c.chatStartTimeout())
+ defer startTimer.Stop()
+ renderStartTimer := time.NewTimer(c.chatRenderStartTimeout())
+ defer renderStartTimer.Stop()
+
// Do not enforce a hard timeout for streaming chat requests. The GUI path can run
// multiple compaction rounds stably; WebUI should behave the same and only stop
// when the user cancels, the connection closes, or the agent returns done/error.
@@ -109,25 +120,32 @@ func (c *websocketConnection) handleChatStart(req websocketRequest) {
case <-ctx.Done():
_ = c.writeError(responseID, websocketErrorMessage(ctx.Err()))
return
+ case <-startTimer.C:
+ c.sm.FailStartingChatRun(
+ sourceRequestID,
+ "Desktop backend did not accept the remote chat request. Please retry.",
+ )
+ case <-renderStartTimer.C:
+ c.sm.FailUnstartedChatRun(
+ sourceRequestID,
+ "Desktop app accepted the remote chat request but did not start it. Please retry.",
+ )
case <-eventDone:
return
case event, ok := <-eventCh:
if !ok {
return
}
- chatEvent := event.Event
- if chatEvent == nil {
- continue
- }
- if chatEvent.GetConversationId() != "" {
- body.ConversationID = strings.TrimSpace(chatEvent.GetConversationId())
+ if eventConversationID(event) != "" {
+ body.ConversationID = eventConversationID(event)
c.updateActiveChatConversationID(responseID, body.ConversationID)
}
- if err := c.writeChatEvent(responseID, websocketChatEventPayload(chatEvent, event.Seq, event.Workdir)); err != nil {
+ terminal, err := c.writeChatBroadcastEvent(responseID, event)
+ if err != nil {
c.close()
return
}
- if chatEvent.GetType() == gatewayv1.ChatEvent_DONE || chatEvent.GetType() == gatewayv1.ChatEvent_ERROR {
+ if terminal {
return
}
}
@@ -178,6 +196,11 @@ func (c *websocketConnection) handleChatResume(req websocketRequest) {
c.registerActiveChat(responseID, snapshot.RequestID, snapshot.ConversationID, cancel)
defer c.releaseActiveChat(responseID)
+ startTimer := time.NewTimer(c.chatStartTimeout())
+ defer startTimer.Stop()
+ renderStartTimer := time.NewTimer(c.chatRenderStartTimeout())
+ defer renderStartTimer.Stop()
+
if snapshot.Done && snapshot.LatestSeq <= body.AfterSeq {
payload := map[string]any{
"type": "done",
@@ -199,24 +222,31 @@ func (c *websocketConnection) handleChatResume(req websocketRequest) {
case <-ctx.Done():
_ = c.writeError(responseID, websocketErrorMessage(ctx.Err()))
return
+ case <-startTimer.C:
+ c.sm.FailStartingChatRun(
+ snapshot.RequestID,
+ "Desktop backend did not accept the remote chat request. Please retry.",
+ )
+ case <-renderStartTimer.C:
+ c.sm.FailUnstartedChatRun(
+ snapshot.RequestID,
+ "Desktop app accepted the remote chat request but did not start it. Please retry.",
+ )
case <-eventDone:
return
case event, ok := <-eventCh:
if !ok {
return
}
- chatEvent := event.Event
- if chatEvent == nil {
- continue
- }
- if chatEvent.GetConversationId() != "" {
- c.updateActiveChatConversationID(responseID, strings.TrimSpace(chatEvent.GetConversationId()))
+ if conversationID := eventConversationID(event); conversationID != "" {
+ c.updateActiveChatConversationID(responseID, conversationID)
}
- if err := c.writeChatEvent(responseID, websocketChatEventPayload(chatEvent, event.Seq, event.Workdir)); err != nil {
+ terminal, err := c.writeChatBroadcastEvent(responseID, event)
+ if err != nil {
c.close()
return
}
- if chatEvent.GetType() == gatewayv1.ChatEvent_DONE || chatEvent.GetType() == gatewayv1.ChatEvent_ERROR {
+ if terminal {
return
}
}
@@ -279,15 +309,12 @@ func (c *websocketConnection) handleChatAttach(req websocketRequest) {
if !ok {
return
}
- chatEvent := event.Event
- if chatEvent == nil {
- continue
- }
- if err := c.writeChatEvent(req.ID, websocketChatEventPayload(chatEvent, event.Seq, event.Workdir)); err != nil {
+ terminal, err := c.writeChatBroadcastEvent(req.ID, event)
+ if err != nil {
c.close()
return
}
- if chatEvent.GetType() == gatewayv1.ChatEvent_DONE || chatEvent.GetType() == gatewayv1.ChatEvent_ERROR {
+ if terminal {
return
}
}
@@ -393,6 +420,87 @@ func (c *websocketConnection) cancelActiveChatsByConversation(conversationID str
}
}
+func (c *websocketConnection) writeChatBroadcastEvent(
+ requestID string,
+ event *session.ChatBroadcastEvent,
+) (bool, error) {
+ if event == nil {
+ return false, nil
+ }
+ if event.Control != nil {
+ payload := websocketChatControlPayload(event.Control, event.Seq, event.Workdir)
+ if err := c.writeChatControl(requestID, payload); err != nil {
+ return false, err
+ }
+ return isTerminalChatControlPayload(event.Control), nil
+ }
+ if event.Event == nil {
+ return false, nil
+ }
+ if err := c.writeChatEvent(
+ requestID,
+ websocketChatEventPayload(event.Event, event.Seq, event.Workdir),
+ ); err != nil {
+ return false, err
+ }
+ return event.Event.GetType() == gatewayv1.ChatEvent_DONE ||
+ event.Event.GetType() == gatewayv1.ChatEvent_ERROR, nil
+}
+
+func eventConversationID(event *session.ChatBroadcastEvent) string {
+ if event == nil {
+ return ""
+ }
+ if event.Control != nil {
+ return strings.TrimSpace(event.Control.GetConversationId())
+ }
+ if event.Event != nil {
+ return strings.TrimSpace(event.Event.GetConversationId())
+ }
+ return ""
+}
+
+func isTerminalChatControlPayload(control *gatewayv1.ChatControlEvent) bool {
+ switch strings.TrimSpace(control.GetState()) {
+ case "completed", "failed", "cancelled":
+ return true
+ default:
+ return false
+ }
+}
+
+func websocketChatControlPayload(
+ control *gatewayv1.ChatControlEvent,
+ seq int64,
+ workdirInput ...string,
+) map[string]any {
+ payload := map[string]any{
+ "type": strings.TrimSpace(control.GetType()),
+ "request_id": strings.TrimSpace(control.GetRequestId()),
+ "client_request_id": strings.TrimSpace(control.GetClientRequestId()),
+ "conversation_id": strings.TrimSpace(control.GetConversationId()),
+ "run_epoch": control.GetRunEpoch(),
+ "state": strings.TrimSpace(control.GetState()),
+ }
+ if seq > 0 {
+ payload["seq"] = seq
+ } else if control.GetSeq() > 0 {
+ payload["seq"] = control.GetSeq()
+ }
+ if errorCode := strings.TrimSpace(control.GetErrorCode()); errorCode != "" {
+ payload["error_code"] = errorCode
+ }
+ if message := strings.TrimSpace(control.GetMessage()); message != "" {
+ payload["message"] = message
+ }
+ if len(workdirInput) > 0 {
+ if workdir := strings.TrimSpace(workdirInput[0]); workdir != "" {
+ payload["workdir"] = workdir
+ }
+ }
+ return payload
+}
+
func websocketChatEventPayload(event *gatewayv1.ChatEvent, seq int64, workdirInput ...string) map[string]any {
payload := map[string]any{
"type": websocketChatEventType(event.GetType()),
diff --git a/crates/agent-gateway/internal/session/manager.go b/crates/agent-gateway/internal/session/manager.go
index b40db5b98..5a7479a9c 100644
--- a/crates/agent-gateway/internal/session/manager.go
+++ b/crates/agent-gateway/internal/session/manager.go
@@ -18,9 +18,14 @@ var ErrTunnelLimitExceeded = errors.New("tunnel limit exceeded")
const (
maxBufferedChatRunEvents = 50000
chatRunDoneRetention = time.Hour
+ chatRunStartRetention = 5 * time.Minute
chatRunStaleRetention = 12 * time.Hour
agentDisconnectedChatRunMessage = "Desktop agent disconnected. Please retry."
+
+ chatRuntimeReadyTTL = 15 * time.Second
+ agentSessionHeartbeatTTL = 90 * time.Second
+ defaultRuntimeReadyState = "ready"
)
type AuthSnapshot struct {
@@ -62,6 +67,7 @@ type agentStream struct {
type ChatBroadcastEvent struct {
RequestID string
Event *gatewayv1.ChatEvent
+ Control *gatewayv1.ChatControlEvent
Seq int64
Workdir string
}
@@ -73,6 +79,9 @@ type ChatRunSnapshot struct {
Workdir string
FirstSeq int64
LatestSeq int64
+ RunEpoch int64
+ State string
+ ErrorCode string
Done bool
}
@@ -82,14 +91,30 @@ type ActiveChatRunSummary struct {
UpdatedAt int64
}
+const (
+ ChatRunStateQueued = "queued"
+ ChatRunStateDelivered = "delivered"
+ ChatRunStateClaimed = "claimed"
+ ChatRunStateStarting = "starting"
+ ChatRunStateRunning = "running"
+ ChatRunStateCompleted = "completed"
+ ChatRunStateFailed = "failed"
+ ChatRunStateCancelled = "cancelled"
+)
+
type chatRun struct {
requestID string
conversationID string
clientRequestID string
workdir string
sessionEpoch uint64
+ runEpoch int64
events []*ChatBroadcastEvent
nextSeq int64
+ state string
+ errorCode string
+ accepted bool
+ started bool
done bool
updatedAt time.Time
expiresAt time.Time
@@ -109,12 +134,19 @@ type chatRunSubscriber struct {
}
type Status struct {
- Online bool `json:"online"`
- AgentID string `json:"agent_id"`
- AgentVersion string `json:"agent_version"`
- SessionID string `json:"session_id,omitempty"`
- ConnectedSince int64 `json:"connected_since"`
- LastHeartbeat int64 `json:"last_heartbeat"`
+ Online bool `json:"online"`
+ AgentReady bool `json:"agent_ready"`
+ ChatRuntimeReady bool `json:"chat_runtime_ready"`
+ AgentID string `json:"agent_id"`
+ AgentVersion string `json:"agent_version"`
+ SessionID string `json:"session_id,omitempty"`
+ ConnectedSince int64 `json:"connected_since"`
+ LastHeartbeat int64 `json:"last_heartbeat"`
+ RuntimeState string `json:"runtime_state,omitempty"`
+ RuntimeLastHeartbeat int64 `json:"runtime_last_heartbeat,omitempty"`
+ RuntimeWorkerID string `json:"runtime_worker_id,omitempty"`
+ RuntimeVisible bool `json:"runtime_visible,omitempty"`
+ RuntimeActiveRunCount uint32 `json:"runtime_active_run_count,omitempty"`
}
func NewManager() *Manager {
diff --git a/crates/agent-gateway/internal/session/manager_chat_runs.go b/crates/agent-gateway/internal/session/manager_chat_runs.go
index 53cf22653..1cf07a609 100644
--- a/crates/agent-gateway/internal/session/manager_chat_runs.go
+++ b/crates/agent-gateway/internal/session/manager_chat_runs.go
@@ -42,6 +42,25 @@ func (m *Manager) StartChatRunWithClientRequest(
conversationID string,
clientRequestID string,
workdirInput ...string,
+) (ChatRunSnapshot, bool, error) {
+ return m.startChatRunWithClientRequest(requestID, conversationID, clientRequestID, true, workdirInput...)
+}
+
+func (m *Manager) StartPendingChatRunWithClientRequest(
+ requestID string,
+ conversationID string,
+ clientRequestID string,
+ workdirInput ...string,
+) (ChatRunSnapshot, bool, error) {
+ return m.startChatRunWithClientRequest(requestID, conversationID, clientRequestID, false, workdirInput...)
+}
+
+func (m *Manager) startChatRunWithClientRequest(
+ requestID string,
+ conversationID string,
+ clientRequestID string,
+ started bool,
+ workdirInput ...string,
) (ChatRunSnapshot, bool, error) {
requestID = strings.TrimSpace(requestID)
if requestID == "" {
@@ -68,6 +87,9 @@ func (m *Manager) StartChatRunWithClientRequest(
if workdir != "" && existing.workdir == "" {
existing.workdir = workdir
}
+ if started && existing.state != ChatRunStateRunning {
+ existing.applyState(ChatRunStateRunning)
+ }
return existing.snapshot(), false, nil
}
m.releaseCompletedChatRunLocked(existingRequestID, existing)
@@ -80,15 +102,23 @@ func (m *Manager) StartChatRunWithClientRequest(
m.removeChatRunLocked(requestID, existing)
}
+ m.chatStore.nextChatRunEpoch += 1
+ initialState := ChatRunStateQueued
+ if started {
+ initialState = ChatRunStateRunning
+ }
run := &chatRun{
requestID: requestID,
conversationID: conversationID,
clientRequestID: clientRequestID,
workdir: workdir,
sessionEpoch: sessionEpoch,
+ runEpoch: m.chatStore.nextChatRunEpoch,
+ state: initialState,
updatedAt: now,
subscribers: make(map[int]*chatRunSubscriber),
}
+ run.applyState(initialState)
m.chatStore.chatRuns[requestID] = run
if conversationID != "" {
m.chatStore.chatRunByConversation[conversationID] = requestID
@@ -149,7 +179,7 @@ func (m *Manager) ActiveChatRunSummaries() []ActiveChatRunSummary {
seen := make(map[string]int, len(m.chatStore.chatRuns)+len(m.chatStore.historyActiveRuns))
summaries := make([]ActiveChatRunSummary, 0, len(m.chatStore.chatRuns)+len(m.chatStore.historyActiveRuns))
for _, run := range m.chatStore.chatRuns {
- if run == nil || run.done {
+ if run == nil || run.done || normalizeChatRunState(run.state) != ChatRunStateRunning {
continue
}
conversationID := strings.TrimSpace(run.conversationID)
@@ -232,7 +262,7 @@ func (m *Manager) failOpenChatRunsForSessionEpoch(sessionEpoch uint64, message s
now := time.Now()
type broadcastTarget struct {
- event *ChatBroadcastEvent
+ events []*ChatBroadcastEvent
subscribers []*chatRunSubscriber
}
targets := make([]broadcastTarget, 0)
@@ -247,7 +277,8 @@ func (m *Manager) failOpenChatRunsForSessionEpoch(sessionEpoch uint64, message s
run.nextSeq += 1
run.updatedAt = now
- run.done = true
+ run.applyState(ChatRunStateFailed)
+ run.errorCode = "agent_disconnected"
run.expiresAt = now.Add(chatRunDoneRetention)
chatEvent := &gatewayv1.ChatEvent{
@@ -272,7 +303,7 @@ func (m *Manager) failOpenChatRunsForSessionEpoch(sessionEpoch uint64, message s
subscribers = append(subscribers, subscriber)
}
targets = append(targets, broadcastTarget{
- event: broadcast,
+ events: []*ChatBroadcastEvent{broadcast},
subscribers: subscribers,
})
}
@@ -283,20 +314,145 @@ func (m *Manager) failOpenChatRunsForSessionEpoch(sessionEpoch uint64, message s
for _, target := range targets {
for _, subscriber := range target.subscribers {
- select {
- case <-subscriber.done:
- case subscriber.ch <- cloneChatBroadcastEvent(target.event):
+ for _, event := range target.events {
+ select {
+ case <-subscriber.done:
+ case subscriber.ch <- cloneChatBroadcastEvent(event):
+ }
}
}
for _, ch := range globalSubscribers {
- select {
- case ch <- cloneChatBroadcastEvent(target.event):
- default:
+ for _, event := range target.events {
+ select {
+ case ch <- cloneChatBroadcastEvent(event):
+ default:
+ }
}
}
}
}
+func (m *Manager) FailStartingChatRun(requestID string, message string) bool {
+ failed, sessionEpoch := m.failChatRunIf(
+ requestID,
+ message,
+ "Desktop backend did not accept the remote chat request. Please retry.",
+ func(run *chatRun) bool {
+ if run == nil || run.done {
+ return false
+ }
+ state := normalizeChatRunState(run.state)
+ return state == ChatRunStateQueued
+ },
+ )
+ if failed {
+ m.ClearSessionForEpoch(sessionEpoch)
+ }
+ return failed
+}
+
+func (m *Manager) FailUnstartedChatRun(requestID string, message string) bool {
+ failed, _ := m.failChatRunIf(
+ requestID,
+ message,
+ "Desktop app accepted the remote chat request but did not start it. Please retry.",
+ func(run *chatRun) bool {
+ if run == nil || run.done {
+ return false
+ }
+ state := normalizeChatRunState(run.state)
+ return state != ChatRunStateQueued &&
+ state != ChatRunStateRunning &&
+ !isTerminalChatRunState(state)
+ },
+ )
+ return failed
+}
+
+func (m *Manager) failChatRunIf(
+ requestID string,
+ message string,
+ defaultMessage string,
+ shouldFail func(*chatRun) bool,
+) (bool, uint64) {
+ requestID = strings.TrimSpace(requestID)
+ message = strings.TrimSpace(message)
+ if requestID == "" {
+ return false, 0
+ }
+ if message == "" {
+ message = defaultMessage
+ }
+
+ data, err := json.Marshal(map[string]string{"message": message})
+ if err != nil {
+ fallback, marshalErr := json.Marshal(map[string]string{"message": defaultMessage})
+ if marshalErr != nil {
+ fallback = []byte(`{"message":"Remote chat request failed. Please retry."}`)
+ }
+ data = fallback
+ }
+
+ now := time.Now()
+ var broadcast *ChatBroadcastEvent
+ var runSubscribers []*chatRunSubscriber
+ var subscribers []chan *ChatBroadcastEvent
+
+ m.chatStore.chatMu.Lock()
+ m.pruneExpiredChatRunsLocked(now)
+ run := m.chatStore.chatRuns[requestID]
+ if shouldFail == nil || !shouldFail(run) {
+ m.chatStore.chatMu.Unlock()
+ return false, 0
+ }
+ sessionEpoch := run.sessionEpoch
+
+ run.nextSeq += 1
+ run.updatedAt = now
+ run.applyState(ChatRunStateFailed)
+ run.errorCode = "desktop_runtime_unavailable"
+ run.expiresAt = now.Add(chatRunDoneRetention)
+ chatEvent := &gatewayv1.ChatEvent{
+ Type: gatewayv1.ChatEvent_ERROR,
+ ConversationId: strings.TrimSpace(run.conversationID),
+ Data: string(data),
+ }
+ broadcast = &ChatBroadcastEvent{
+ RequestID: requestID,
+ Event: chatEvent,
+ Seq: run.nextSeq,
+ Workdir: strings.TrimSpace(run.workdir),
+ }
+ run.events = append(run.events, cloneChatBroadcastEvent(broadcast))
+ if len(run.events) > maxBufferedChatRunEvents {
+ copy(run.events, run.events[len(run.events)-maxBufferedChatRunEvents:])
+ run.events = run.events[:maxBufferedChatRunEvents]
+ }
+ runSubscribers = make([]*chatRunSubscriber, 0, len(run.subscribers))
+ for _, subscriber := range run.subscribers {
+ runSubscribers = append(runSubscribers, subscriber)
+ }
+ subscribers = make([]chan *ChatBroadcastEvent, 0, len(m.chatStore.chatSubscribers))
+ for _, ch := range m.chatStore.chatSubscribers {
+ subscribers = append(subscribers, ch)
+ }
+ m.chatStore.chatMu.Unlock()
+
+ for _, subscriber := range runSubscribers {
+ select {
+ case <-subscriber.done:
+ case subscriber.ch <- cloneChatBroadcastEvent(broadcast):
+ }
+ }
+ for _, ch := range subscribers {
+ select {
+ case ch <- cloneChatBroadcastEvent(broadcast):
+ default:
+ }
+ }
+ return true, sessionEpoch
+}
+
func (m *Manager) SubscribeChatRun(
requestID string,
conversationID string,
@@ -381,6 +537,14 @@ func (m *Manager) broadcastChatEvent(requestID string, event *gatewayv1.ChatEven
conversationID := strings.TrimSpace(event.GetConversationId())
now := time.Now()
sessionEpoch := m.currentSessionEpoch()
+ if isChatAcceptedControlEvent(event) {
+ m.markChatRunStateSilent(requestID, conversationID, ChatRunStateDelivered, now)
+ return
+ }
+ if isChatStartedControlEvent(event) {
+ m.markChatRunStateSilent(requestID, conversationID, ChatRunStateRunning, now)
+ return
+ }
m.chatStore.chatMu.Lock()
m.pruneExpiredChatRunsLocked(now)
@@ -391,21 +555,27 @@ func (m *Manager) broadcastChatEvent(requestID string, event *gatewayv1.ChatEven
var runSubscribers []*chatRunSubscriber
run := m.chatStore.chatRuns[requestID]
if run == nil && requestID != "" {
+ m.chatStore.nextChatRunEpoch += 1
run = &chatRun{
requestID: requestID,
conversationID: conversationID,
sessionEpoch: sessionEpoch,
+ runEpoch: m.chatStore.nextChatRunEpoch,
+ state: ChatRunStateQueued,
updatedAt: now,
subscribers: make(map[int]*chatRunSubscriber),
}
+ run.applyState(ChatRunStateQueued)
m.chatStore.chatRuns[requestID] = run
if conversationID != "" {
m.chatStore.chatRunByConversation[conversationID] = requestID
}
}
if run != nil {
- run.nextSeq += 1
- run.updatedAt = now
+ if run.done {
+ m.chatStore.chatMu.Unlock()
+ return
+ }
if conversationID != "" {
if run.conversationID != "" && run.conversationID != conversationID {
if m.chatStore.chatRunByConversation[run.conversationID] == requestID {
@@ -420,6 +590,11 @@ func (m *Manager) broadcastChatEvent(requestID string, event *gatewayv1.ChatEven
}
}
}
+ if !run.done && normalizeChatRunState(run.state) != ChatRunStateRunning && !isTerminalChatEvent(event) {
+ run.applyState(ChatRunStateRunning)
+ }
+ run.nextSeq += 1
+ run.updatedAt = now
broadcast.Seq = run.nextSeq
broadcast.Workdir = strings.TrimSpace(run.workdir)
run.events = append(run.events, cloneChatBroadcastEvent(broadcast))
@@ -428,7 +603,14 @@ func (m *Manager) broadcastChatEvent(requestID string, event *gatewayv1.ChatEven
run.events = run.events[:maxBufferedChatRunEvents]
}
if isTerminalChatEvent(event) {
- run.done = true
+ if event.GetType() == gatewayv1.ChatEvent_DONE {
+ run.applyState(ChatRunStateCompleted)
+ } else {
+ run.applyState(ChatRunStateFailed)
+ if run.errorCode == "" {
+ run.errorCode = "desktop_error"
+ }
+ }
run.expiresAt = now.Add(chatRunDoneRetention)
}
runSubscribers = make([]*chatRunSubscriber, 0, len(run.subscribers))
@@ -456,6 +638,139 @@ func (m *Manager) broadcastChatEvent(requestID string, event *gatewayv1.ChatEven
}
}
+func (m *Manager) broadcastChatControl(requestID string, control *gatewayv1.ChatControlEvent) {
+ if control == nil {
+ return
+ }
+ requestID = strings.TrimSpace(requestID)
+ if requestID == "" {
+ requestID = strings.TrimSpace(control.GetRequestId())
+ }
+ conversationID := strings.TrimSpace(control.GetConversationId())
+ controlType := strings.TrimSpace(control.GetType())
+ state := normalizeChatRunState(control.GetState())
+ if state == "" {
+ state = chatRunStateForControlType(controlType)
+ }
+ errorCode := strings.TrimSpace(control.GetErrorCode())
+ message := strings.TrimSpace(control.GetMessage())
+ m.markChatRunControl(requestID, conversationID, controlType, state, errorCode, message, time.Now())
+}
+
+func (m *Manager) markChatRunStateSilent(
+ requestID string,
+ conversationID string,
+ state string,
+ now time.Time,
+) {
+ requestID = strings.TrimSpace(requestID)
+ conversationID = strings.TrimSpace(conversationID)
+ state = normalizeChatRunState(state)
+ if requestID == "" || state == "" {
+ return
+ }
+ m.chatStore.chatMu.Lock()
+ defer m.chatStore.chatMu.Unlock()
+ m.pruneExpiredChatRunsLocked(now)
+ run := m.chatStore.chatRuns[requestID]
+ if run == nil || run.done {
+ return
+ }
+ if conversationID != "" {
+ if run.conversationID != "" && run.conversationID != conversationID {
+ if m.chatStore.chatRunByConversation[run.conversationID] == requestID {
+ delete(m.chatStore.chatRunByConversation, run.conversationID)
+ }
+ }
+ run.conversationID = conversationID
+ m.chatStore.chatRunByConversation[conversationID] = requestID
+ }
+ run.applyState(state)
+ run.updatedAt = now
+ if isTerminalChatRunState(state) {
+ run.expiresAt = now.Add(chatRunDoneRetention)
+ }
+}
+
+func (m *Manager) markChatRunControl(
+ requestID string,
+ conversationID string,
+ controlType string,
+ state string,
+ errorCode string,
+ message string,
+ now time.Time,
+) {
+ requestID = strings.TrimSpace(requestID)
+ conversationID = strings.TrimSpace(conversationID)
+ if requestID == "" {
+ return
+ }
+
+ state = normalizeChatRunState(state)
+ controlType = strings.TrimSpace(controlType)
+ if controlType == "" {
+ controlType = chatControlTypeForState(state)
+ }
+
+ m.chatStore.chatMu.Lock()
+ m.pruneExpiredChatRunsLocked(now)
+ run := m.chatStore.chatRuns[requestID]
+ if run == nil {
+ m.chatStore.nextChatRunEpoch += 1
+ run = &chatRun{
+ requestID: requestID,
+ conversationID: conversationID,
+ sessionEpoch: m.currentSessionEpoch(),
+ runEpoch: m.chatStore.nextChatRunEpoch,
+ state: ChatRunStateQueued,
+ updatedAt: now,
+ subscribers: make(map[int]*chatRunSubscriber),
+ }
+ run.applyState(ChatRunStateQueued)
+ m.chatStore.chatRuns[requestID] = run
+ }
+ if run.done {
+ m.chatStore.chatMu.Unlock()
+ return
+ }
+ if conversationID != "" {
+ if run.conversationID != "" && run.conversationID != conversationID {
+ if m.chatStore.chatRunByConversation[run.conversationID] == requestID {
+ delete(m.chatStore.chatRunByConversation, run.conversationID)
+ }
+ }
+ run.conversationID = conversationID
+ m.chatStore.chatRunByConversation[conversationID] = requestID
+ }
+ broadcast := m.appendChatControlLocked(run, controlType, errorCode, message, now)
+ runSubscribers := make([]*chatRunSubscriber, 0, len(run.subscribers))
+ for _, subscriber := range run.subscribers {
+ runSubscribers = append(runSubscribers, subscriber)
+ }
+ subscribers := make([]chan *ChatBroadcastEvent, 0, len(m.chatStore.chatSubscribers))
+ for _, ch := range m.chatStore.chatSubscribers {
+ subscribers = append(subscribers, ch)
+ }
+ m.chatStore.chatMu.Unlock()
+
+ if broadcast == nil {
+ return
+ }
+ for _, subscriber := range runSubscribers {
+ select {
+ case <-subscriber.done:
+ case subscriber.ch <- cloneChatBroadcastEvent(broadcast):
+ }
+ }
+ for _, ch := range subscribers {
+ select {
+ case ch <- cloneChatBroadcastEvent(broadcast):
+ default:
+ }
+ }
+}
+
func (m *Manager) DispatchFromAgent(env *gatewayv1.AgentEnvelope) {
m.dispatchFromAgent(nil, env)
}
@@ -472,10 +787,19 @@ func (m *Manager) dispatchFromAgent(expected *AgentSession, env *gatewayv1.Agent
return
}
+ if runtimeStatus := env.GetRuntimeStatus(); runtimeStatus != nil {
+ m.UpdateRuntimeStatus(session, runtimeStatus)
+ return
+ }
+
if chatEvent := env.GetChatEvent(); chatEvent != nil {
m.broadcastChatEvent(env.GetRequestId(), chatEvent)
}
+ if chatControl := env.GetChatControl(); chatControl != nil {
+ m.broadcastChatControl(env.GetRequestId(), chatControl)
+ }
+
if historySync := env.GetHistorySync(); historySync != nil {
m.broadcastHistorySync(historySync)
return
@@ -509,6 +833,7 @@ func (r *chatRun) snapshot() ChatRunSnapshot {
if len(r.events) > 0 {
firstSeq = r.events[0].Seq
}
+ state := normalizeChatRunState(r.state)
return ChatRunSnapshot{
RequestID: r.requestID,
ConversationID: r.conversationID,
@@ -516,10 +841,27 @@ func (r *chatRun) snapshot() ChatRunSnapshot {
Workdir: r.workdir,
FirstSeq: firstSeq,
LatestSeq: r.nextSeq,
+ RunEpoch: r.runEpoch,
+ State: state,
+ ErrorCode: strings.TrimSpace(r.errorCode),
Done: r.done,
}
}
+func (r *chatRun) applyState(state string) {
+ state = normalizeChatRunState(state)
+ if state == "" {
+ state = ChatRunStateQueued
+ }
+ r.state = state
+ r.accepted = state != ChatRunStateQueued
+ r.started = state == ChatRunStateRunning || state == ChatRunStateCompleted
+ r.done = isTerminalChatRunState(state)
+ if state != ChatRunStateFailed {
+ r.errorCode = ""
+ }
+}
+
func (s *chatRunSubscriber) close() {
s.closeOnce.Do(func() {
close(s.done)
@@ -538,6 +880,10 @@ func (m *Manager) pruneExpiredChatRunsLocked(now time.Time) {
}
continue
}
+ if normalizeChatRunState(run.state) != ChatRunStateRunning && !run.updatedAt.IsZero() && now.Sub(run.updatedAt) > chatRunStartRetention {
+ m.removeChatRunLocked(requestID, run)
+ continue
+ }
if !run.updatedAt.IsZero() && now.Sub(run.updatedAt) > chatRunStaleRetention {
m.removeChatRunLocked(requestID, run)
}
@@ -574,14 +920,182 @@ func cloneChatBroadcastEvent(event *ChatBroadcastEvent) *ChatBroadcastEvent {
return &ChatBroadcastEvent{
RequestID: event.RequestID,
Event: event.Event,
+ Control: event.Control,
Seq: event.Seq,
Workdir: event.Workdir,
}
}
+func normalizeChatRunState(state string) string {
+ switch strings.TrimSpace(state) {
+ case ChatRunStateQueued:
+ return ChatRunStateQueued
+ case ChatRunStateDelivered:
+ return ChatRunStateDelivered
+ case ChatRunStateClaimed:
+ return ChatRunStateClaimed
+ case ChatRunStateStarting:
+ return ChatRunStateStarting
+ case ChatRunStateRunning:
+ return ChatRunStateRunning
+ case ChatRunStateCompleted:
+ return ChatRunStateCompleted
+ case ChatRunStateFailed:
+ return ChatRunStateFailed
+ case ChatRunStateCancelled:
+ return ChatRunStateCancelled
+ default:
+ return ""
+ }
+}
+
+func isTerminalChatRunState(state string) bool {
+ switch normalizeChatRunState(state) {
+ case ChatRunStateCompleted, ChatRunStateFailed, ChatRunStateCancelled:
+ return true
+ default:
+ return false
+ }
+}
+
+func ChatRunStateIsActive(state string) bool {
+ switch normalizeChatRunState(state) {
+ case ChatRunStateQueued, ChatRunStateDelivered, ChatRunStateClaimed, ChatRunStateStarting, ChatRunStateRunning:
+ return true
+ default:
+ return false
+ }
+}
+
+func chatRunStateForControlType(controlType string) string {
+ switch strings.TrimSpace(controlType) {
+ case "accepted":
+ return ChatRunStateQueued
+ case "delivered":
+ return ChatRunStateDelivered
+ case "claimed":
+ return ChatRunStateClaimed
+ case "starting":
+ return ChatRunStateStarting
+ case "started":
+ return ChatRunStateRunning
+ case "completed":
+ return ChatRunStateCompleted
+ case "failed":
+ return ChatRunStateFailed
+ case "cancelled":
+ return ChatRunStateCancelled
+ default:
+ return ""
+ }
+}
+
+func chatControlTypeForState(state string) string {
+ switch normalizeChatRunState(state) {
+ case ChatRunStateQueued:
+ return "accepted"
+ case ChatRunStateDelivered:
+ return "delivered"
+ case ChatRunStateClaimed:
+ return "claimed"
+ case ChatRunStateStarting:
+ return "starting"
+ case ChatRunStateRunning:
+ return "started"
+ case ChatRunStateCompleted:
+ return "completed"
+ case ChatRunStateFailed:
+ return "failed"
+ case ChatRunStateCancelled:
+ return "cancelled"
+ default:
+ return "progress"
+ }
+}
+
+func (m *Manager) appendChatControlLocked(
+ run *chatRun,
+ controlType string,
+ errorCode string,
+ message string,
+ now time.Time,
+) *ChatBroadcastEvent {
+ if run == nil {
+ return nil
+ }
+ controlType = strings.TrimSpace(controlType)
+ state := chatRunStateForControlType(controlType)
+ if state == "" {
+ state = normalizeChatRunState(run.state)
+ }
+ if state == "" {
+ state = ChatRunStateQueued
+ }
+ run.applyState(state)
+ if errorCode = strings.TrimSpace(errorCode); errorCode != "" {
+ run.errorCode = errorCode
+ }
+ run.updatedAt = now
+ if isTerminalChatRunState(state) {
+ run.expiresAt = now.Add(chatRunDoneRetention)
+ }
+ run.nextSeq += 1
+ seq := run.nextSeq
+ if controlType == "" {
+ controlType = chatControlTypeForState(state)
+ }
+ control := &gatewayv1.ChatControlEvent{
+ RequestId: strings.TrimSpace(run.requestID),
+ ClientRequestId: strings.TrimSpace(run.clientRequestID),
+ ConversationId: strings.TrimSpace(run.conversationID),
+ RunEpoch: run.runEpoch,
+ Type: controlType,
+ State: normalizeChatRunState(run.state),
+ ErrorCode: strings.TrimSpace(run.errorCode),
+ Message: strings.TrimSpace(message),
+ Seq: seq,
+ }
+ broadcast := &ChatBroadcastEvent{
+ RequestID: strings.TrimSpace(run.requestID),
+ Control: control,
+ Seq: seq,
+ Workdir: strings.TrimSpace(run.workdir),
+ }
+ run.events = append(run.events, cloneChatBroadcastEvent(broadcast))
+ if len(run.events) > maxBufferedChatRunEvents {
+ copy(run.events, run.events[len(run.events)-maxBufferedChatRunEvents:])
+ run.events = run.events[:maxBufferedChatRunEvents]
+ }
+ return broadcast
+}
+
func isTerminalChatEvent(event *gatewayv1.ChatEvent) bool {
if event == nil {
return false
}
return event.GetType() == gatewayv1.ChatEvent_DONE || event.GetType() == gatewayv1.ChatEvent_ERROR
}
+
+func isChatStartedControlEvent(event *gatewayv1.ChatEvent) bool {
+ return chatControlEventType(event) == "started"
+}
+
+func isChatAcceptedControlEvent(event *gatewayv1.ChatEvent) bool {
+ return chatControlEventType(event) == "accepted"
+}
+
+func chatControlEventType(event *gatewayv1.ChatEvent) string {
+ if event == nil || event.GetType() != gatewayv1.ChatEvent_TOKEN {
+ return ""
+ }
+ raw := strings.TrimSpace(event.GetData())
+ if raw == "" {
+ return ""
+ }
+ var decoded map[string]any
+ if err := json.Unmarshal([]byte(raw), &decoded); err != nil {
+ return ""
+ }
+ value, _ := decoded["type"].(string)
+ return strings.TrimSpace(value)
+}
diff --git a/crates/agent-gateway/internal/session/manager_registry.go b/crates/agent-gateway/internal/session/manager_registry.go
index 925bc2838..c31f46961 100644
--- a/crates/agent-gateway/internal/session/manager_registry.go
+++ b/crates/agent-gateway/internal/session/manager_registry.go
@@ -2,6 +2,7 @@ package session
import (
"context"
+ "strings"
"time"
gatewayv1 "github.com/liveagent/agent-gateway/internal/proto/v1"
@@ -41,6 +42,7 @@ func (m *Manager) SetSession(s *AgentSession) {
}
if previous != s {
m.registry.sessionEpoch += 1
+ clearRuntimeStatusLocked(m.registry)
}
sessionChanged := previous != s
m.registry.session = s
@@ -63,6 +65,7 @@ func (m *Manager) ClearSession(session *AgentSession) {
}
clearedEpoch := m.registry.sessionEpoch
m.registry.session = nil
+ clearRuntimeStatusLocked(m.registry)
m.registry.mu.Unlock()
if session == nil {
@@ -74,10 +77,54 @@ func (m *Manager) ClearSession(session *AgentSession) {
m.failOpenChatRunsForSessionEpoch(clearedEpoch, agentDisconnectedChatRunMessage)
}
+func (m *Manager) ClearSessionIfHeartbeatStale(session *AgentSession, timeout time.Duration) bool {
+ if session == nil || timeout <= 0 {
+ return false
+ }
+
+ now := time.Now()
+ m.registry.mu.Lock()
+ if m.registry.session != session {
+ m.registry.mu.Unlock()
+ return false
+ }
+ if lastPing := m.registry.session.LastPing; !lastPing.IsZero() && now.Sub(lastPing) <= timeout {
+ m.registry.mu.Unlock()
+ return false
+ }
+ clearedEpoch := m.registry.sessionEpoch
+ m.registry.session = nil
+ clearRuntimeStatusLocked(m.registry)
+ m.registry.mu.Unlock()
+
+ session.Close()
+ m.clearTerminalSessionSnapshot()
+ m.failOpenChatRunsForSessionEpoch(clearedEpoch, agentDisconnectedChatRunMessage)
+ return true
+}
+
+func (m *Manager) ClearSessionForEpoch(sessionEpoch uint64) bool {
+ m.registry.mu.Lock()
+ session := m.registry.session
+ if session == nil || m.registry.sessionEpoch != sessionEpoch {
+ m.registry.mu.Unlock()
+ return false
+ }
+ m.registry.session = nil
+ clearRuntimeStatusLocked(m.registry)
+ m.registry.mu.Unlock()
+
+ session.Close()
+ m.clearTerminalSessionSnapshot()
+ m.failOpenChatRunsForSessionEpoch(sessionEpoch, agentDisconnectedChatRunMessage)
+ return true
+}
+
func (m *Manager) Status() Status {
m.registry.mu.RLock()
defer m.registry.mu.RUnlock()
+ now := time.Now()
status := Status{}
if m.registry.authValid {
status.AgentID = m.registry.lastAuth.AgentID
@@ -88,14 +135,52 @@ func (m *Manager) Status() Status {
return status
}
status.Online = true
+ status.AgentReady = true
status.AgentID = m.registry.session.AgentID
status.AgentVersion = m.registry.session.AgentVersion
status.SessionID = m.registry.session.SessionID
status.ConnectedSince = m.registry.session.ConnectedAt.Unix()
status.LastHeartbeat = m.registry.session.LastPing.Unix()
+ status.RuntimeState = m.registry.runtimeState
+ status.RuntimeWorkerID = m.registry.runtimeWorkerID
+ status.RuntimeVisible = m.registry.runtimeVisible
+ status.RuntimeActiveRunCount = m.registry.runtimeActiveRunCount
+ if !m.registry.runtimeLastHeartbeat.IsZero() {
+ status.RuntimeLastHeartbeat = m.registry.runtimeLastHeartbeat.Unix()
+ }
+ status.ChatRuntimeReady = runtimeReadyLocked(m.registry, now)
return status
}
+func (m *Manager) ChatRuntimeReady() bool {
+ m.registry.mu.RLock()
+ defer m.registry.mu.RUnlock()
+ return runtimeReadyLocked(m.registry, time.Now())
+}
+
+func (m *Manager) UpdateRuntimeStatus(
+ session *AgentSession,
+ event *gatewayv1.RuntimeStatusEvent,
+) {
+ if event == nil {
+ return
+ }
+ workerID := strings.TrimSpace(event.GetWorkerId())
+ state := normalizeRuntimeState(event.GetState())
+ now := time.Now()
+
+ m.registry.mu.Lock()
+ defer m.registry.mu.Unlock()
+ if m.registry.session == nil || (session != nil && m.registry.session != session) {
+ return
+ }
+ m.registry.runtimeState = state
+ m.registry.runtimeWorkerID = workerID
+ m.registry.runtimeLastHeartbeat = now
+ m.registry.runtimeVisible = event.GetVisible()
+ m.registry.runtimeActiveRunCount = event.GetActiveRunCount()
+}
+
func (m *Manager) TouchHeartbeat(session *AgentSession) {
m.registry.mu.Lock()
defer m.registry.mu.Unlock()
@@ -104,6 +189,42 @@ func (m *Manager) TouchHeartbeat(session *AgentSession) {
}
}
+func clearRuntimeStatusLocked(registry *sessionRegistry) {
+ registry.runtimeState = ""
+ registry.runtimeWorkerID = ""
+ registry.runtimeLastHeartbeat = time.Time{}
+ registry.runtimeVisible = false
+ registry.runtimeActiveRunCount = 0
+}
+
+func runtimeReadyLocked(registry *sessionRegistry, now time.Time) bool {
+ if registry == nil || registry.session == nil {
+ return false
+ }
+ if registry.session.LastPing.IsZero() || now.Sub(registry.session.LastPing) > agentSessionHeartbeatTTL {
+ return false
+ }
+ if registry.runtimeLastHeartbeat.IsZero() ||
+ now.Sub(registry.runtimeLastHeartbeat) > chatRuntimeReadyTTL {
+ return false
+ }
+ switch normalizeRuntimeState(registry.runtimeState) {
+ case "ready", "draining", "busy":
+ return true
+ default:
+ return false
+ }
+}
+
+func normalizeRuntimeState(state string) string {
+ switch strings.TrimSpace(state) {
+ case "ready", "draining", "busy", "suspended":
+ return strings.TrimSpace(state)
+ default:
+ return defaultRuntimeReadyState
+ }
+}
+
func (m *Manager) SendToAgent(env *gatewayv1.GatewayEnvelope) error {
m.registry.mu.RLock()
session := m.registry.session
@@ -112,7 +233,9 @@ func (m *Manager) SendToAgent(env *gatewayv1.GatewayEnvelope) error {
return ErrAgentOffline
}
- return session.SendToAgent(env)
+ err := session.SendToAgent(env)
+ m.clearSessionAfterSendError(session, err)
+ return err
}
func (m *Manager) SendToAgentContext(ctx context.Context, env *gatewayv1.GatewayEnvelope) error {
@@ -123,7 +246,16 @@ func (m *Manager) SendToAgentContext(ctx context.Context, env *gatewayv1.Gateway
return ErrAgentOffline
}
- return session.SendToAgentContext(ctx, env)
+ err := session.SendToAgentContext(ctx, env)
+ m.clearSessionAfterSendError(session, err)
+ return err
+}
+
+func (m *Manager) clearSessionAfterSendError(session *AgentSession, err error) {
+ if err == nil || session == nil {
+ return
+ }
+ m.ClearSession(session)
}
func (m *Manager) currentSessionEpoch() uint64 {
diff --git a/crates/agent-gateway/internal/session/manager_state.go b/crates/agent-gateway/internal/session/manager_state.go
index a32a4528b..55c780bbe 100644
--- a/crates/agent-gateway/internal/session/manager_state.go
+++ b/crates/agent-gateway/internal/session/manager_state.go
@@ -2,6 +2,7 @@ package session
import (
"sync"
+ "time"
gatewayv1 "github.com/liveagent/agent-gateway/internal/proto/v1"
)
@@ -12,6 +13,12 @@ type sessionRegistry struct {
sessionEpoch uint64
lastAuth AuthSnapshot
authValid bool
+
+ runtimeState string
+ runtimeWorkerID string
+ runtimeLastHeartbeat time.Time
+ runtimeVisible bool
+ runtimeActiveRunCount uint32
}
func newSessionRegistry() *sessionRegistry {
@@ -49,6 +56,7 @@ type chatRunStore struct {
nextChatSubID int
chatSubscribers map[int]chan *ChatBroadcastEvent
nextChatRunSubID int
+ nextChatRunEpoch int64
chatRuns map[string]*chatRun
chatRunByConversation map[string]string
chatRunByClientRequest map[string]string
diff --git a/crates/agent-gateway/proto/v1/gateway.proto b/crates/agent-gateway/proto/v1/gateway.proto
index e187c39be..d15952d75 100644
--- a/crates/agent-gateway/proto/v1/gateway.proto
+++ b/crates/agent-gateway/proto/v1/gateway.proto
@@ -116,6 +116,8 @@ message AgentEnvelope {
TunnelControlRequest tunnel_control = 67;
TunnelControlResponse tunnel_control_resp = 68;
TunnelFrame tunnel_frame = 69;
+ ChatControlEvent chat_control = 70;
+ RuntimeStatusEvent runtime_status = 71;
ErrorResponse error = 99;
}
}
@@ -345,6 +347,26 @@ message ChatEvent {
}
}
+message ChatControlEvent {
+ string request_id = 1;
+ string client_request_id = 2;
+ string conversation_id = 3;
+ int64 run_epoch = 4;
+ string type = 5;
+ string state = 6;
+ string error_code = 7;
+ string message = 8;
+ int64 seq = 9;
+}
+
+message RuntimeStatusEvent {
+ string worker_id = 1;
+ string state = 2;
+ bool visible = 3;
+ uint32 active_run_count = 4;
+ int64 timestamp = 5;
+}
+
message CronManageRequest {
string action = 1;
string task_id = 2;
diff --git a/crates/agent-gateway/test/session/manager_test.go b/crates/agent-gateway/test/session/manager_test.go
index 4def205b9..41d177d95 100644
--- a/crates/agent-gateway/test/session/manager_test.go
+++ b/crates/agent-gateway/test/session/manager_test.go
@@ -77,6 +77,122 @@ func TestClearSessionDoesNotCloseReplacement(t *testing.T) {
}
}
+func TestClearSessionIfHeartbeatStaleClosesOnlyCurrentSession(t *testing.T) {
+ t.Parallel()
+
+ sm := newTestSessionManager()
+ first := session.NewAgentSession(sm.LatestAuthSnapshot())
+ sm.SetSession(first)
+ second := session.NewAgentSession(sm.LatestAuthSnapshot())
+ sm.SetSession(second)
+
+ time.Sleep(time.Millisecond)
+ if sm.ClearSessionIfHeartbeatStale(first, time.Nanosecond) {
+ t.Fatalf("stale first session should not close replacement session")
+ }
+ assertDoneOpen(t, second.Done())
+ if status := sm.Status(); !status.Online {
+ t.Fatalf("status online = false after stale old-session heartbeat timeout")
+ }
+
+ time.Sleep(time.Millisecond)
+ if !sm.ClearSessionIfHeartbeatStale(second, time.Nanosecond) {
+ t.Fatalf("current stale session was not cleared")
+ }
+ assertDoneClosed(t, second.Done())
+ if status := sm.Status(); status.Online {
+ t.Fatalf("status online = true after current session heartbeat timeout")
+ }
+ if err := sm.SendToAgent(&gatewayv1.GatewayEnvelope{RequestId: "after-timeout"}); !errors.Is(err, session.ErrAgentOffline) {
+ t.Fatalf("SendToAgent after heartbeat timeout = %v, want ErrAgentOffline", err)
+ }
+}
+
+func TestChatRuntimeReadyRequiresFreshRuntimeHeartbeat(t *testing.T) {
+ t.Parallel()
+
+ sm := newTestSessionManager()
+ sess := session.NewAgentSession(sm.LatestAuthSnapshot())
+ sm.SetSession(sess)
+
+ if status := sm.Status(); !status.Online || status.ChatRuntimeReady {
+ t.Fatalf("initial status = %#v, want online without chat runtime readiness", status)
+ }
+
+ sm.UpdateRuntimeStatus(sess, &gatewayv1.RuntimeStatusEvent{
+ WorkerId: "runtime-1",
+ State: "ready",
+ Visible: true,
+ ActiveRunCount: 0,
+ Timestamp: time.Now().Unix(),
+ })
+ if status := sm.Status(); !status.ChatRuntimeReady ||
+ status.RuntimeState != "ready" ||
+ status.RuntimeWorkerID != "runtime-1" ||
+ status.RuntimeLastHeartbeat == 0 {
+ t.Fatalf("ready runtime status = %#v", status)
+ }
+
+ sm.UpdateRuntimeStatus(sess, &gatewayv1.RuntimeStatusEvent{
+ WorkerId: "runtime-1",
+ State: "suspended",
+ Timestamp: time.Now().Unix(),
+ })
+ if status := sm.Status(); status.ChatRuntimeReady || status.RuntimeState != "suspended" {
+ t.Fatalf("suspended runtime status = %#v, want not ready", status)
+ }
+
+ sm.UpdateRuntimeStatus(sess, &gatewayv1.RuntimeStatusEvent{
+ WorkerId: "runtime-1",
+ State: "busy",
+ Timestamp: time.Now().Unix(),
+ })
+ if !sm.ChatRuntimeReady() {
+ t.Fatalf("busy runtime should be ready to manage chat runs")
+ }
+
+ sm.ClearSession(sess)
+ if status := sm.Status(); status.ChatRuntimeReady || status.RuntimeState != "" {
+ t.Fatalf("cleared session status = %#v, want runtime readiness reset", status)
+ }
+}
+
+func TestClearSessionIfHeartbeatStaleFailsOpenChatRuns(t *testing.T) {
+ t.Parallel()
+
+ sm := newTestSessionManager()
+ sess := session.NewAgentSession(sm.LatestAuthSnapshot())
+ sm.SetSession(sess)
+ if _, _, err := sm.StartPendingChatRunWithClientRequest(
+ "request-1",
+ "conversation-1",
+ "client-submit-1",
+ ); err != nil {
+ t.Fatalf("StartPendingChatRunWithClientRequest: %v", err)
+ }
+ ch, _, cleanup, _, err := sm.SubscribeChatRun("request-1", "conversation-1", 0)
+ if err != nil {
+ t.Fatalf("SubscribeChatRun: %v", err)
+ }
+ defer cleanup()
+
+ time.Sleep(time.Millisecond)
+ if !sm.ClearSessionIfHeartbeatStale(sess, time.Nanosecond) {
+ t.Fatalf("current stale session was not cleared")
+ }
+ select {
+ case event := <-ch:
+ if event.Event.GetType() != gatewayv1.ChatEvent_ERROR {
+ t.Fatalf("event type = %v, want ERROR", event.Event.GetType())
+ }
+ if !strings.Contains(event.Event.GetData(), "Desktop agent disconnected") {
+ t.Fatalf("event data = %q", event.Event.GetData())
+ }
+ case <-time.After(time.Second):
+ t.Fatalf("timed out waiting for heartbeat timeout chat error")
+ }
+}
+
func TestDispatchFromStaleSessionIsIgnored(t *testing.T) {
t.Parallel()
@@ -179,6 +295,9 @@ func TestSendToAgentContextReturnsWhenOutboundQueueIsFull(t *testing.T) {
if !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("SendToAgentContext with full queue = %v, want context deadline exceeded", err)
}
+ if status := sm.Status(); status.Online {
+ t.Fatalf("status online = true after SendToAgentContext timeout")
+ }
}
func TestRemoveChatRunByConversationReleasesBufferedRun(t *testing.T) {
@@ -275,6 +394,251 @@ func TestStartChatRunWithClientRequestReusesExistingRun(t *testing.T) {
}
}
+func TestPendingChatRunBecomesActiveOnlyAfterStartedEvent(t *testing.T) {
+ t.Parallel()
+
+ sm := newTestSessionManager()
+ sm.SetSession(session.NewAgentSession(sm.LatestAuthSnapshot()))
+ snapshot, created, err := sm.StartPendingChatRunWithClientRequest(
+ "request-1",
+ "conversation-1",
+ "client-submit-1",
+ "/workspace",
+ )
+ if err != nil {
+ t.Fatalf("StartPendingChatRunWithClientRequest: %v", err)
+ }
+ if !created || snapshot.RequestID != "request-1" {
+ t.Fatalf("pending run = %#v created=%v", snapshot, created)
+ }
+ if got := sm.ActiveChatRunConversationIDs(); len(got) != 0 {
+ t.Fatalf("pending active chat runs = %#v, want empty", got)
+ }
+
+ ch, _, cleanup, _, err := sm.SubscribeChatRun("request-1", "conversation-1", 0)
+ if err != nil {
+ t.Fatalf("SubscribeChatRun: %v", err)
+ }
+ defer cleanup()
+
+ sm.DispatchFromAgent(&gatewayv1.AgentEnvelope{
+ RequestId: "request-1",
+ Payload: &gatewayv1.AgentEnvelope_ChatEvent{
+ ChatEvent: &gatewayv1.ChatEvent{
+ Type: gatewayv1.ChatEvent_TOKEN,
+ ConversationId: "conversation-1",
+ Data: `{"type":"accepted"}`,
+ },
+ },
+ })
+ if got := sm.ActiveChatRunConversationIDs(); len(got) != 0 {
+ t.Fatalf("accepted active chat runs = %#v, want empty", got)
+ }
+ if sm.FailStartingChatRun("request-1", "desktop did not accept") {
+ t.Fatalf("accepted pending run should not fail the accept watchdog")
+ }
+
+ sm.DispatchFromAgent(&gatewayv1.AgentEnvelope{
+ RequestId: "request-1",
+ Payload: &gatewayv1.AgentEnvelope_ChatEvent{
+ ChatEvent: &gatewayv1.ChatEvent{
+ Type: gatewayv1.ChatEvent_TOKEN,
+ ConversationId: "conversation-1",
+ Data: `{"type":"started"}`,
+ },
+ },
+ })
+
+ got := sm.ActiveChatRunConversationIDs()
+ want := []string{"conversation-1"}
+ if fmt.Sprint(got) != fmt.Sprint(want) {
+ t.Fatalf("active chat runs after started = %#v, want %#v", got, want)
+ }
+ select {
+ case event := <-ch:
+ t.Fatalf("started control event leaked to subscriber: %#v", event)
+ case <-time.After(50 * time.Millisecond):
+ }
+}
+
+func TestFailStartingChatRunBroadcastsErrorAndClearsActiveSummary(t *testing.T) {
+ t.Parallel()
+
+ sm := newTestSessionManager()
+ sm.SetSession(session.NewAgentSession(sm.LatestAuthSnapshot()))
+ if _, _, err := sm.StartPendingChatRunWithClientRequest(
+ "request-1",
+ "conversation-1",
+ "client-submit-1",
+ ); err != nil {
+ t.Fatalf("StartPendingChatRunWithClientRequest: %v", err)
+ }
+ ch, _, cleanup, _, err := sm.SubscribeChatRun("request-1", "conversation-1", 0)
+ if err != nil {
+ t.Fatalf("SubscribeChatRun: %v", err)
+ }
+ defer cleanup()
+
+ if !sm.FailStartingChatRun("request-1", "desktop did not accept") {
+ t.Fatalf("FailStartingChatRun returned false")
+ }
+
+ select {
+ case event := <-ch:
+ if event.Event.GetType() != gatewayv1.ChatEvent_ERROR {
+ t.Fatalf("event type = %v, want ERROR", event.Event.GetType())
+ }
+ if !strings.Contains(event.Event.GetData(), "desktop did not accept") {
+ t.Fatalf("event data = %q", event.Event.GetData())
+ }
+ case <-time.After(time.Second):
+ t.Fatalf("timed out waiting for starting run failure event")
+ }
+ if got := sm.ActiveChatRunConversationIDs(); len(got) != 0 {
+ t.Fatalf("active chat runs after failed start = %#v, want empty", got)
+ }
+ if status := sm.Status(); status.Online {
+ t.Fatalf("status online = true after chat run failed before desktop accept")
+ }
+}
+
+func TestFailUnstartedChatRunBroadcastsErrorUnlessStarted(t *testing.T) {
+ t.Parallel()
+
+ sm := newTestSessionManager()
+ sm.SetSession(session.NewAgentSession(sm.LatestAuthSnapshot()))
+ if _, _, err := sm.StartPendingChatRunWithClientRequest(
+ "request-1",
+ "conversation-1",
+ "client-submit-1",
+ ); err != nil {
+ t.Fatalf("StartPendingChatRunWithClientRequest request-1: %v", err)
+ }
+ ch, _, cleanup, _, err := sm.SubscribeChatRun("request-1", "conversation-1", 0)
+ if err != nil {
+ t.Fatalf("SubscribeChatRun: %v", err)
+ }
+ defer cleanup()
+ if sm.FailUnstartedChatRun("request-1", "desktop app did not start") {
+ t.Fatalf("unaccepted pending run should not fail the render-start watchdog")
+ }
+ sm.DispatchFromAgent(&gatewayv1.AgentEnvelope{
+ RequestId: "request-1",
+ Payload: &gatewayv1.AgentEnvelope_ChatEvent{
+ ChatEvent: &gatewayv1.ChatEvent{
+ Type: gatewayv1.ChatEvent_TOKEN,
+ ConversationId: "conversation-1",
+ Data: `{"type":"accepted"}`,
+ },
+ },
+ })
+
+ if !sm.FailUnstartedChatRun("request-1", "desktop app did not start") {
+ t.Fatalf("FailUnstartedChatRun returned false for accepted pending run")
+ }
+ select {
+ case event := <-ch:
+ if event.Event.GetType() != gatewayv1.ChatEvent_ERROR {
+ t.Fatalf("event type = %v, want ERROR", event.Event.GetType())
+ }
+ if !strings.Contains(event.Event.GetData(), "desktop app did not start") {
+ t.Fatalf("event data = %q", event.Event.GetData())
+ }
+ case <-time.After(time.Second):
+ t.Fatalf("timed out waiting for unstarted run failure event")
+ }
+
+ if _, _, err := sm.StartPendingChatRunWithClientRequest(
+ "request-2",
+ "conversation-2",
+ "client-submit-2",
+ ); err != nil {
+ t.Fatalf("StartPendingChatRunWithClientRequest request-2: %v", err)
+ }
+ sm.DispatchFromAgent(&gatewayv1.AgentEnvelope{
+ RequestId: "request-2",
+ Payload: &gatewayv1.AgentEnvelope_ChatEvent{
+ ChatEvent: &gatewayv1.ChatEvent{
+ Type: gatewayv1.ChatEvent_TOKEN,
+ ConversationId: "conversation-2",
+ Data: `{"type":"started"}`,
+ },
+ },
+ })
+ if sm.FailUnstartedChatRun("request-2", "desktop app did not start") {
+ t.Fatalf("started run should not fail the render-start watchdog")
+ }
+}
+
+func TestTerminalChatRunStateIsImmutable(t *testing.T) {
+ t.Parallel()
+
+ sm := newTestSessionManager()
+ sm.SetSession(session.NewAgentSession(sm.LatestAuthSnapshot()))
+ if _, _, err := sm.StartPendingChatRunWithClientRequest(
+ "request-1",
+ "conversation-1",
+ "client-submit-1",
+ ); err != nil {
+ t.Fatalf("StartPendingChatRunWithClientRequest: %v", err)
+ }
+ sm.DispatchFromAgent(&gatewayv1.AgentEnvelope{
+ RequestId: "request-1",
+ Payload: &gatewayv1.AgentEnvelope_ChatEvent{
+ ChatEvent: &gatewayv1.ChatEvent{
+ Type: gatewayv1.ChatEvent_ERROR,
+ ConversationId: "conversation-1",
+ Data: `{"message":"startup failed"}`,
+ },
+ },
+ })
+ sm.DispatchFromAgent(&gatewayv1.AgentEnvelope{
+ RequestId: "request-1",
+ Payload: &gatewayv1.AgentEnvelope_ChatControl{
+ ChatControl: &gatewayv1.ChatControlEvent{
+ Type: "completed",
+ State: session.ChatRunStateCompleted,
+ RequestId: "request-1",
+ ConversationId: "conversation-1",
+ },
+ },
+ })
+ sm.DispatchFromAgent(&gatewayv1.AgentEnvelope{
+ RequestId: "request-1",
+ Payload: &gatewayv1.AgentEnvelope_ChatEvent{
+ ChatEvent: &gatewayv1.ChatEvent{
+ Type: gatewayv1.ChatEvent_TOKEN,
+ ConversationId: "conversation-1",
+ Data: `{"text":"late token"}`,
+ },
+ },
+ })
+
+ ch, done, cleanup, snapshot, err := sm.SubscribeChatRun("request-1", "conversation-1", 0)
+ if err != nil {
+ t.Fatalf("SubscribeChatRun: %v", err)
+ }
+ defer cleanup()
+ assertDoneOpen(t, done)
+ if snapshot.State != session.ChatRunStateFailed {
+ t.Fatalf("terminal state = %q, want %q", snapshot.State, session.ChatRunStateFailed)
+ }
+
+ select {
+ case event := <-ch:
+ if event.Event == nil || event.Event.GetType() != gatewayv1.ChatEvent_ERROR {
+ t.Fatalf("replayed event = %#v, want ERROR", event)
+ }
+ case <-time.After(time.Second):
+ t.Fatalf("timed out waiting for replayed error event")
+ }
+ select {
+ case event := <-ch:
+ t.Fatalf("terminal completion control should be ignored after failure: %#v", event)
+ default:
+ }
+}
+
func TestDesktopBroadcastChatEventCreatesAttachableRun(t *testing.T) {
t.Parallel()
diff --git a/crates/agent-gateway/test/websocket/chat_bridge_test.go b/crates/agent-gateway/test/websocket/chat_bridge_test.go
index f20742cac..f644ad2c3 100644
--- a/crates/agent-gateway/test/websocket/chat_bridge_test.go
+++ b/crates/agent-gateway/test/websocket/chat_bridge_test.go
@@ -77,6 +77,17 @@ func receiveEnvelopeWithID(t *testing.T, conn *websocket.Conn, id string) wsEnve
return wsEnvelope{}
}
+func assertNoEnvelopeWithin(t *testing.T, conn *websocket.Conn, timeout time.Duration) {
+ t.Helper()
+ if err := conn.SetDeadline(time.Now().Add(timeout)); err != nil {
+ t.Fatalf("set websocket deadline: %v", err)
+ }
+ var env wsEnvelope
+ if err := websocket.JSON.Receive(conn, &env); err == nil {
+ t.Fatalf("unexpected websocket envelope: %#v", env)
+ }
+}
+
func authWebSocket(t *testing.T, conn *websocket.Conn, token string) {
t.Helper()
sendEnvelope(t, conn, "auth-1", "auth", map[string]any{"token": token})
@@ -98,6 +109,31 @@ func readOutboundEnvelope(t *testing.T, agentSession *session.AgentSession) *gat
}
}
+func dispatchChatStarted(t *testing.T, sm *session.Manager, requestID string, conversationID string) {
+ t.Helper()
+ sm.DispatchFromAgent(&gatewayv1.AgentEnvelope{
+ RequestId: requestID,
+ Timestamp: time.Now().Unix(),
+ Payload: &gatewayv1.AgentEnvelope_ChatEvent{
+ ChatEvent: &gatewayv1.ChatEvent{
+ Type: gatewayv1.ChatEvent_TOKEN,
+ ConversationId: conversationID,
+ Data: `{"type":"started"}`,
+ },
+ },
+ })
+}
+
+func markRuntimeReady(sm *session.Manager, agentSession *session.AgentSession) {
+ sm.UpdateRuntimeStatus(agentSession, &gatewayv1.RuntimeStatusEvent{
+ WorkerId: "test-runtime",
+ State: "ready",
+ Visible: true,
+ ActiveRunCount: 0,
+ Timestamp: time.Now().Unix(),
+ })
+}
+
func TestWebSocketRejectsRequestsBeforeAuth(t *testing.T) {
t.Parallel()
@@ -122,6 +158,7 @@ func TestWebSocketChatStartForwardsNormalizedRequestAndStreamsEvents(t *testing.
sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1")
agentSession := session.NewAgentSession(sm.LatestAuthSnapshot())
sm.SetSession(agentSession)
+ markRuntimeReady(sm, agentSession)
handler := server.NewWebSocketServer(&config.Config{
Token: "ws-token",
@@ -281,6 +318,43 @@ func TestWebSocketChatStartForwardsNormalizedRequestAndStreamsEvents(t *testing.
}
}
+func TestWebSocketChatStartRequiresRuntimeReady(t *testing.T) {
+ t.Parallel()
+
+ sm := session.NewManager()
+ sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1")
+ agentSession := session.NewAgentSession(sm.LatestAuthSnapshot())
+ sm.SetSession(agentSession)
+
+ handler := server.NewWebSocketServer(&config.Config{
+ Token: "ws-token",
+ RequestTimeout: time.Second,
+ }, sm)
+ conn, cleanup := dialGatewayWebSocket(t, handler)
+ defer cleanup()
+
+ authWebSocket(t, conn, "ws-token")
+ sendEnvelope(t, conn, "chat-not-ready", "chat.start", map[string]any{
+ "conversation_id": "conversation-not-ready",
+ "message": "hello gateway",
+ })
+
+ env := receiveEnvelope(t, conn)
+ if env.ID != "chat-not-ready" ||
+ env.Type != "error" ||
+ !strings.Contains(env.Error, "Desktop chat runtime is not ready") {
+ t.Fatalf("not-ready response = %#v, want chat runtime readiness error", env)
+ }
+ select {
+ case outbound := <-agentSession.Outbound():
+ t.Fatalf("unexpected outbound request while runtime not ready: %#v", outbound)
+ case <-time.After(100 * time.Millisecond):
+ }
+ if got := sm.ActiveChatRunConversationIDs(); len(got) != 0 {
+ t.Fatalf("active chat runs after not-ready request = %#v, want empty", got)
+ }
+}
+
func TestWebSocketChatStartClearsRunWhenAgentDeliveryStalls(t *testing.T) {
t.Parallel()
@@ -288,6 +362,7 @@ func TestWebSocketChatStartClearsRunWhenAgentDeliveryStalls(t *testing.T) {
sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1")
agentSession := session.NewAgentSession(sm.LatestAuthSnapshot())
sm.SetSession(agentSession)
+ markRuntimeReady(sm, agentSession)
handler := server.NewWebSocketServer(&config.Config{
Token: "ws-token",
@@ -321,6 +396,273 @@ func TestWebSocketChatStartClearsRunWhenAgentDeliveryStalls(t *testing.T) {
}
}
+func TestWebSocketChatStartClearsRunWhenDesktopDoesNotAccept(t *testing.T) {
+ t.Parallel()
+
+ sm := session.NewManager()
+ sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1")
+ agentSession := session.NewAgentSession(sm.LatestAuthSnapshot())
+ sm.SetSession(agentSession)
+ markRuntimeReady(sm, agentSession)
+
+ handler := server.NewWebSocketServer(&config.Config{
+ Token: "ws-token",
+ RequestTimeout: time.Second,
+ WebSocketWriteTimeout: time.Second,
+ ChatStartTimeout: 50 * time.Millisecond,
+ }, sm)
+ conn, cleanup := dialGatewayWebSocket(t, handler)
+ defer cleanup()
+
+ authWebSocket(t, conn, "ws-token")
+ sendEnvelope(t, conn, "chat-unaccepted", "chat.start", map[string]any{
+ "conversation_id": "conversation-unaccepted",
+ "message": "hello gateway",
+ })
+
+ outbound := readOutboundEnvelope(t, agentSession)
+ if outbound.GetChatRequest() == nil {
+ t.Fatalf("outbound payload = %T, want ChatRequest", outbound.GetPayload())
+ }
+ if got := sm.ActiveChatRunConversationIDs(); len(got) != 0 {
+ t.Fatalf("active chat runs before desktop accept = %#v, want empty", got)
+ }
+
+ env := receiveEnvelope(t, conn)
+ if env.ID != "chat-unaccepted" || env.Type != "chat.event" {
+ t.Fatalf("unaccepted desktop response = %#v, want chat.event", env)
+ }
+ var payload map[string]any
+ if err := json.Unmarshal(env.Payload, &payload); err != nil {
+ t.Fatalf("decode unaccepted desktop payload: %v", err)
+ }
+ if payload["type"] != "error" ||
+ payload["conversation_id"] != "conversation-unaccepted" ||
+ !strings.Contains(fmt.Sprint(payload["message"]), "Desktop backend did not accept") {
+ t.Fatalf("unaccepted desktop payload = %#v", payload)
+ }
+ if got := sm.ActiveChatRunConversationIDs(); len(got) != 0 {
+ t.Fatalf("active chat runs after unaccepted desktop request = %#v, want empty", got)
+ }
+ if status := sm.Status(); status.Online {
+ t.Fatalf("status online = true after desktop failed to accept chat request")
+ }
+}
+
+func TestWebSocketChatStartAcceptedByDesktopDoesNotTripStartTimeout(t *testing.T) {
+ t.Parallel()
+
+ sm := session.NewManager()
+ sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1")
+ agentSession := session.NewAgentSession(sm.LatestAuthSnapshot())
+ sm.SetSession(agentSession)
+ markRuntimeReady(sm, agentSession)
+
+ handler := server.NewWebSocketServer(&config.Config{
+ Token: "ws-token",
+ RequestTimeout: time.Second,
+ WebSocketWriteTimeout: time.Second,
+ ChatStartTimeout: 50 * time.Millisecond,
+ }, sm)
+ conn, cleanup := dialGatewayWebSocket(t, handler)
+ defer cleanup()
+
+ authWebSocket(t, conn, "ws-token")
+ sendEnvelope(t, conn, "chat-accepted", "chat.start", map[string]any{
+ "conversation_id": "conversation-accepted",
+ "message": "hello gateway",
+ })
+
+ outbound := readOutboundEnvelope(t, agentSession)
+ if outbound.GetChatRequest() == nil {
+ t.Fatalf("outbound payload = %T, want ChatRequest", outbound.GetPayload())
+ }
+ sm.DispatchFromAgent(&gatewayv1.AgentEnvelope{
+ RequestId: outbound.GetRequestId(),
+ Timestamp: time.Now().Unix(),
+ Payload: &gatewayv1.AgentEnvelope_ChatEvent{
+ ChatEvent: &gatewayv1.ChatEvent{
+ Type: gatewayv1.ChatEvent_TOKEN,
+ ConversationId: "conversation-accepted",
+ Data: `{"type":"accepted"}`,
+ },
+ },
+ })
+
+ if got := sm.ActiveChatRunConversationIDs(); len(got) != 0 {
+ t.Fatalf("active chat runs after desktop accept before start = %#v, want empty", got)
+ }
+ assertNoEnvelopeWithin(t, conn, 120*time.Millisecond)
+}
+
+func TestWebSocketChatStartFailsWhenDesktopAcceptsButDoesNotStart(t *testing.T) {
+ t.Parallel()
+
+ sm := session.NewManager()
+ sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1")
+ agentSession := session.NewAgentSession(sm.LatestAuthSnapshot())
+ sm.SetSession(agentSession)
+ markRuntimeReady(sm, agentSession)
+
+ handler := server.NewWebSocketServer(&config.Config{
+ Token: "ws-token",
+ RequestTimeout: time.Second,
+ WebSocketWriteTimeout: time.Second,
+ ChatStartTimeout: 25 * time.Millisecond,
+ ChatRenderStartTimeout: 75 * time.Millisecond,
+ }, sm)
+ conn, cleanup := dialGatewayWebSocket(t, handler)
+ defer cleanup()
+
+ authWebSocket(t, conn, "ws-token")
+ sendEnvelope(t, conn, "chat-render-stalled", "chat.start", map[string]any{
+ "conversation_id": "conversation-render-stalled",
+ "message": "hello gateway",
+ })
+
+ outbound := readOutboundEnvelope(t, agentSession)
+ if outbound.GetChatRequest() == nil {
+ t.Fatalf("outbound payload = %T, want ChatRequest", outbound.GetPayload())
+ }
+ sm.DispatchFromAgent(&gatewayv1.AgentEnvelope{
+ RequestId: outbound.GetRequestId(),
+ Timestamp: time.Now().Unix(),
+ Payload: &gatewayv1.AgentEnvelope_ChatEvent{
+ ChatEvent: &gatewayv1.ChatEvent{
+ Type: gatewayv1.ChatEvent_TOKEN,
+ ConversationId: "conversation-render-stalled",
+ Data: `{"type":"accepted"}`,
+ },
+ },
+ })
+
+ env := receiveEnvelope(t, conn)
+ if env.ID != "chat-render-stalled" || env.Type != "chat.event" {
+ t.Fatalf("render-stalled response = %#v, want chat.event", env)
+ }
+ var payload map[string]any
+ if err := json.Unmarshal(env.Payload, &payload); err != nil {
+ t.Fatalf("decode render-stalled payload: %v", err)
+ }
+ if payload["type"] != "error" ||
+ payload["conversation_id"] != "conversation-render-stalled" ||
+ !strings.Contains(fmt.Sprint(payload["message"]), "Desktop app accepted") {
+ t.Fatalf("render-stalled payload = %#v", payload)
+ }
+}
+
+func TestWebSocketChatResumeFailsPendingRunWhenDesktopStillDoesNotAccept(t *testing.T) {
+ t.Parallel()
+
+ sm := session.NewManager()
+ sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1")
+ agentSession := session.NewAgentSession(sm.LatestAuthSnapshot())
+ sm.SetSession(agentSession)
+ markRuntimeReady(sm, agentSession)
+
+ handler := server.NewWebSocketServer(&config.Config{
+ Token: "ws-token",
+ RequestTimeout: time.Second,
+ WebSocketWriteTimeout: time.Second,
+ ChatStartTimeout: 50 * time.Millisecond,
+ }, sm)
+ conn1, cleanup1 := dialGatewayWebSocket(t, handler)
+ defer cleanup1()
+
+ authWebSocket(t, conn1, "ws-token")
+ sendEnvelope(t, conn1, "chat-pending", "chat.start", map[string]any{
+ "conversation_id": "conversation-pending",
+ "message": "hello gateway",
+ })
+ outbound := readOutboundEnvelope(t, agentSession)
+ if outbound.GetChatRequest() == nil {
+ t.Fatalf("outbound payload = %T, want ChatRequest", outbound.GetPayload())
+ }
+ _ = conn1.Close()
+
+ conn2, cleanup2 := dialGatewayWebSocket(t, handler)
+ defer cleanup2()
+ authWebSocket(t, conn2, "ws-token")
+ sendEnvelope(t, conn2, "resume-pending", "chat.resume", map[string]any{
+ "request_id": outbound.GetRequestId(),
+ "conversation_id": "conversation-pending",
+ })
+
+ env := receiveEnvelope(t, conn2)
+ if env.ID != outbound.GetRequestId() || env.Type != "chat.event" {
+ t.Fatalf("resume pending response = %#v, want chat.event", env)
+ }
+ var payload map[string]any
+ if err := json.Unmarshal(env.Payload, &payload); err != nil {
+ t.Fatalf("decode resume pending payload: %v", err)
+ }
+ if payload["type"] != "error" || !strings.Contains(fmt.Sprint(payload["message"]), "Desktop backend did not accept") {
+ t.Fatalf("resume pending payload = %#v", payload)
+ }
+ if status := sm.Status(); status.Online {
+ t.Fatalf("status online = true after resumed pending chat was not accepted")
+ }
+}
+
+func TestWebSocketChatResumeFailsAcceptedRunWhenDesktopDoesNotStart(t *testing.T) {
+ t.Parallel()
+
+ sm := session.NewManager()
+ sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1")
+ agentSession := session.NewAgentSession(sm.LatestAuthSnapshot())
+ sm.SetSession(agentSession)
+ markRuntimeReady(sm, agentSession)
+
+ handler := server.NewWebSocketServer(&config.Config{
+ Token: "ws-token",
+ RequestTimeout: time.Second,
+ WebSocketWriteTimeout: time.Second,
+ ChatStartTimeout: 25 * time.Millisecond,
+ ChatRenderStartTimeout: 75 * time.Millisecond,
+ }, sm)
+ conn1, cleanup1 := dialGatewayWebSocket(t, handler)
+ defer cleanup1()
+
+ authWebSocket(t, conn1, "ws-token")
+ sendEnvelope(t, conn1, "chat-accepted-pending", "chat.start", map[string]any{
+ "conversation_id": "conversation-accepted-pending",
+ "message": "hello gateway",
+ })
+ outbound := readOutboundEnvelope(t, agentSession)
+ sm.DispatchFromAgent(&gatewayv1.AgentEnvelope{
+ RequestId: outbound.GetRequestId(),
+ Timestamp: time.Now().Unix(),
+ Payload: &gatewayv1.AgentEnvelope_ChatEvent{
+ ChatEvent: &gatewayv1.ChatEvent{
+ Type: gatewayv1.ChatEvent_TOKEN,
+ ConversationId: "conversation-accepted-pending",
+ Data: `{"type":"accepted"}`,
+ },
+ },
+ })
+ _ = conn1.Close()
+
+ conn2, cleanup2 := dialGatewayWebSocket(t, handler)
+ defer cleanup2()
+ authWebSocket(t, conn2, "ws-token")
+ sendEnvelope(t, conn2, "resume-accepted-pending", "chat.resume", map[string]any{
+ "request_id": outbound.GetRequestId(),
+ "conversation_id": "conversation-accepted-pending",
+ })
+
+ env := receiveEnvelope(t, conn2)
+ if env.ID != outbound.GetRequestId() || env.Type != "chat.event" {
+ t.Fatalf("resume accepted pending response = %#v, want chat.event", env)
+ }
+ var payload map[string]any
+ if err := json.Unmarshal(env.Payload, &payload); err != nil {
+ t.Fatalf("decode resume accepted pending payload: %v", err)
+ }
+ if payload["type"] != "error" || !strings.Contains(fmt.Sprint(payload["message"]), "Desktop app accepted") {
+ t.Fatalf("resume accepted pending payload = %#v", payload)
+ }
+}
+
func TestWebSocketChatStartDedupesClientRequestID(t *testing.T) {
t.Parallel()
@@ -328,6 +670,7 @@ func TestWebSocketChatStartDedupesClientRequestID(t *testing.T) {
sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1")
agentSession := session.NewAgentSession(sm.LatestAuthSnapshot())
sm.SetSession(agentSession)
+ markRuntimeReady(sm, agentSession)
handler := server.NewWebSocketServer(&config.Config{
Token: "ws-token",
@@ -409,6 +752,7 @@ func TestWebSocketChatStartFailsWhenAgentSessionDisconnects(t *testing.T) {
sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1")
agentSession := session.NewAgentSession(sm.LatestAuthSnapshot())
sm.SetSession(agentSession)
+ markRuntimeReady(sm, agentSession)
handler := server.NewWebSocketServer(&config.Config{
Token: "ws-token",
@@ -449,6 +793,7 @@ func TestWebSocketChatStartFailsWhenAgentSessionDisconnects(t *testing.T) {
replacementSession := session.NewAgentSession(sm.LatestAuthSnapshot())
sm.SetSession(replacementSession)
+ markRuntimeReady(sm, replacementSession)
sendEnvelope(t, conn, "chat-2", "chat.start", map[string]any{
"conversation_id": "conversation-1",
"client_request_id": "client-submit-1",
@@ -467,6 +812,7 @@ func TestWebSocketMemoryManageForwardsJSONArgs(t *testing.T) {
sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1")
agentSession := session.NewAgentSession(sm.LatestAuthSnapshot())
sm.SetSession(agentSession)
+ markRuntimeReady(sm, agentSession)
handler := server.NewWebSocketServer(&config.Config{
Token: "ws-token",
@@ -530,6 +876,7 @@ func TestWebSocketChatResumeReplaysEventsAfterReconnect(t *testing.T) {
sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1")
agentSession := session.NewAgentSession(sm.LatestAuthSnapshot())
sm.SetSession(agentSession)
+ markRuntimeReady(sm, agentSession)
handler := server.NewWebSocketServer(&config.Config{
Token: "ws-token",
@@ -639,6 +986,7 @@ func TestWebSocketChatAttachReplaysBufferedEventsByConversationID(t *testing.T)
sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1")
agentSession := session.NewAgentSession(sm.LatestAuthSnapshot())
sm.SetSession(agentSession)
+ markRuntimeReady(sm, agentSession)
handler := server.NewWebSocketServer(&config.Config{
Token: "ws-token",
@@ -729,6 +1077,7 @@ func TestWebSocketChatAttachExpiresAfterDoneHistoryUpsert(t *testing.T) {
sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1")
agentSession := session.NewAgentSession(sm.LatestAuthSnapshot())
sm.SetSession(agentSession)
+ markRuntimeReady(sm, agentSession)
handler := server.NewWebSocketServer(&config.Config{
Token: "ws-token",
@@ -794,6 +1143,7 @@ func TestWebSocketChatCancelReleasesBufferedAttachRun(t *testing.T) {
sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1")
agentSession := session.NewAgentSession(sm.LatestAuthSnapshot())
sm.SetSession(agentSession)
+ markRuntimeReady(sm, agentSession)
handler := server.NewWebSocketServer(&config.Config{
Token: "ws-token",
@@ -853,6 +1203,7 @@ func TestWebSocketForwardsHistorySettingsAndFsRPCs(t *testing.T) {
sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1")
agentSession := session.NewAgentSession(sm.LatestAuthSnapshot())
sm.SetSession(agentSession)
+ markRuntimeReady(sm, agentSession)
handler := server.NewWebSocketServer(&config.Config{
Token: "ws-token",
@@ -871,6 +1222,7 @@ func TestWebSocketForwardsHistorySettingsAndFsRPCs(t *testing.T) {
if chatOutbound.GetChatRequest() == nil {
t.Fatalf("chat outbound payload = %T, want ChatRequest", chatOutbound.GetPayload())
}
+ dispatchChatStarted(t, sm, chatOutbound.GetRequestId(), "conversation-1")
sendEnvelope(t, conn, "history-1", "history.list", map[string]any{
"page": 2,
@@ -1318,6 +1670,7 @@ func TestWebSocketDefaultsInvalidHistoryListPagination(t *testing.T) {
sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1")
agentSession := session.NewAgentSession(sm.LatestAuthSnapshot())
sm.SetSession(agentSession)
+ markRuntimeReady(sm, agentSession)
handler := server.NewWebSocketServer(&config.Config{
Token: "ws-token",
From b34feaba867a3cbd8da53d3429277249f251ee3d Mon Sep 17 00:00:00 2001
From: su-fen <715041@qq.com>
Date: Wed, 10 Jun 2026 10:37:43 +0800
Subject: [PATCH 5/9] fix(gui): lease gateway chat requests and validate models
---
.../src-tauri/src/commands/gateway.rs | 98 +-
.../agent-gui/src-tauri/src/commands/git.rs | 13 +-
crates/agent-gui/src-tauri/src/lib.rs | 7 +
.../src-tauri/src/services/gateway.rs | 979 +++++++++++++++++-
.../conversation/run/gatewayBridgeEvents.ts | 16 +-
crates/agent-gui/src/pages/ChatPage.tsx | 116 +--
.../src/pages/chat/ChatComposerBar.tsx | 37 -
.../src/pages/chat/gatewayBridgeTypes.ts | 16 +
.../src/pages/chat/modelSelection.ts | 71 ++
.../src/pages/chat/useGatewayBridgeBatcher.ts | 20 +-
.../pages/chat/useGatewayBridgeListeners.ts | 382 ++++++-
.../src/pages/chat/usePendingUploads.ts | 186 +++-
.../test/chat/gateway-bridge-events.test.mjs | 45 +-
.../test/chat/model-selection.test.mjs | 128 +++
14 files changed, 1840 insertions(+), 274 deletions(-)
create mode 100644 crates/agent-gui/src/pages/chat/modelSelection.ts
create mode 100644 crates/agent-gui/test/chat/model-selection.test.mjs
diff --git a/crates/agent-gui/src-tauri/src/commands/gateway.rs b/crates/agent-gui/src-tauri/src/commands/gateway.rs
index f4112d74e..3691e5616 100644
--- a/crates/agent-gui/src-tauri/src/commands/gateway.rs
+++ b/crates/agent-gui/src-tauri/src/commands/gateway.rs
@@ -4,8 +4,9 @@ use serde_json::Value;
use crate::commands::settings::{load_remote_settings, open_db, parse_remote_settings_payload};
use crate::services::gateway::{
- build_history_sync_activity, GatewayChatRequestEvent, GatewayController, GatewayStatusSnapshot,
- GatewayTunnelCreateInput, GatewayTunnelSummary, GatewayTunnelUpdateInput,
+ build_history_sync_activity, GatewayChatClaimedRequest, GatewayChatRequestEvent,
+ GatewayController, GatewayStatusSnapshot, GatewayTunnelCreateInput, GatewayTunnelSummary,
+ GatewayTunnelUpdateInput,
};
#[tauri::command]
@@ -44,9 +45,12 @@ pub fn gateway_status(
pub async fn gateway_send_chat_event(
request_id: String,
event: Value,
+ worker_id: Option,
gateway_controller: tauri::State<'_, Arc>,
) -> Result<(), String> {
- gateway_controller.send_chat_event(request_id, event).await
+ gateway_controller
+ .send_chat_event(request_id, event, worker_id)
+ .await
}
#[tauri::command(rename_all = "snake_case")]
@@ -64,6 +68,94 @@ pub fn gateway_take_pending_chat_requests(
gateway_controller.take_pending_chat_requests()
}
+#[tauri::command(rename_all = "snake_case")]
+pub async fn gateway_chat_claim_next(
+ worker_id: String,
+ lease_ms: Option,
+ gateway_controller: tauri::State<'_, Arc>,
+) -> Result
-
- {
- if (!tunnelToolAvailable) return;
- onOpenTunnelToolPanel?.();
- }}
- aria-label={
- tunnelToolAvailable
- ? t("chat.runtime.tunnelToolAvailable")
- : tunnelTooltip
- }
- className={cn(
- "composer-toolbar-action inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full outline-hidden transition-colors",
- "disabled:pointer-events-none disabled:opacity-40",
- tunnelToolAvailable
- ? "text-cyan-600 hover:text-cyan-700 dark:text-cyan-300 dark:hover:text-cyan-200"
- : "text-muted-foreground hover:text-foreground dark:hover:text-white",
- )}
- >
-
-
-
-
{reasoningOptions.length > 0 ? (