diff --git a/crates/agent-gateway/internal/proto/v2/gateway.pb.go b/crates/agent-gateway/internal/proto/v2/gateway.pb.go index b8bade09d..c70e0abbf 100644 --- a/crates/agent-gateway/internal/proto/v2/gateway.pb.go +++ b/crates/agent-gateway/internal/proto/v2/gateway.pb.go @@ -277,7 +277,7 @@ func (x ChatIngressAck_Action) Number() protoreflect.EnumNumber { // Deprecated: Use ChatIngressAck_Action.Descriptor instead. func (ChatIngressAck_Action) EnumDescriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{148, 0} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{150, 0} } type GatewayEnvelope struct { @@ -334,6 +334,7 @@ type GatewayEnvelope struct { // *GatewayEnvelope_ManagedProcessRequest // *GatewayEnvelope_HistoryBranch // *GatewayEnvelope_ProviderUsage + // *GatewayEnvelope_ChatFileOpen Payload isGatewayEnvelope_Payload `protobuf_oneof:"payload"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -822,6 +823,15 @@ func (x *GatewayEnvelope) GetProviderUsage() *ProviderUsageRequest { return nil } +func (x *GatewayEnvelope) GetChatFileOpen() *ChatFileOpenRequest { + if x != nil { + if x, ok := x.Payload.(*GatewayEnvelope_ChatFileOpen); ok { + return x.ChatFileOpen + } + } + return nil +} + type isGatewayEnvelope_Payload interface { isGatewayEnvelope_Payload() } @@ -1018,6 +1028,10 @@ type GatewayEnvelope_ProviderUsage struct { ProviderUsage *ProviderUsageRequest `protobuf:"bytes,93,opt,name=provider_usage,json=providerUsage,proto3,oneof"` } +type GatewayEnvelope_ChatFileOpen struct { + ChatFileOpen *ChatFileOpenRequest `protobuf:"bytes,94,opt,name=chat_file_open,json=chatFileOpen,proto3,oneof"` +} + func (*GatewayEnvelope_ChatCommand) isGatewayEnvelope_Payload() {} func (*GatewayEnvelope_CronManage) isGatewayEnvelope_Payload() {} @@ -1114,6 +1128,8 @@ func (*GatewayEnvelope_HistoryBranch) isGatewayEnvelope_Payload() {} func (*GatewayEnvelope_ProviderUsage) isGatewayEnvelope_Payload() {} +func (*GatewayEnvelope_ChatFileOpen) 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"` @@ -1180,6 +1196,7 @@ type AgentEnvelope struct { // *AgentEnvelope_ChatIngressBatch // *AgentEnvelope_ChatIngressResume // *AgentEnvelope_ChatIngressFragment + // *AgentEnvelope_ChatFileOpenResp // *AgentEnvelope_Error Payload isAgentEnvelope_Payload `protobuf_oneof:"payload"` unknownFields protoimpl.UnknownFields @@ -1777,6 +1794,15 @@ func (x *AgentEnvelope) GetChatIngressFragment() *ChatIngressFragment { return nil } +func (x *AgentEnvelope) GetChatFileOpenResp() *ChatFileOpenResponse { + if x != nil { + if x, ok := x.Payload.(*AgentEnvelope_ChatFileOpenResp); ok { + return x.ChatFileOpenResp + } + } + return nil +} + func (x *AgentEnvelope) GetError() *ErrorResponse { if x != nil { if x, ok := x.Payload.(*AgentEnvelope_Error); ok { @@ -2030,6 +2056,10 @@ type AgentEnvelope_ChatIngressFragment struct { ChatIngressFragment *ChatIngressFragment `protobuf:"bytes,97,opt,name=chat_ingress_fragment,json=chatIngressFragment,proto3,oneof"` } +type AgentEnvelope_ChatFileOpenResp struct { + ChatFileOpenResp *ChatFileOpenResponse `protobuf:"bytes,98,opt,name=chat_file_open_resp,json=chatFileOpenResp,proto3,oneof"` +} + type AgentEnvelope_Error struct { Error *ErrorResponse `protobuf:"bytes,99,opt,name=error,proto3,oneof"` } @@ -2154,6 +2184,8 @@ func (*AgentEnvelope_ChatIngressResume) isAgentEnvelope_Payload() {} func (*AgentEnvelope_ChatIngressFragment) isAgentEnvelope_Payload() {} +func (*AgentEnvelope_ChatFileOpenResp) isAgentEnvelope_Payload() {} + func (*AgentEnvelope_Error) isAgentEnvelope_Payload() {} type ChatSelectedModel struct { @@ -10593,6 +10625,206 @@ func (x *FsReadWorkspaceImageResponse) GetContentHash() string { return "" } +type ChatFileOpenRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` + Workdir string `protobuf:"bytes,2,opt,name=workdir,proto3" json:"workdir,omitempty"` + Path string `protobuf:"bytes,3,opt,name=path,proto3" json:"path,omitempty"` + Source string `protobuf:"bytes,4,opt,name=source,proto3" json:"source,omitempty"` + Line *uint32 `protobuf:"varint,5,opt,name=line,proto3,oneof" json:"line,omitempty"` + EndLine *uint32 `protobuf:"varint,6,opt,name=end_line,json=endLine,proto3,oneof" json:"end_line,omitempty"` + Column *uint32 `protobuf:"varint,7,opt,name=column,proto3,oneof" json:"column,omitempty"` + OpenInFileManager bool `protobuf:"varint,8,opt,name=open_in_file_manager,json=openInFileManager,proto3" json:"open_in_file_manager,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ChatFileOpenRequest) Reset() { + *x = ChatFileOpenRequest{} + mi := &file_proto_v2_gateway_proto_msgTypes[124] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ChatFileOpenRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChatFileOpenRequest) ProtoMessage() {} + +func (x *ChatFileOpenRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_v2_gateway_proto_msgTypes[124] + 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 ChatFileOpenRequest.ProtoReflect.Descriptor instead. +func (*ChatFileOpenRequest) Descriptor() ([]byte, []int) { + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{124} +} + +func (x *ChatFileOpenRequest) GetConversationId() string { + if x != nil { + return x.ConversationId + } + return "" +} + +func (x *ChatFileOpenRequest) GetWorkdir() string { + if x != nil { + return x.Workdir + } + return "" +} + +func (x *ChatFileOpenRequest) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *ChatFileOpenRequest) GetSource() string { + if x != nil { + return x.Source + } + return "" +} + +func (x *ChatFileOpenRequest) GetLine() uint32 { + if x != nil && x.Line != nil { + return *x.Line + } + return 0 +} + +func (x *ChatFileOpenRequest) GetEndLine() uint32 { + if x != nil && x.EndLine != nil { + return *x.EndLine + } + return 0 +} + +func (x *ChatFileOpenRequest) GetColumn() uint32 { + if x != nil && x.Column != nil { + return *x.Column + } + return 0 +} + +func (x *ChatFileOpenRequest) GetOpenInFileManager() bool { + if x != nil { + return x.OpenInFileManager + } + return false +} + +type ChatFileOpenResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Action string `protobuf:"bytes,1,opt,name=action,proto3" json:"action,omitempty"` + Kind string `protobuf:"bytes,2,opt,name=kind,proto3" json:"kind,omitempty"` + Workdir string `protobuf:"bytes,3,opt,name=workdir,proto3" json:"workdir,omitempty"` + Path string `protobuf:"bytes,4,opt,name=path,proto3" json:"path,omitempty"` + Line *uint32 `protobuf:"varint,5,opt,name=line,proto3,oneof" json:"line,omitempty"` + EndLine *uint32 `protobuf:"varint,6,opt,name=end_line,json=endLine,proto3,oneof" json:"end_line,omitempty"` + Column *uint32 `protobuf:"varint,7,opt,name=column,proto3,oneof" json:"column,omitempty"` + OutsideWorkspace bool `protobuf:"varint,8,opt,name=outside_workspace,json=outsideWorkspace,proto3" json:"outside_workspace,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ChatFileOpenResponse) Reset() { + *x = ChatFileOpenResponse{} + mi := &file_proto_v2_gateway_proto_msgTypes[125] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ChatFileOpenResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChatFileOpenResponse) ProtoMessage() {} + +func (x *ChatFileOpenResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_v2_gateway_proto_msgTypes[125] + 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 ChatFileOpenResponse.ProtoReflect.Descriptor instead. +func (*ChatFileOpenResponse) Descriptor() ([]byte, []int) { + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{125} +} + +func (x *ChatFileOpenResponse) GetAction() string { + if x != nil { + return x.Action + } + return "" +} + +func (x *ChatFileOpenResponse) GetKind() string { + if x != nil { + return x.Kind + } + return "" +} + +func (x *ChatFileOpenResponse) GetWorkdir() string { + if x != nil { + return x.Workdir + } + return "" +} + +func (x *ChatFileOpenResponse) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *ChatFileOpenResponse) GetLine() uint32 { + if x != nil && x.Line != nil { + return *x.Line + } + return 0 +} + +func (x *ChatFileOpenResponse) GetEndLine() uint32 { + if x != nil && x.EndLine != nil { + return *x.EndLine + } + return 0 +} + +func (x *ChatFileOpenResponse) GetColumn() uint32 { + if x != nil && x.Column != nil { + return *x.Column + } + return 0 +} + +func (x *ChatFileOpenResponse) GetOutsideWorkspace() bool { + if x != nil { + return x.OutsideWorkspace + } + return false +} + type FsWriteTextRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Workdir string `protobuf:"bytes,1,opt,name=workdir,proto3" json:"workdir,omitempty"` @@ -10609,7 +10841,7 @@ type FsWriteTextRequest struct { func (x *FsWriteTextRequest) Reset() { *x = FsWriteTextRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[124] + mi := &file_proto_v2_gateway_proto_msgTypes[126] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10621,7 +10853,7 @@ func (x *FsWriteTextRequest) String() string { func (*FsWriteTextRequest) ProtoMessage() {} func (x *FsWriteTextRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[124] + mi := &file_proto_v2_gateway_proto_msgTypes[126] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10634,7 +10866,7 @@ func (x *FsWriteTextRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FsWriteTextRequest.ProtoReflect.Descriptor instead. func (*FsWriteTextRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{124} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{126} } func (x *FsWriteTextRequest) GetWorkdir() string { @@ -10708,7 +10940,7 @@ type FsWriteTextResponse struct { func (x *FsWriteTextResponse) Reset() { *x = FsWriteTextResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[125] + mi := &file_proto_v2_gateway_proto_msgTypes[127] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10720,7 +10952,7 @@ func (x *FsWriteTextResponse) String() string { func (*FsWriteTextResponse) ProtoMessage() {} func (x *FsWriteTextResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[125] + mi := &file_proto_v2_gateway_proto_msgTypes[127] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10733,7 +10965,7 @@ func (x *FsWriteTextResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use FsWriteTextResponse.ProtoReflect.Descriptor instead. func (*FsWriteTextResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{125} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{127} } func (x *FsWriteTextResponse) GetPath() string { @@ -10795,7 +11027,7 @@ type FsCreateDirRequest struct { func (x *FsCreateDirRequest) Reset() { *x = FsCreateDirRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[126] + mi := &file_proto_v2_gateway_proto_msgTypes[128] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10807,7 +11039,7 @@ func (x *FsCreateDirRequest) String() string { func (*FsCreateDirRequest) ProtoMessage() {} func (x *FsCreateDirRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[126] + mi := &file_proto_v2_gateway_proto_msgTypes[128] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10820,7 +11052,7 @@ func (x *FsCreateDirRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FsCreateDirRequest.ProtoReflect.Descriptor instead. func (*FsCreateDirRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{126} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{128} } func (x *FsCreateDirRequest) GetWorkdir() string { @@ -10847,7 +11079,7 @@ type FsCreateDirResponse struct { func (x *FsCreateDirResponse) Reset() { *x = FsCreateDirResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[127] + mi := &file_proto_v2_gateway_proto_msgTypes[129] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10859,7 +11091,7 @@ func (x *FsCreateDirResponse) String() string { func (*FsCreateDirResponse) ProtoMessage() {} func (x *FsCreateDirResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[127] + mi := &file_proto_v2_gateway_proto_msgTypes[129] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10872,7 +11104,7 @@ func (x *FsCreateDirResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use FsCreateDirResponse.ProtoReflect.Descriptor instead. func (*FsCreateDirResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{127} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{129} } func (x *FsCreateDirResponse) GetPath() string { @@ -10900,7 +11132,7 @@ type FsRenameRequest struct { func (x *FsRenameRequest) Reset() { *x = FsRenameRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[128] + mi := &file_proto_v2_gateway_proto_msgTypes[130] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10912,7 +11144,7 @@ func (x *FsRenameRequest) String() string { func (*FsRenameRequest) ProtoMessage() {} func (x *FsRenameRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[128] + mi := &file_proto_v2_gateway_proto_msgTypes[130] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10925,7 +11157,7 @@ func (x *FsRenameRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FsRenameRequest.ProtoReflect.Descriptor instead. func (*FsRenameRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{128} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{130} } func (x *FsRenameRequest) GetWorkdir() string { @@ -10960,7 +11192,7 @@ type FsRenameResponse struct { func (x *FsRenameResponse) Reset() { *x = FsRenameResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[129] + mi := &file_proto_v2_gateway_proto_msgTypes[131] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10972,7 +11204,7 @@ func (x *FsRenameResponse) String() string { func (*FsRenameResponse) ProtoMessage() {} func (x *FsRenameResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[129] + mi := &file_proto_v2_gateway_proto_msgTypes[131] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10985,7 +11217,7 @@ func (x *FsRenameResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use FsRenameResponse.ProtoReflect.Descriptor instead. func (*FsRenameResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{129} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{131} } func (x *FsRenameResponse) GetFromPath() string { @@ -11019,7 +11251,7 @@ type FsDeleteRequest struct { func (x *FsDeleteRequest) Reset() { *x = FsDeleteRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[130] + mi := &file_proto_v2_gateway_proto_msgTypes[132] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11031,7 +11263,7 @@ func (x *FsDeleteRequest) String() string { func (*FsDeleteRequest) ProtoMessage() {} func (x *FsDeleteRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[130] + mi := &file_proto_v2_gateway_proto_msgTypes[132] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11044,7 +11276,7 @@ func (x *FsDeleteRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FsDeleteRequest.ProtoReflect.Descriptor instead. func (*FsDeleteRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{130} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{132} } func (x *FsDeleteRequest) GetWorkdir() string { @@ -11071,7 +11303,7 @@ type FsDeleteResponse struct { func (x *FsDeleteResponse) Reset() { *x = FsDeleteResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[131] + mi := &file_proto_v2_gateway_proto_msgTypes[133] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11083,7 +11315,7 @@ func (x *FsDeleteResponse) String() string { func (*FsDeleteResponse) ProtoMessage() {} func (x *FsDeleteResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[131] + mi := &file_proto_v2_gateway_proto_msgTypes[133] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11096,7 +11328,7 @@ func (x *FsDeleteResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use FsDeleteResponse.ProtoReflect.Descriptor instead. func (*FsDeleteResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{131} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{133} } func (x *FsDeleteResponse) GetPath() string { @@ -11122,7 +11354,7 @@ type PingRequest struct { func (x *PingRequest) Reset() { *x = PingRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[132] + mi := &file_proto_v2_gateway_proto_msgTypes[134] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11134,7 +11366,7 @@ func (x *PingRequest) String() string { func (*PingRequest) ProtoMessage() {} func (x *PingRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[132] + mi := &file_proto_v2_gateway_proto_msgTypes[134] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11147,7 +11379,7 @@ func (x *PingRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PingRequest.ProtoReflect.Descriptor instead. func (*PingRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{132} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{134} } func (x *PingRequest) GetTimestamp() int64 { @@ -11166,7 +11398,7 @@ type PongResponse struct { func (x *PongResponse) Reset() { *x = PongResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[133] + mi := &file_proto_v2_gateway_proto_msgTypes[135] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11178,7 +11410,7 @@ func (x *PongResponse) String() string { func (*PongResponse) ProtoMessage() {} func (x *PongResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[133] + mi := &file_proto_v2_gateway_proto_msgTypes[135] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11191,7 +11423,7 @@ func (x *PongResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PongResponse.ProtoReflect.Descriptor instead. func (*PongResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{133} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{135} } func (x *PongResponse) GetTimestamp() int64 { @@ -11211,7 +11443,7 @@ type ErrorResponse struct { func (x *ErrorResponse) Reset() { *x = ErrorResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[134] + mi := &file_proto_v2_gateway_proto_msgTypes[136] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11223,7 +11455,7 @@ func (x *ErrorResponse) String() string { func (*ErrorResponse) ProtoMessage() {} func (x *ErrorResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[134] + mi := &file_proto_v2_gateway_proto_msgTypes[136] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11236,7 +11468,7 @@ func (x *ErrorResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ErrorResponse.ProtoReflect.Descriptor instead. func (*ErrorResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{134} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{136} } func (x *ErrorResponse) GetCode() int32 { @@ -11265,7 +11497,7 @@ type ProviderModelsRequest struct { func (x *ProviderModelsRequest) Reset() { *x = ProviderModelsRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[135] + mi := &file_proto_v2_gateway_proto_msgTypes[137] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11277,7 +11509,7 @@ func (x *ProviderModelsRequest) String() string { func (*ProviderModelsRequest) ProtoMessage() {} func (x *ProviderModelsRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[135] + mi := &file_proto_v2_gateway_proto_msgTypes[137] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11290,7 +11522,7 @@ func (x *ProviderModelsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderModelsRequest.ProtoReflect.Descriptor instead. func (*ProviderModelsRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{135} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{137} } func (x *ProviderModelsRequest) GetProviderType() string { @@ -11330,7 +11562,7 @@ type ProviderModelsResponse struct { func (x *ProviderModelsResponse) Reset() { *x = ProviderModelsResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[136] + mi := &file_proto_v2_gateway_proto_msgTypes[138] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11342,7 +11574,7 @@ func (x *ProviderModelsResponse) String() string { func (*ProviderModelsResponse) ProtoMessage() {} func (x *ProviderModelsResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[136] + mi := &file_proto_v2_gateway_proto_msgTypes[138] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11355,7 +11587,7 @@ func (x *ProviderModelsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderModelsResponse.ProtoReflect.Descriptor instead. func (*ProviderModelsResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{136} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{138} } func (x *ProviderModelsResponse) GetModelsJson() string { @@ -11378,7 +11610,7 @@ type ProviderUsageRequest struct { func (x *ProviderUsageRequest) Reset() { *x = ProviderUsageRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[137] + mi := &file_proto_v2_gateway_proto_msgTypes[139] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11390,7 +11622,7 @@ func (x *ProviderUsageRequest) String() string { func (*ProviderUsageRequest) ProtoMessage() {} func (x *ProviderUsageRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[137] + mi := &file_proto_v2_gateway_proto_msgTypes[139] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11403,7 +11635,7 @@ func (x *ProviderUsageRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderUsageRequest.ProtoReflect.Descriptor instead. func (*ProviderUsageRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{137} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{139} } func (x *ProviderUsageRequest) GetProviderId() string { @@ -11436,7 +11668,7 @@ type ProviderUsageResponse struct { func (x *ProviderUsageResponse) Reset() { *x = ProviderUsageResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[138] + mi := &file_proto_v2_gateway_proto_msgTypes[140] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11448,7 +11680,7 @@ func (x *ProviderUsageResponse) String() string { func (*ProviderUsageResponse) ProtoMessage() {} func (x *ProviderUsageResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[138] + mi := &file_proto_v2_gateway_proto_msgTypes[140] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11461,7 +11693,7 @@ func (x *ProviderUsageResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderUsageResponse.ProtoReflect.Descriptor instead. func (*ProviderUsageResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{138} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{140} } func (x *ProviderUsageResponse) GetResultJson() string { @@ -11485,7 +11717,7 @@ type ChatIngressBatch struct { func (x *ChatIngressBatch) Reset() { *x = ChatIngressBatch{} - mi := &file_proto_v2_gateway_proto_msgTypes[139] + mi := &file_proto_v2_gateway_proto_msgTypes[141] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11497,7 +11729,7 @@ func (x *ChatIngressBatch) String() string { func (*ChatIngressBatch) ProtoMessage() {} func (x *ChatIngressBatch) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[139] + mi := &file_proto_v2_gateway_proto_msgTypes[141] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11510,7 +11742,7 @@ func (x *ChatIngressBatch) ProtoReflect() protoreflect.Message { // Deprecated: Use ChatIngressBatch.ProtoReflect.Descriptor instead. func (*ChatIngressBatch) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{139} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{141} } func (x *ChatIngressBatch) GetRunId() string { @@ -11556,7 +11788,7 @@ type ChatIngressRecord struct { func (x *ChatIngressRecord) Reset() { *x = ChatIngressRecord{} - mi := &file_proto_v2_gateway_proto_msgTypes[140] + mi := &file_proto_v2_gateway_proto_msgTypes[142] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11568,7 +11800,7 @@ func (x *ChatIngressRecord) String() string { func (*ChatIngressRecord) ProtoMessage() {} func (x *ChatIngressRecord) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[140] + mi := &file_proto_v2_gateway_proto_msgTypes[142] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11581,7 +11813,7 @@ func (x *ChatIngressRecord) ProtoReflect() protoreflect.Message { // Deprecated: Use ChatIngressRecord.ProtoReflect.Descriptor instead. func (*ChatIngressRecord) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{140} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{142} } func (x *ChatIngressRecord) GetPayload() isChatIngressRecord_Payload { @@ -11665,7 +11897,7 @@ type ChatIngressDelta struct { func (x *ChatIngressDelta) Reset() { *x = ChatIngressDelta{} - mi := &file_proto_v2_gateway_proto_msgTypes[141] + mi := &file_proto_v2_gateway_proto_msgTypes[143] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11677,7 +11909,7 @@ func (x *ChatIngressDelta) String() string { func (*ChatIngressDelta) ProtoMessage() {} func (x *ChatIngressDelta) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[141] + mi := &file_proto_v2_gateway_proto_msgTypes[143] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11690,7 +11922,7 @@ func (x *ChatIngressDelta) ProtoReflect() protoreflect.Message { // Deprecated: Use ChatIngressDelta.ProtoReflect.Descriptor instead. func (*ChatIngressDelta) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{141} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{143} } func (x *ChatIngressDelta) GetEventJson() string { @@ -11717,7 +11949,7 @@ type ChatIngressHeartbeat struct { func (x *ChatIngressHeartbeat) Reset() { *x = ChatIngressHeartbeat{} - mi := &file_proto_v2_gateway_proto_msgTypes[142] + mi := &file_proto_v2_gateway_proto_msgTypes[144] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11729,7 +11961,7 @@ func (x *ChatIngressHeartbeat) String() string { func (*ChatIngressHeartbeat) ProtoMessage() {} func (x *ChatIngressHeartbeat) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[142] + mi := &file_proto_v2_gateway_proto_msgTypes[144] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11742,7 +11974,7 @@ func (x *ChatIngressHeartbeat) ProtoReflect() protoreflect.Message { // Deprecated: Use ChatIngressHeartbeat.ProtoReflect.Descriptor instead. func (*ChatIngressHeartbeat) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{142} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{144} } func (x *ChatIngressHeartbeat) GetUpdatedAt() int64 { @@ -11767,7 +11999,7 @@ type ChatIngressCheckpoint struct { func (x *ChatIngressCheckpoint) Reset() { *x = ChatIngressCheckpoint{} - mi := &file_proto_v2_gateway_proto_msgTypes[143] + mi := &file_proto_v2_gateway_proto_msgTypes[145] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11779,7 +12011,7 @@ func (x *ChatIngressCheckpoint) String() string { func (*ChatIngressCheckpoint) ProtoMessage() {} func (x *ChatIngressCheckpoint) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[143] + mi := &file_proto_v2_gateway_proto_msgTypes[145] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11792,7 +12024,7 @@ func (x *ChatIngressCheckpoint) ProtoReflect() protoreflect.Message { // Deprecated: Use ChatIngressCheckpoint.ProtoReflect.Descriptor instead. func (*ChatIngressCheckpoint) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{143} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{145} } func (x *ChatIngressCheckpoint) GetCoversThroughSeq() uint64 { @@ -11862,7 +12094,7 @@ type ChatIngressTerminal struct { func (x *ChatIngressTerminal) Reset() { *x = ChatIngressTerminal{} - mi := &file_proto_v2_gateway_proto_msgTypes[144] + mi := &file_proto_v2_gateway_proto_msgTypes[146] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11874,7 +12106,7 @@ func (x *ChatIngressTerminal) String() string { func (*ChatIngressTerminal) ProtoMessage() {} func (x *ChatIngressTerminal) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[144] + mi := &file_proto_v2_gateway_proto_msgTypes[146] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11887,7 +12119,7 @@ func (x *ChatIngressTerminal) ProtoReflect() protoreflect.Message { // Deprecated: Use ChatIngressTerminal.ProtoReflect.Descriptor instead. func (*ChatIngressTerminal) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{144} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{146} } func (x *ChatIngressTerminal) GetCoversThroughSeq() uint64 { @@ -11971,7 +12203,7 @@ type ChatIngressResume struct { func (x *ChatIngressResume) Reset() { *x = ChatIngressResume{} - mi := &file_proto_v2_gateway_proto_msgTypes[145] + mi := &file_proto_v2_gateway_proto_msgTypes[147] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11983,7 +12215,7 @@ func (x *ChatIngressResume) String() string { func (*ChatIngressResume) ProtoMessage() {} func (x *ChatIngressResume) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[145] + mi := &file_proto_v2_gateway_proto_msgTypes[147] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11996,7 +12228,7 @@ func (x *ChatIngressResume) ProtoReflect() protoreflect.Message { // Deprecated: Use ChatIngressResume.ProtoReflect.Descriptor instead. func (*ChatIngressResume) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{145} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{147} } func (x *ChatIngressResume) GetRuns() []*ChatIngressRunResume { @@ -12022,7 +12254,7 @@ type ChatIngressRunResume struct { func (x *ChatIngressRunResume) Reset() { *x = ChatIngressRunResume{} - mi := &file_proto_v2_gateway_proto_msgTypes[146] + mi := &file_proto_v2_gateway_proto_msgTypes[148] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12034,7 +12266,7 @@ func (x *ChatIngressRunResume) String() string { func (*ChatIngressRunResume) ProtoMessage() {} func (x *ChatIngressRunResume) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[146] + mi := &file_proto_v2_gateway_proto_msgTypes[148] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12047,7 +12279,7 @@ func (x *ChatIngressRunResume) ProtoReflect() protoreflect.Message { // Deprecated: Use ChatIngressRunResume.ProtoReflect.Descriptor instead. func (*ChatIngressRunResume) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{146} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{148} } func (x *ChatIngressRunResume) GetRunId() string { @@ -12125,7 +12357,7 @@ type ChatIngressFragment struct { func (x *ChatIngressFragment) Reset() { *x = ChatIngressFragment{} - mi := &file_proto_v2_gateway_proto_msgTypes[147] + mi := &file_proto_v2_gateway_proto_msgTypes[149] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12137,7 +12369,7 @@ func (x *ChatIngressFragment) String() string { func (*ChatIngressFragment) ProtoMessage() {} func (x *ChatIngressFragment) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[147] + mi := &file_proto_v2_gateway_proto_msgTypes[149] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12150,7 +12382,7 @@ func (x *ChatIngressFragment) ProtoReflect() protoreflect.Message { // Deprecated: Use ChatIngressFragment.ProtoReflect.Descriptor instead. func (*ChatIngressFragment) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{147} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{149} } func (x *ChatIngressFragment) GetRunId() string { @@ -12225,7 +12457,7 @@ type ChatIngressAck struct { func (x *ChatIngressAck) Reset() { *x = ChatIngressAck{} - mi := &file_proto_v2_gateway_proto_msgTypes[148] + mi := &file_proto_v2_gateway_proto_msgTypes[150] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12237,7 +12469,7 @@ func (x *ChatIngressAck) String() string { func (*ChatIngressAck) ProtoMessage() {} func (x *ChatIngressAck) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[148] + mi := &file_proto_v2_gateway_proto_msgTypes[150] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12250,7 +12482,7 @@ func (x *ChatIngressAck) ProtoReflect() protoreflect.Message { // Deprecated: Use ChatIngressAck.ProtoReflect.Descriptor instead. func (*ChatIngressAck) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{148} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{150} } func (x *ChatIngressAck) GetRunId() string { @@ -12313,7 +12545,7 @@ var File_proto_v2_gateway_proto protoreflect.FileDescriptor const file_proto_v2_gateway_proto_rawDesc = "" + "\n" + - "\x16proto/v2/gateway.proto\x12\x14liveagent.gateway.v2\"\xdf \n" + + "\x16proto/v2/gateway.proto\x12\x14liveagent.gateway.v2\"\xb2!\n" + "\x0fGatewayEnvelope\x12\x1d\n" + "\n" + "request_id\x18\x01 \x01(\tR\trequestId\x12\x1c\n" + @@ -12372,8 +12604,9 @@ const file_proto_v2_gateway_proto_rawDesc = "" + "\x0fworkspace_watch\x18Z \x01(\v2+.liveagent.gateway.v2.WorkspaceWatchRequestH\x00R\x0eworkspaceWatch\x12e\n" + "\x17managed_process_request\x18[ \x01(\v2+.liveagent.gateway.v2.ManagedProcessRequestH\x00R\x15managedProcessRequest\x12S\n" + "\x0ehistory_branch\x18\\ \x01(\v2*.liveagent.gateway.v2.HistoryBranchRequestH\x00R\rhistoryBranch\x12S\n" + - "\x0eprovider_usage\x18] \x01(\v2*.liveagent.gateway.v2.ProviderUsageRequestH\x00R\rproviderUsageB\t\n" + - "\apayloadJ\x04\bC\x10DJ\x04\bD\x10EJ\x04\bE\x10FJ\x04\bJ\x10K\"\xb1,\n" + + "\x0eprovider_usage\x18] \x01(\v2*.liveagent.gateway.v2.ProviderUsageRequestH\x00R\rproviderUsage\x12Q\n" + + "\x0echat_file_open\x18^ \x01(\v2).liveagent.gateway.v2.ChatFileOpenRequestH\x00R\fchatFileOpenB\t\n" + + "\apayloadJ\x04\bC\x10DJ\x04\bD\x10EJ\x04\bE\x10FJ\x04\bJ\x10K\"\x8e-\n" + "\rAgentEnvelope\x12\x1d\n" + "\n" + "request_id\x18\x01 \x01(\tR\trequestId\x12\x1c\n" + @@ -12441,7 +12674,8 @@ const file_proto_v2_gateway_proto_rawDesc = "" + "\x13provider_usage_resp\x18^ \x01(\v2+.liveagent.gateway.v2.ProviderUsageResponseH\x00R\x11providerUsageResp\x12V\n" + "\x12chat_ingress_batch\x18_ \x01(\v2&.liveagent.gateway.v2.ChatIngressBatchH\x00R\x10chatIngressBatch\x12Y\n" + "\x13chat_ingress_resume\x18` \x01(\v2'.liveagent.gateway.v2.ChatIngressResumeH\x00R\x11chatIngressResume\x12_\n" + - "\x15chat_ingress_fragment\x18a \x01(\v2).liveagent.gateway.v2.ChatIngressFragmentH\x00R\x13chatIngressFragment\x12;\n" + + "\x15chat_ingress_fragment\x18a \x01(\v2).liveagent.gateway.v2.ChatIngressFragmentH\x00R\x13chatIngressFragment\x12[\n" + + "\x13chat_file_open_resp\x18b \x01(\v2*.liveagent.gateway.v2.ChatFileOpenResponseH\x00R\x10chatFileOpenResp\x12;\n" + "\x05error\x18c \x01(\v2#.liveagent.gateway.v2.ErrorResponseH\x00R\x05errorB\t\n" + "\apayloadJ\x04\bC\x10DJ\x04\bD\x10EJ\x04\bE\x10FJ\x04\bN\x10O\"|\n" + "\x11ChatSelectedModel\x12,\n" + @@ -13190,7 +13424,31 @@ const file_proto_v2_gateway_proto_rawDesc = "" + "\n" + "size_bytes\x18\x04 \x01(\x04R\tsizeBytes\x12\x19\n" + "\bmtime_ms\x18\x05 \x01(\x04R\amtimeMs\x12!\n" + - "\fcontent_hash\x18\x06 \x01(\tR\vcontentHash\"\xbe\x02\n" + + "\fcontent_hash\x18\x06 \x01(\tR\vcontentHash\"\xac\x02\n" + + "\x13ChatFileOpenRequest\x12'\n" + + "\x0fconversation_id\x18\x01 \x01(\tR\x0econversationId\x12\x18\n" + + "\aworkdir\x18\x02 \x01(\tR\aworkdir\x12\x12\n" + + "\x04path\x18\x03 \x01(\tR\x04path\x12\x16\n" + + "\x06source\x18\x04 \x01(\tR\x06source\x12\x17\n" + + "\x04line\x18\x05 \x01(\rH\x00R\x04line\x88\x01\x01\x12\x1e\n" + + "\bend_line\x18\x06 \x01(\rH\x01R\aendLine\x88\x01\x01\x12\x1b\n" + + "\x06column\x18\a \x01(\rH\x02R\x06column\x88\x01\x01\x12/\n" + + "\x14open_in_file_manager\x18\b \x01(\bR\x11openInFileManagerB\a\n" + + "\x05_lineB\v\n" + + "\t_end_lineB\t\n" + + "\a_column\"\x94\x02\n" + + "\x14ChatFileOpenResponse\x12\x16\n" + + "\x06action\x18\x01 \x01(\tR\x06action\x12\x12\n" + + "\x04kind\x18\x02 \x01(\tR\x04kind\x12\x18\n" + + "\aworkdir\x18\x03 \x01(\tR\aworkdir\x12\x12\n" + + "\x04path\x18\x04 \x01(\tR\x04path\x12\x17\n" + + "\x04line\x18\x05 \x01(\rH\x00R\x04line\x88\x01\x01\x12\x1e\n" + + "\bend_line\x18\x06 \x01(\rH\x01R\aendLine\x88\x01\x01\x12\x1b\n" + + "\x06column\x18\a \x01(\rH\x02R\x06column\x88\x01\x01\x12+\n" + + "\x11outside_workspace\x18\b \x01(\bR\x10outsideWorkspaceB\a\n" + + "\x05_lineB\v\n" + + "\t_end_lineB\t\n" + + "\a_column\"\xbe\x02\n" + "\x12FsWriteTextRequest\x12\x18\n" + "\aworkdir\x18\x01 \x01(\tR\aworkdir\x12\x12\n" + "\x04path\x18\x02 \x01(\tR\x04path\x12\x18\n" + @@ -13367,7 +13625,7 @@ func file_proto_v2_gateway_proto_rawDescGZIP() []byte { } var file_proto_v2_gateway_proto_enumTypes = make([]protoimpl.EnumInfo, 4) -var file_proto_v2_gateway_proto_msgTypes = make([]protoimpl.MessageInfo, 149) +var file_proto_v2_gateway_proto_msgTypes = make([]protoimpl.MessageInfo, 151) var file_proto_v2_gateway_proto_goTypes = []any{ (TunnelFrameKind)(0), // 0: liveagent.gateway.v2.TunnelFrameKind (TunnelWsMessageType)(0), // 1: liveagent.gateway.v2.TunnelWsMessageType @@ -13497,31 +13755,33 @@ var file_proto_v2_gateway_proto_goTypes = []any{ (*FsReadEditableTextResponse)(nil), // 125: liveagent.gateway.v2.FsReadEditableTextResponse (*FsReadWorkspaceImageRequest)(nil), // 126: liveagent.gateway.v2.FsReadWorkspaceImageRequest (*FsReadWorkspaceImageResponse)(nil), // 127: liveagent.gateway.v2.FsReadWorkspaceImageResponse - (*FsWriteTextRequest)(nil), // 128: liveagent.gateway.v2.FsWriteTextRequest - (*FsWriteTextResponse)(nil), // 129: liveagent.gateway.v2.FsWriteTextResponse - (*FsCreateDirRequest)(nil), // 130: liveagent.gateway.v2.FsCreateDirRequest - (*FsCreateDirResponse)(nil), // 131: liveagent.gateway.v2.FsCreateDirResponse - (*FsRenameRequest)(nil), // 132: liveagent.gateway.v2.FsRenameRequest - (*FsRenameResponse)(nil), // 133: liveagent.gateway.v2.FsRenameResponse - (*FsDeleteRequest)(nil), // 134: liveagent.gateway.v2.FsDeleteRequest - (*FsDeleteResponse)(nil), // 135: liveagent.gateway.v2.FsDeleteResponse - (*PingRequest)(nil), // 136: liveagent.gateway.v2.PingRequest - (*PongResponse)(nil), // 137: liveagent.gateway.v2.PongResponse - (*ErrorResponse)(nil), // 138: liveagent.gateway.v2.ErrorResponse - (*ProviderModelsRequest)(nil), // 139: liveagent.gateway.v2.ProviderModelsRequest - (*ProviderModelsResponse)(nil), // 140: liveagent.gateway.v2.ProviderModelsResponse - (*ProviderUsageRequest)(nil), // 141: liveagent.gateway.v2.ProviderUsageRequest - (*ProviderUsageResponse)(nil), // 142: liveagent.gateway.v2.ProviderUsageResponse - (*ChatIngressBatch)(nil), // 143: liveagent.gateway.v2.ChatIngressBatch - (*ChatIngressRecord)(nil), // 144: liveagent.gateway.v2.ChatIngressRecord - (*ChatIngressDelta)(nil), // 145: liveagent.gateway.v2.ChatIngressDelta - (*ChatIngressHeartbeat)(nil), // 146: liveagent.gateway.v2.ChatIngressHeartbeat - (*ChatIngressCheckpoint)(nil), // 147: liveagent.gateway.v2.ChatIngressCheckpoint - (*ChatIngressTerminal)(nil), // 148: liveagent.gateway.v2.ChatIngressTerminal - (*ChatIngressResume)(nil), // 149: liveagent.gateway.v2.ChatIngressResume - (*ChatIngressRunResume)(nil), // 150: liveagent.gateway.v2.ChatIngressRunResume - (*ChatIngressFragment)(nil), // 151: liveagent.gateway.v2.ChatIngressFragment - (*ChatIngressAck)(nil), // 152: liveagent.gateway.v2.ChatIngressAck + (*ChatFileOpenRequest)(nil), // 128: liveagent.gateway.v2.ChatFileOpenRequest + (*ChatFileOpenResponse)(nil), // 129: liveagent.gateway.v2.ChatFileOpenResponse + (*FsWriteTextRequest)(nil), // 130: liveagent.gateway.v2.FsWriteTextRequest + (*FsWriteTextResponse)(nil), // 131: liveagent.gateway.v2.FsWriteTextResponse + (*FsCreateDirRequest)(nil), // 132: liveagent.gateway.v2.FsCreateDirRequest + (*FsCreateDirResponse)(nil), // 133: liveagent.gateway.v2.FsCreateDirResponse + (*FsRenameRequest)(nil), // 134: liveagent.gateway.v2.FsRenameRequest + (*FsRenameResponse)(nil), // 135: liveagent.gateway.v2.FsRenameResponse + (*FsDeleteRequest)(nil), // 136: liveagent.gateway.v2.FsDeleteRequest + (*FsDeleteResponse)(nil), // 137: liveagent.gateway.v2.FsDeleteResponse + (*PingRequest)(nil), // 138: liveagent.gateway.v2.PingRequest + (*PongResponse)(nil), // 139: liveagent.gateway.v2.PongResponse + (*ErrorResponse)(nil), // 140: liveagent.gateway.v2.ErrorResponse + (*ProviderModelsRequest)(nil), // 141: liveagent.gateway.v2.ProviderModelsRequest + (*ProviderModelsResponse)(nil), // 142: liveagent.gateway.v2.ProviderModelsResponse + (*ProviderUsageRequest)(nil), // 143: liveagent.gateway.v2.ProviderUsageRequest + (*ProviderUsageResponse)(nil), // 144: liveagent.gateway.v2.ProviderUsageResponse + (*ChatIngressBatch)(nil), // 145: liveagent.gateway.v2.ChatIngressBatch + (*ChatIngressRecord)(nil), // 146: liveagent.gateway.v2.ChatIngressRecord + (*ChatIngressDelta)(nil), // 147: liveagent.gateway.v2.ChatIngressDelta + (*ChatIngressHeartbeat)(nil), // 148: liveagent.gateway.v2.ChatIngressHeartbeat + (*ChatIngressCheckpoint)(nil), // 149: liveagent.gateway.v2.ChatIngressCheckpoint + (*ChatIngressTerminal)(nil), // 150: liveagent.gateway.v2.ChatIngressTerminal + (*ChatIngressResume)(nil), // 151: liveagent.gateway.v2.ChatIngressResume + (*ChatIngressRunResume)(nil), // 152: liveagent.gateway.v2.ChatIngressRunResume + (*ChatIngressFragment)(nil), // 153: liveagent.gateway.v2.ChatIngressFragment + (*ChatIngressAck)(nil), // 154: liveagent.gateway.v2.ChatIngressAck } var file_proto_v2_gateway_proto_depIdxs = []int32{ 56, // 0: liveagent.gateway.v2.GatewayEnvelope.chat_command:type_name -> liveagent.gateway.v2.ChatCommandRequest @@ -13546,163 +13806,165 @@ var file_proto_v2_gateway_proto_depIdxs = []int32{ 10, // 19: liveagent.gateway.v2.GatewayEnvelope.upload_readable_files:type_name -> liveagent.gateway.v2.UploadReadableFilesRequest 114, // 20: liveagent.gateway.v2.GatewayEnvelope.fs_roots:type_name -> liveagent.gateway.v2.FsRootsRequest 116, // 21: liveagent.gateway.v2.GatewayEnvelope.fs_list_dirs:type_name -> liveagent.gateway.v2.FsListDirsRequest - 136, // 22: liveagent.gateway.v2.GatewayEnvelope.ping:type_name -> liveagent.gateway.v2.PingRequest + 138, // 22: liveagent.gateway.v2.GatewayEnvelope.ping:type_name -> liveagent.gateway.v2.PingRequest 12, // 23: liveagent.gateway.v2.GatewayEnvelope.uploaded_image_preview:type_name -> liveagent.gateway.v2.UploadedImagePreviewRequest 31, // 24: liveagent.gateway.v2.GatewayEnvelope.memory_manage:type_name -> liveagent.gateway.v2.MemoryManageRequest 108, // 25: liveagent.gateway.v2.GatewayEnvelope.skill_manage:type_name -> liveagent.gateway.v2.SkillManageRequest 119, // 26: liveagent.gateway.v2.GatewayEnvelope.fs_create_project_folder:type_name -> liveagent.gateway.v2.FsCreateProjectFolderRequest 33, // 27: liveagent.gateway.v2.GatewayEnvelope.terminal_request:type_name -> liveagent.gateway.v2.TerminalRequest 121, // 28: liveagent.gateway.v2.GatewayEnvelope.fs_list:type_name -> liveagent.gateway.v2.FsListRequest - 128, // 29: liveagent.gateway.v2.GatewayEnvelope.fs_write_text:type_name -> liveagent.gateway.v2.FsWriteTextRequest - 130, // 30: liveagent.gateway.v2.GatewayEnvelope.fs_create_dir:type_name -> liveagent.gateway.v2.FsCreateDirRequest - 132, // 31: liveagent.gateway.v2.GatewayEnvelope.fs_rename:type_name -> liveagent.gateway.v2.FsRenameRequest - 134, // 32: liveagent.gateway.v2.GatewayEnvelope.fs_delete:type_name -> liveagent.gateway.v2.FsDeleteRequest + 130, // 29: liveagent.gateway.v2.GatewayEnvelope.fs_write_text:type_name -> liveagent.gateway.v2.FsWriteTextRequest + 132, // 30: liveagent.gateway.v2.GatewayEnvelope.fs_create_dir:type_name -> liveagent.gateway.v2.FsCreateDirRequest + 134, // 31: liveagent.gateway.v2.GatewayEnvelope.fs_rename:type_name -> liveagent.gateway.v2.FsRenameRequest + 136, // 32: liveagent.gateway.v2.GatewayEnvelope.fs_delete:type_name -> liveagent.gateway.v2.FsDeleteRequest 51, // 33: liveagent.gateway.v2.GatewayEnvelope.git_request:type_name -> liveagent.gateway.v2.GitRequest 124, // 34: liveagent.gateway.v2.GatewayEnvelope.fs_read_editable_text:type_name -> liveagent.gateway.v2.FsReadEditableTextRequest 126, // 35: liveagent.gateway.v2.GatewayEnvelope.fs_read_workspace_image:type_name -> liveagent.gateway.v2.FsReadWorkspaceImageRequest 36, // 36: liveagent.gateway.v2.GatewayEnvelope.sftp_request:type_name -> liveagent.gateway.v2.SftpRequest - 139, // 37: liveagent.gateway.v2.GatewayEnvelope.provider_models:type_name -> liveagent.gateway.v2.ProviderModelsRequest + 141, // 37: liveagent.gateway.v2.GatewayEnvelope.provider_models:type_name -> liveagent.gateway.v2.ProviderModelsRequest 99, // 38: liveagent.gateway.v2.GatewayEnvelope.settings_reset_ssh_known_host:type_name -> liveagent.gateway.v2.SettingsResetSshKnownHostRequest 57, // 39: liveagent.gateway.v2.GatewayEnvelope.chat_queue:type_name -> liveagent.gateway.v2.ChatQueueRequest - 152, // 40: liveagent.gateway.v2.GatewayEnvelope.chat_ingress_ack:type_name -> liveagent.gateway.v2.ChatIngressAck + 154, // 40: liveagent.gateway.v2.GatewayEnvelope.chat_ingress_ack:type_name -> liveagent.gateway.v2.ChatIngressAck 18, // 41: liveagent.gateway.v2.GatewayEnvelope.tunnel_state:type_name -> liveagent.gateway.v2.TunnelStateSnapshot 19, // 42: liveagent.gateway.v2.GatewayEnvelope.tunnel_mutation:type_name -> liveagent.gateway.v2.TunnelMutation 24, // 43: liveagent.gateway.v2.GatewayEnvelope.tunnel_frame:type_name -> liveagent.gateway.v2.TunnelFrame 25, // 44: liveagent.gateway.v2.GatewayEnvelope.workspace_watch:type_name -> liveagent.gateway.v2.WorkspaceWatchRequest 29, // 45: liveagent.gateway.v2.GatewayEnvelope.managed_process_request:type_name -> liveagent.gateway.v2.ManagedProcessRequest 76, // 46: liveagent.gateway.v2.GatewayEnvelope.history_branch:type_name -> liveagent.gateway.v2.HistoryBranchRequest - 141, // 47: liveagent.gateway.v2.GatewayEnvelope.provider_usage:type_name -> liveagent.gateway.v2.ProviderUsageRequest - 60, // 48: liveagent.gateway.v2.AgentEnvelope.chat_event:type_name -> liveagent.gateway.v2.ChatEvent - 66, // 49: liveagent.gateway.v2.AgentEnvelope.cron_manage_resp:type_name -> liveagent.gateway.v2.CronManageResponse - 68, // 50: liveagent.gateway.v2.AgentEnvelope.history_list_resp:type_name -> liveagent.gateway.v2.HistoryListResponse - 71, // 51: liveagent.gateway.v2.AgentEnvelope.history_get_resp:type_name -> liveagent.gateway.v2.HistoryGetResponse - 75, // 52: liveagent.gateway.v2.AgentEnvelope.history_rename_resp:type_name -> liveagent.gateway.v2.HistoryRenameResponse - 91, // 53: liveagent.gateway.v2.AgentEnvelope.history_delete_resp:type_name -> liveagent.gateway.v2.HistoryDeleteResponse - 92, // 54: liveagent.gateway.v2.AgentEnvelope.history_sync:type_name -> liveagent.gateway.v2.HistorySyncEvent - 73, // 55: liveagent.gateway.v2.AgentEnvelope.history_prefix_resp:type_name -> liveagent.gateway.v2.HistoryPrefixResponse - 79, // 56: liveagent.gateway.v2.AgentEnvelope.history_pin_resp:type_name -> liveagent.gateway.v2.HistoryPinResponse - 82, // 57: liveagent.gateway.v2.AgentEnvelope.history_share_get_resp:type_name -> liveagent.gateway.v2.HistoryShareGetResponse - 84, // 58: liveagent.gateway.v2.AgentEnvelope.history_share_set_resp:type_name -> liveagent.gateway.v2.HistoryShareSetResponse - 86, // 59: liveagent.gateway.v2.AgentEnvelope.history_share_resolve_resp:type_name -> liveagent.gateway.v2.HistoryShareResolveResponse - 89, // 60: liveagent.gateway.v2.AgentEnvelope.history_workdirs_resp:type_name -> liveagent.gateway.v2.HistoryWorkdirsResponse - 94, // 61: liveagent.gateway.v2.AgentEnvelope.provider_list_resp:type_name -> liveagent.gateway.v2.ProviderListResponse - 96, // 62: liveagent.gateway.v2.AgentEnvelope.settings_get_resp:type_name -> liveagent.gateway.v2.SettingsGetResponse - 98, // 63: liveagent.gateway.v2.AgentEnvelope.settings_update_resp:type_name -> liveagent.gateway.v2.SettingsUpdateResponse - 101, // 64: liveagent.gateway.v2.AgentEnvelope.settings_sync:type_name -> liveagent.gateway.v2.SettingsSyncEvent - 103, // 65: liveagent.gateway.v2.AgentEnvelope.skill_files_list_resp:type_name -> liveagent.gateway.v2.SkillFilesListResponse - 105, // 66: liveagent.gateway.v2.AgentEnvelope.skill_metadata_read_resp:type_name -> liveagent.gateway.v2.SkillMetadataReadResponse - 107, // 67: liveagent.gateway.v2.AgentEnvelope.skill_text_read_resp:type_name -> liveagent.gateway.v2.SkillTextReadResponse - 112, // 68: liveagent.gateway.v2.AgentEnvelope.file_mention_list_resp:type_name -> liveagent.gateway.v2.FileMentionListResponse - 11, // 69: liveagent.gateway.v2.AgentEnvelope.upload_readable_files_resp:type_name -> liveagent.gateway.v2.UploadReadableFilesResponse - 115, // 70: liveagent.gateway.v2.AgentEnvelope.fs_roots_resp:type_name -> liveagent.gateway.v2.FsRootsResponse - 137, // 71: liveagent.gateway.v2.AgentEnvelope.pong:type_name -> liveagent.gateway.v2.PongResponse - 118, // 72: liveagent.gateway.v2.AgentEnvelope.fs_list_dirs_resp:type_name -> liveagent.gateway.v2.FsListDirsResponse - 13, // 73: liveagent.gateway.v2.AgentEnvelope.uploaded_image_preview_resp:type_name -> liveagent.gateway.v2.UploadedImagePreviewResponse - 32, // 74: liveagent.gateway.v2.AgentEnvelope.memory_manage_resp:type_name -> liveagent.gateway.v2.MemoryManageResponse - 109, // 75: liveagent.gateway.v2.AgentEnvelope.skill_manage_resp:type_name -> liveagent.gateway.v2.SkillManageResponse - 120, // 76: liveagent.gateway.v2.AgentEnvelope.fs_create_project_folder_resp:type_name -> liveagent.gateway.v2.FsCreateProjectFolderResponse - 48, // 77: liveagent.gateway.v2.AgentEnvelope.terminal_response:type_name -> liveagent.gateway.v2.TerminalResponse - 49, // 78: liveagent.gateway.v2.AgentEnvelope.terminal_event:type_name -> liveagent.gateway.v2.TerminalEvent - 123, // 79: liveagent.gateway.v2.AgentEnvelope.fs_list_resp:type_name -> liveagent.gateway.v2.FsListResponse - 129, // 80: liveagent.gateway.v2.AgentEnvelope.fs_write_text_resp:type_name -> liveagent.gateway.v2.FsWriteTextResponse - 131, // 81: liveagent.gateway.v2.AgentEnvelope.fs_create_dir_resp:type_name -> liveagent.gateway.v2.FsCreateDirResponse - 133, // 82: liveagent.gateway.v2.AgentEnvelope.fs_rename_resp:type_name -> liveagent.gateway.v2.FsRenameResponse - 135, // 83: liveagent.gateway.v2.AgentEnvelope.fs_delete_resp:type_name -> liveagent.gateway.v2.FsDeleteResponse - 52, // 84: liveagent.gateway.v2.AgentEnvelope.git_response:type_name -> liveagent.gateway.v2.GitResponse - 125, // 85: liveagent.gateway.v2.AgentEnvelope.fs_read_editable_text_resp:type_name -> liveagent.gateway.v2.FsReadEditableTextResponse - 127, // 86: liveagent.gateway.v2.AgentEnvelope.fs_read_workspace_image_resp:type_name -> liveagent.gateway.v2.FsReadWorkspaceImageResponse - 39, // 87: liveagent.gateway.v2.AgentEnvelope.sftp_response:type_name -> liveagent.gateway.v2.SftpResponse - 40, // 88: liveagent.gateway.v2.AgentEnvelope.sftp_event:type_name -> liveagent.gateway.v2.SftpEvent - 58, // 89: liveagent.gateway.v2.AgentEnvelope.chat_queue_resp:type_name -> liveagent.gateway.v2.ChatQueueResponse - 59, // 90: liveagent.gateway.v2.AgentEnvelope.chat_queue_event:type_name -> liveagent.gateway.v2.ChatQueueEvent - 61, // 91: liveagent.gateway.v2.AgentEnvelope.chat_control:type_name -> liveagent.gateway.v2.ChatControlEvent - 63, // 92: liveagent.gateway.v2.AgentEnvelope.runtime_status:type_name -> liveagent.gateway.v2.RuntimeStatusEvent - 100, // 93: liveagent.gateway.v2.AgentEnvelope.settings_reset_ssh_known_host_resp:type_name -> liveagent.gateway.v2.SettingsResetSshKnownHostResponse - 62, // 94: liveagent.gateway.v2.AgentEnvelope.chat_runtime_snapshot:type_name -> liveagent.gateway.v2.ChatRuntimeSnapshot - 140, // 95: liveagent.gateway.v2.AgentEnvelope.provider_models_resp:type_name -> liveagent.gateway.v2.ProviderModelsResponse - 15, // 96: liveagent.gateway.v2.AgentEnvelope.tunnel_desired:type_name -> liveagent.gateway.v2.TunnelDesiredState - 20, // 97: liveagent.gateway.v2.AgentEnvelope.tunnel_mutation_result:type_name -> liveagent.gateway.v2.TunnelMutationResult - 24, // 98: liveagent.gateway.v2.AgentEnvelope.tunnel_frame:type_name -> liveagent.gateway.v2.TunnelFrame - 22, // 99: liveagent.gateway.v2.AgentEnvelope.tunnel_probe_report:type_name -> liveagent.gateway.v2.TunnelProbeReport - 26, // 100: liveagent.gateway.v2.AgentEnvelope.workspace_activity:type_name -> liveagent.gateway.v2.WorkspaceActivityEvent - 30, // 101: liveagent.gateway.v2.AgentEnvelope.managed_process_response:type_name -> liveagent.gateway.v2.ManagedProcessResponse - 28, // 102: liveagent.gateway.v2.AgentEnvelope.managed_process_snapshot:type_name -> liveagent.gateway.v2.ManagedProcessSnapshot - 77, // 103: liveagent.gateway.v2.AgentEnvelope.history_branch_resp:type_name -> liveagent.gateway.v2.HistoryBranchResponse - 142, // 104: liveagent.gateway.v2.AgentEnvelope.provider_usage_resp:type_name -> liveagent.gateway.v2.ProviderUsageResponse - 143, // 105: liveagent.gateway.v2.AgentEnvelope.chat_ingress_batch:type_name -> liveagent.gateway.v2.ChatIngressBatch - 149, // 106: liveagent.gateway.v2.AgentEnvelope.chat_ingress_resume:type_name -> liveagent.gateway.v2.ChatIngressResume - 151, // 107: liveagent.gateway.v2.AgentEnvelope.chat_ingress_fragment:type_name -> liveagent.gateway.v2.ChatIngressFragment - 138, // 108: liveagent.gateway.v2.AgentEnvelope.error:type_name -> liveagent.gateway.v2.ErrorResponse - 9, // 109: liveagent.gateway.v2.UploadReadableFilesRequest.files:type_name -> liveagent.gateway.v2.UploadReadableFile - 8, // 110: liveagent.gateway.v2.UploadReadableFilesResponse.files:type_name -> liveagent.gateway.v2.ChatUploadedFile - 14, // 111: liveagent.gateway.v2.TunnelDesiredState.tunnels:type_name -> liveagent.gateway.v2.TunnelSpec - 16, // 112: liveagent.gateway.v2.TunnelStatus.local:type_name -> liveagent.gateway.v2.TunnelHealth - 17, // 113: liveagent.gateway.v2.TunnelStateSnapshot.tunnels:type_name -> liveagent.gateway.v2.TunnelStatus - 16, // 114: liveagent.gateway.v2.TunnelStateSnapshot.relay:type_name -> liveagent.gateway.v2.TunnelHealth - 16, // 115: liveagent.gateway.v2.TunnelProbeResult.local:type_name -> liveagent.gateway.v2.TunnelHealth - 21, // 116: liveagent.gateway.v2.TunnelProbeReport.results:type_name -> liveagent.gateway.v2.TunnelProbeResult - 0, // 117: liveagent.gateway.v2.TunnelFrame.kind:type_name -> liveagent.gateway.v2.TunnelFrameKind - 23, // 118: liveagent.gateway.v2.TunnelFrame.headers:type_name -> liveagent.gateway.v2.TunnelHeader - 1, // 119: liveagent.gateway.v2.TunnelFrame.ws_message_type:type_name -> liveagent.gateway.v2.TunnelWsMessageType - 27, // 120: liveagent.gateway.v2.ManagedProcessSnapshot.processes:type_name -> liveagent.gateway.v2.ManagedProcessRecord - 28, // 121: liveagent.gateway.v2.ManagedProcessResponse.snapshot:type_name -> liveagent.gateway.v2.ManagedProcessSnapshot - 35, // 122: liveagent.gateway.v2.TerminalSession.ssh:type_name -> liveagent.gateway.v2.TerminalSshMetadata - 37, // 123: liveagent.gateway.v2.SftpResponse.entries:type_name -> liveagent.gateway.v2.SftpEntry - 37, // 124: liveagent.gateway.v2.SftpResponse.entry:type_name -> liveagent.gateway.v2.SftpEntry - 38, // 125: liveagent.gateway.v2.SftpResponse.transfer:type_name -> liveagent.gateway.v2.SftpTransfer - 38, // 126: liveagent.gateway.v2.SftpEvent.transfer:type_name -> liveagent.gateway.v2.SftpTransfer - 43, // 127: liveagent.gateway.v2.TerminalSshTabsSnapshot.tabs:type_name -> liveagent.gateway.v2.TerminalSshTab - 45, // 128: liveagent.gateway.v2.TerminalSshLocalForwardsSnapshot.forwards:type_name -> liveagent.gateway.v2.TerminalSshLocalForward - 45, // 129: liveagent.gateway.v2.TerminalSshLocalForwardAction.forward:type_name -> liveagent.gateway.v2.TerminalSshLocalForward - 34, // 130: liveagent.gateway.v2.TerminalResponse.sessions:type_name -> liveagent.gateway.v2.TerminalSession - 34, // 131: liveagent.gateway.v2.TerminalResponse.session:type_name -> liveagent.gateway.v2.TerminalSession - 42, // 132: liveagent.gateway.v2.TerminalResponse.shell_options:type_name -> liveagent.gateway.v2.TerminalShellOption - 41, // 133: liveagent.gateway.v2.TerminalResponse.ssh_prompt:type_name -> liveagent.gateway.v2.TerminalSshPrompt - 44, // 134: liveagent.gateway.v2.TerminalResponse.ssh_tabs:type_name -> liveagent.gateway.v2.TerminalSshTabsSnapshot - 46, // 135: liveagent.gateway.v2.TerminalResponse.ssh_local_forwards:type_name -> liveagent.gateway.v2.TerminalSshLocalForwardsSnapshot - 47, // 136: liveagent.gateway.v2.TerminalResponse.ssh_local_forward:type_name -> liveagent.gateway.v2.TerminalSshLocalForwardAction - 34, // 137: liveagent.gateway.v2.TerminalEvent.session:type_name -> liveagent.gateway.v2.TerminalSession - 44, // 138: liveagent.gateway.v2.TerminalEvent.ssh_tabs:type_name -> liveagent.gateway.v2.TerminalSshTabsSnapshot - 47, // 139: liveagent.gateway.v2.TerminalEvent.ssh_local_forward:type_name -> liveagent.gateway.v2.TerminalSshLocalForwardAction - 34, // 140: liveagent.gateway.v2.TerminalStreamFrame.session:type_name -> liveagent.gateway.v2.TerminalSession - 6, // 141: liveagent.gateway.v2.ChatRequest.selected_model:type_name -> liveagent.gateway.v2.ChatSelectedModel - 8, // 142: liveagent.gateway.v2.ChatRequest.uploaded_files:type_name -> liveagent.gateway.v2.ChatUploadedFile - 7, // 143: liveagent.gateway.v2.ChatRequest.runtime_controls:type_name -> liveagent.gateway.v2.ChatRuntimeControls - 53, // 144: liveagent.gateway.v2.ChatCommandRequest.request:type_name -> liveagent.gateway.v2.ChatRequest - 54, // 145: liveagent.gateway.v2.ChatCommandRequest.base_message_ref:type_name -> liveagent.gateway.v2.ChatMessageRef - 55, // 146: liveagent.gateway.v2.ChatCommandRequest.cancel:type_name -> liveagent.gateway.v2.CancelChatRequest - 2, // 147: liveagent.gateway.v2.ChatEvent.type:type_name -> liveagent.gateway.v2.ChatEvent.ChatEventType - 64, // 148: liveagent.gateway.v2.RuntimeStatusEvent.active_runs:type_name -> liveagent.gateway.v2.ChatRunReport - 64, // 149: liveagent.gateway.v2.RuntimeStatusEvent.finished_runs:type_name -> liveagent.gateway.v2.ChatRunReport - 69, // 150: liveagent.gateway.v2.HistoryListResponse.conversations:type_name -> liveagent.gateway.v2.ConversationSummary - 69, // 151: liveagent.gateway.v2.HistoryGetResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary - 54, // 152: liveagent.gateway.v2.HistoryPrefixRequest.base_message_ref:type_name -> liveagent.gateway.v2.ChatMessageRef - 69, // 153: liveagent.gateway.v2.HistoryPrefixResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary - 69, // 154: liveagent.gateway.v2.HistoryRenameResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary - 54, // 155: liveagent.gateway.v2.HistoryBranchRequest.base_message_ref:type_name -> liveagent.gateway.v2.ChatMessageRef - 69, // 156: liveagent.gateway.v2.HistoryBranchResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary - 69, // 157: liveagent.gateway.v2.HistoryPinResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary - 80, // 158: liveagent.gateway.v2.HistoryShareGetResponse.share:type_name -> liveagent.gateway.v2.HistoryShareStatus - 80, // 159: liveagent.gateway.v2.HistoryShareSetResponse.share:type_name -> liveagent.gateway.v2.HistoryShareStatus - 69, // 160: liveagent.gateway.v2.HistoryShareResolveResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary - 88, // 161: liveagent.gateway.v2.HistoryWorkdirsResponse.workdirs:type_name -> liveagent.gateway.v2.HistoryWorkdirSummary - 69, // 162: liveagent.gateway.v2.HistorySyncEvent.conversation:type_name -> liveagent.gateway.v2.ConversationSummary - 111, // 163: liveagent.gateway.v2.FileMentionListResponse.entries:type_name -> liveagent.gateway.v2.FileMentionEntry - 113, // 164: liveagent.gateway.v2.FsRootsResponse.roots:type_name -> liveagent.gateway.v2.FsRoot - 117, // 165: liveagent.gateway.v2.FsListDirsResponse.entries:type_name -> liveagent.gateway.v2.FsDirEntry - 122, // 166: liveagent.gateway.v2.FsListResponse.entries:type_name -> liveagent.gateway.v2.FsListEntry - 144, // 167: liveagent.gateway.v2.ChatIngressBatch.records:type_name -> liveagent.gateway.v2.ChatIngressRecord - 145, // 168: liveagent.gateway.v2.ChatIngressRecord.delta:type_name -> liveagent.gateway.v2.ChatIngressDelta - 147, // 169: liveagent.gateway.v2.ChatIngressRecord.checkpoint:type_name -> liveagent.gateway.v2.ChatIngressCheckpoint - 148, // 170: liveagent.gateway.v2.ChatIngressRecord.terminal:type_name -> liveagent.gateway.v2.ChatIngressTerminal - 146, // 171: liveagent.gateway.v2.ChatIngressRecord.heartbeat:type_name -> liveagent.gateway.v2.ChatIngressHeartbeat - 150, // 172: liveagent.gateway.v2.ChatIngressResume.runs:type_name -> liveagent.gateway.v2.ChatIngressRunResume - 3, // 173: liveagent.gateway.v2.ChatIngressAck.action:type_name -> liveagent.gateway.v2.ChatIngressAck.Action - 174, // [174:174] is the sub-list for method output_type - 174, // [174:174] is the sub-list for method input_type - 174, // [174:174] is the sub-list for extension type_name - 174, // [174:174] is the sub-list for extension extendee - 0, // [0:174] is the sub-list for field type_name + 143, // 47: liveagent.gateway.v2.GatewayEnvelope.provider_usage:type_name -> liveagent.gateway.v2.ProviderUsageRequest + 128, // 48: liveagent.gateway.v2.GatewayEnvelope.chat_file_open:type_name -> liveagent.gateway.v2.ChatFileOpenRequest + 60, // 49: liveagent.gateway.v2.AgentEnvelope.chat_event:type_name -> liveagent.gateway.v2.ChatEvent + 66, // 50: liveagent.gateway.v2.AgentEnvelope.cron_manage_resp:type_name -> liveagent.gateway.v2.CronManageResponse + 68, // 51: liveagent.gateway.v2.AgentEnvelope.history_list_resp:type_name -> liveagent.gateway.v2.HistoryListResponse + 71, // 52: liveagent.gateway.v2.AgentEnvelope.history_get_resp:type_name -> liveagent.gateway.v2.HistoryGetResponse + 75, // 53: liveagent.gateway.v2.AgentEnvelope.history_rename_resp:type_name -> liveagent.gateway.v2.HistoryRenameResponse + 91, // 54: liveagent.gateway.v2.AgentEnvelope.history_delete_resp:type_name -> liveagent.gateway.v2.HistoryDeleteResponse + 92, // 55: liveagent.gateway.v2.AgentEnvelope.history_sync:type_name -> liveagent.gateway.v2.HistorySyncEvent + 73, // 56: liveagent.gateway.v2.AgentEnvelope.history_prefix_resp:type_name -> liveagent.gateway.v2.HistoryPrefixResponse + 79, // 57: liveagent.gateway.v2.AgentEnvelope.history_pin_resp:type_name -> liveagent.gateway.v2.HistoryPinResponse + 82, // 58: liveagent.gateway.v2.AgentEnvelope.history_share_get_resp:type_name -> liveagent.gateway.v2.HistoryShareGetResponse + 84, // 59: liveagent.gateway.v2.AgentEnvelope.history_share_set_resp:type_name -> liveagent.gateway.v2.HistoryShareSetResponse + 86, // 60: liveagent.gateway.v2.AgentEnvelope.history_share_resolve_resp:type_name -> liveagent.gateway.v2.HistoryShareResolveResponse + 89, // 61: liveagent.gateway.v2.AgentEnvelope.history_workdirs_resp:type_name -> liveagent.gateway.v2.HistoryWorkdirsResponse + 94, // 62: liveagent.gateway.v2.AgentEnvelope.provider_list_resp:type_name -> liveagent.gateway.v2.ProviderListResponse + 96, // 63: liveagent.gateway.v2.AgentEnvelope.settings_get_resp:type_name -> liveagent.gateway.v2.SettingsGetResponse + 98, // 64: liveagent.gateway.v2.AgentEnvelope.settings_update_resp:type_name -> liveagent.gateway.v2.SettingsUpdateResponse + 101, // 65: liveagent.gateway.v2.AgentEnvelope.settings_sync:type_name -> liveagent.gateway.v2.SettingsSyncEvent + 103, // 66: liveagent.gateway.v2.AgentEnvelope.skill_files_list_resp:type_name -> liveagent.gateway.v2.SkillFilesListResponse + 105, // 67: liveagent.gateway.v2.AgentEnvelope.skill_metadata_read_resp:type_name -> liveagent.gateway.v2.SkillMetadataReadResponse + 107, // 68: liveagent.gateway.v2.AgentEnvelope.skill_text_read_resp:type_name -> liveagent.gateway.v2.SkillTextReadResponse + 112, // 69: liveagent.gateway.v2.AgentEnvelope.file_mention_list_resp:type_name -> liveagent.gateway.v2.FileMentionListResponse + 11, // 70: liveagent.gateway.v2.AgentEnvelope.upload_readable_files_resp:type_name -> liveagent.gateway.v2.UploadReadableFilesResponse + 115, // 71: liveagent.gateway.v2.AgentEnvelope.fs_roots_resp:type_name -> liveagent.gateway.v2.FsRootsResponse + 139, // 72: liveagent.gateway.v2.AgentEnvelope.pong:type_name -> liveagent.gateway.v2.PongResponse + 118, // 73: liveagent.gateway.v2.AgentEnvelope.fs_list_dirs_resp:type_name -> liveagent.gateway.v2.FsListDirsResponse + 13, // 74: liveagent.gateway.v2.AgentEnvelope.uploaded_image_preview_resp:type_name -> liveagent.gateway.v2.UploadedImagePreviewResponse + 32, // 75: liveagent.gateway.v2.AgentEnvelope.memory_manage_resp:type_name -> liveagent.gateway.v2.MemoryManageResponse + 109, // 76: liveagent.gateway.v2.AgentEnvelope.skill_manage_resp:type_name -> liveagent.gateway.v2.SkillManageResponse + 120, // 77: liveagent.gateway.v2.AgentEnvelope.fs_create_project_folder_resp:type_name -> liveagent.gateway.v2.FsCreateProjectFolderResponse + 48, // 78: liveagent.gateway.v2.AgentEnvelope.terminal_response:type_name -> liveagent.gateway.v2.TerminalResponse + 49, // 79: liveagent.gateway.v2.AgentEnvelope.terminal_event:type_name -> liveagent.gateway.v2.TerminalEvent + 123, // 80: liveagent.gateway.v2.AgentEnvelope.fs_list_resp:type_name -> liveagent.gateway.v2.FsListResponse + 131, // 81: liveagent.gateway.v2.AgentEnvelope.fs_write_text_resp:type_name -> liveagent.gateway.v2.FsWriteTextResponse + 133, // 82: liveagent.gateway.v2.AgentEnvelope.fs_create_dir_resp:type_name -> liveagent.gateway.v2.FsCreateDirResponse + 135, // 83: liveagent.gateway.v2.AgentEnvelope.fs_rename_resp:type_name -> liveagent.gateway.v2.FsRenameResponse + 137, // 84: liveagent.gateway.v2.AgentEnvelope.fs_delete_resp:type_name -> liveagent.gateway.v2.FsDeleteResponse + 52, // 85: liveagent.gateway.v2.AgentEnvelope.git_response:type_name -> liveagent.gateway.v2.GitResponse + 125, // 86: liveagent.gateway.v2.AgentEnvelope.fs_read_editable_text_resp:type_name -> liveagent.gateway.v2.FsReadEditableTextResponse + 127, // 87: liveagent.gateway.v2.AgentEnvelope.fs_read_workspace_image_resp:type_name -> liveagent.gateway.v2.FsReadWorkspaceImageResponse + 39, // 88: liveagent.gateway.v2.AgentEnvelope.sftp_response:type_name -> liveagent.gateway.v2.SftpResponse + 40, // 89: liveagent.gateway.v2.AgentEnvelope.sftp_event:type_name -> liveagent.gateway.v2.SftpEvent + 58, // 90: liveagent.gateway.v2.AgentEnvelope.chat_queue_resp:type_name -> liveagent.gateway.v2.ChatQueueResponse + 59, // 91: liveagent.gateway.v2.AgentEnvelope.chat_queue_event:type_name -> liveagent.gateway.v2.ChatQueueEvent + 61, // 92: liveagent.gateway.v2.AgentEnvelope.chat_control:type_name -> liveagent.gateway.v2.ChatControlEvent + 63, // 93: liveagent.gateway.v2.AgentEnvelope.runtime_status:type_name -> liveagent.gateway.v2.RuntimeStatusEvent + 100, // 94: liveagent.gateway.v2.AgentEnvelope.settings_reset_ssh_known_host_resp:type_name -> liveagent.gateway.v2.SettingsResetSshKnownHostResponse + 62, // 95: liveagent.gateway.v2.AgentEnvelope.chat_runtime_snapshot:type_name -> liveagent.gateway.v2.ChatRuntimeSnapshot + 142, // 96: liveagent.gateway.v2.AgentEnvelope.provider_models_resp:type_name -> liveagent.gateway.v2.ProviderModelsResponse + 15, // 97: liveagent.gateway.v2.AgentEnvelope.tunnel_desired:type_name -> liveagent.gateway.v2.TunnelDesiredState + 20, // 98: liveagent.gateway.v2.AgentEnvelope.tunnel_mutation_result:type_name -> liveagent.gateway.v2.TunnelMutationResult + 24, // 99: liveagent.gateway.v2.AgentEnvelope.tunnel_frame:type_name -> liveagent.gateway.v2.TunnelFrame + 22, // 100: liveagent.gateway.v2.AgentEnvelope.tunnel_probe_report:type_name -> liveagent.gateway.v2.TunnelProbeReport + 26, // 101: liveagent.gateway.v2.AgentEnvelope.workspace_activity:type_name -> liveagent.gateway.v2.WorkspaceActivityEvent + 30, // 102: liveagent.gateway.v2.AgentEnvelope.managed_process_response:type_name -> liveagent.gateway.v2.ManagedProcessResponse + 28, // 103: liveagent.gateway.v2.AgentEnvelope.managed_process_snapshot:type_name -> liveagent.gateway.v2.ManagedProcessSnapshot + 77, // 104: liveagent.gateway.v2.AgentEnvelope.history_branch_resp:type_name -> liveagent.gateway.v2.HistoryBranchResponse + 144, // 105: liveagent.gateway.v2.AgentEnvelope.provider_usage_resp:type_name -> liveagent.gateway.v2.ProviderUsageResponse + 145, // 106: liveagent.gateway.v2.AgentEnvelope.chat_ingress_batch:type_name -> liveagent.gateway.v2.ChatIngressBatch + 151, // 107: liveagent.gateway.v2.AgentEnvelope.chat_ingress_resume:type_name -> liveagent.gateway.v2.ChatIngressResume + 153, // 108: liveagent.gateway.v2.AgentEnvelope.chat_ingress_fragment:type_name -> liveagent.gateway.v2.ChatIngressFragment + 129, // 109: liveagent.gateway.v2.AgentEnvelope.chat_file_open_resp:type_name -> liveagent.gateway.v2.ChatFileOpenResponse + 140, // 110: liveagent.gateway.v2.AgentEnvelope.error:type_name -> liveagent.gateway.v2.ErrorResponse + 9, // 111: liveagent.gateway.v2.UploadReadableFilesRequest.files:type_name -> liveagent.gateway.v2.UploadReadableFile + 8, // 112: liveagent.gateway.v2.UploadReadableFilesResponse.files:type_name -> liveagent.gateway.v2.ChatUploadedFile + 14, // 113: liveagent.gateway.v2.TunnelDesiredState.tunnels:type_name -> liveagent.gateway.v2.TunnelSpec + 16, // 114: liveagent.gateway.v2.TunnelStatus.local:type_name -> liveagent.gateway.v2.TunnelHealth + 17, // 115: liveagent.gateway.v2.TunnelStateSnapshot.tunnels:type_name -> liveagent.gateway.v2.TunnelStatus + 16, // 116: liveagent.gateway.v2.TunnelStateSnapshot.relay:type_name -> liveagent.gateway.v2.TunnelHealth + 16, // 117: liveagent.gateway.v2.TunnelProbeResult.local:type_name -> liveagent.gateway.v2.TunnelHealth + 21, // 118: liveagent.gateway.v2.TunnelProbeReport.results:type_name -> liveagent.gateway.v2.TunnelProbeResult + 0, // 119: liveagent.gateway.v2.TunnelFrame.kind:type_name -> liveagent.gateway.v2.TunnelFrameKind + 23, // 120: liveagent.gateway.v2.TunnelFrame.headers:type_name -> liveagent.gateway.v2.TunnelHeader + 1, // 121: liveagent.gateway.v2.TunnelFrame.ws_message_type:type_name -> liveagent.gateway.v2.TunnelWsMessageType + 27, // 122: liveagent.gateway.v2.ManagedProcessSnapshot.processes:type_name -> liveagent.gateway.v2.ManagedProcessRecord + 28, // 123: liveagent.gateway.v2.ManagedProcessResponse.snapshot:type_name -> liveagent.gateway.v2.ManagedProcessSnapshot + 35, // 124: liveagent.gateway.v2.TerminalSession.ssh:type_name -> liveagent.gateway.v2.TerminalSshMetadata + 37, // 125: liveagent.gateway.v2.SftpResponse.entries:type_name -> liveagent.gateway.v2.SftpEntry + 37, // 126: liveagent.gateway.v2.SftpResponse.entry:type_name -> liveagent.gateway.v2.SftpEntry + 38, // 127: liveagent.gateway.v2.SftpResponse.transfer:type_name -> liveagent.gateway.v2.SftpTransfer + 38, // 128: liveagent.gateway.v2.SftpEvent.transfer:type_name -> liveagent.gateway.v2.SftpTransfer + 43, // 129: liveagent.gateway.v2.TerminalSshTabsSnapshot.tabs:type_name -> liveagent.gateway.v2.TerminalSshTab + 45, // 130: liveagent.gateway.v2.TerminalSshLocalForwardsSnapshot.forwards:type_name -> liveagent.gateway.v2.TerminalSshLocalForward + 45, // 131: liveagent.gateway.v2.TerminalSshLocalForwardAction.forward:type_name -> liveagent.gateway.v2.TerminalSshLocalForward + 34, // 132: liveagent.gateway.v2.TerminalResponse.sessions:type_name -> liveagent.gateway.v2.TerminalSession + 34, // 133: liveagent.gateway.v2.TerminalResponse.session:type_name -> liveagent.gateway.v2.TerminalSession + 42, // 134: liveagent.gateway.v2.TerminalResponse.shell_options:type_name -> liveagent.gateway.v2.TerminalShellOption + 41, // 135: liveagent.gateway.v2.TerminalResponse.ssh_prompt:type_name -> liveagent.gateway.v2.TerminalSshPrompt + 44, // 136: liveagent.gateway.v2.TerminalResponse.ssh_tabs:type_name -> liveagent.gateway.v2.TerminalSshTabsSnapshot + 46, // 137: liveagent.gateway.v2.TerminalResponse.ssh_local_forwards:type_name -> liveagent.gateway.v2.TerminalSshLocalForwardsSnapshot + 47, // 138: liveagent.gateway.v2.TerminalResponse.ssh_local_forward:type_name -> liveagent.gateway.v2.TerminalSshLocalForwardAction + 34, // 139: liveagent.gateway.v2.TerminalEvent.session:type_name -> liveagent.gateway.v2.TerminalSession + 44, // 140: liveagent.gateway.v2.TerminalEvent.ssh_tabs:type_name -> liveagent.gateway.v2.TerminalSshTabsSnapshot + 47, // 141: liveagent.gateway.v2.TerminalEvent.ssh_local_forward:type_name -> liveagent.gateway.v2.TerminalSshLocalForwardAction + 34, // 142: liveagent.gateway.v2.TerminalStreamFrame.session:type_name -> liveagent.gateway.v2.TerminalSession + 6, // 143: liveagent.gateway.v2.ChatRequest.selected_model:type_name -> liveagent.gateway.v2.ChatSelectedModel + 8, // 144: liveagent.gateway.v2.ChatRequest.uploaded_files:type_name -> liveagent.gateway.v2.ChatUploadedFile + 7, // 145: liveagent.gateway.v2.ChatRequest.runtime_controls:type_name -> liveagent.gateway.v2.ChatRuntimeControls + 53, // 146: liveagent.gateway.v2.ChatCommandRequest.request:type_name -> liveagent.gateway.v2.ChatRequest + 54, // 147: liveagent.gateway.v2.ChatCommandRequest.base_message_ref:type_name -> liveagent.gateway.v2.ChatMessageRef + 55, // 148: liveagent.gateway.v2.ChatCommandRequest.cancel:type_name -> liveagent.gateway.v2.CancelChatRequest + 2, // 149: liveagent.gateway.v2.ChatEvent.type:type_name -> liveagent.gateway.v2.ChatEvent.ChatEventType + 64, // 150: liveagent.gateway.v2.RuntimeStatusEvent.active_runs:type_name -> liveagent.gateway.v2.ChatRunReport + 64, // 151: liveagent.gateway.v2.RuntimeStatusEvent.finished_runs:type_name -> liveagent.gateway.v2.ChatRunReport + 69, // 152: liveagent.gateway.v2.HistoryListResponse.conversations:type_name -> liveagent.gateway.v2.ConversationSummary + 69, // 153: liveagent.gateway.v2.HistoryGetResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary + 54, // 154: liveagent.gateway.v2.HistoryPrefixRequest.base_message_ref:type_name -> liveagent.gateway.v2.ChatMessageRef + 69, // 155: liveagent.gateway.v2.HistoryPrefixResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary + 69, // 156: liveagent.gateway.v2.HistoryRenameResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary + 54, // 157: liveagent.gateway.v2.HistoryBranchRequest.base_message_ref:type_name -> liveagent.gateway.v2.ChatMessageRef + 69, // 158: liveagent.gateway.v2.HistoryBranchResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary + 69, // 159: liveagent.gateway.v2.HistoryPinResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary + 80, // 160: liveagent.gateway.v2.HistoryShareGetResponse.share:type_name -> liveagent.gateway.v2.HistoryShareStatus + 80, // 161: liveagent.gateway.v2.HistoryShareSetResponse.share:type_name -> liveagent.gateway.v2.HistoryShareStatus + 69, // 162: liveagent.gateway.v2.HistoryShareResolveResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary + 88, // 163: liveagent.gateway.v2.HistoryWorkdirsResponse.workdirs:type_name -> liveagent.gateway.v2.HistoryWorkdirSummary + 69, // 164: liveagent.gateway.v2.HistorySyncEvent.conversation:type_name -> liveagent.gateway.v2.ConversationSummary + 111, // 165: liveagent.gateway.v2.FileMentionListResponse.entries:type_name -> liveagent.gateway.v2.FileMentionEntry + 113, // 166: liveagent.gateway.v2.FsRootsResponse.roots:type_name -> liveagent.gateway.v2.FsRoot + 117, // 167: liveagent.gateway.v2.FsListDirsResponse.entries:type_name -> liveagent.gateway.v2.FsDirEntry + 122, // 168: liveagent.gateway.v2.FsListResponse.entries:type_name -> liveagent.gateway.v2.FsListEntry + 146, // 169: liveagent.gateway.v2.ChatIngressBatch.records:type_name -> liveagent.gateway.v2.ChatIngressRecord + 147, // 170: liveagent.gateway.v2.ChatIngressRecord.delta:type_name -> liveagent.gateway.v2.ChatIngressDelta + 149, // 171: liveagent.gateway.v2.ChatIngressRecord.checkpoint:type_name -> liveagent.gateway.v2.ChatIngressCheckpoint + 150, // 172: liveagent.gateway.v2.ChatIngressRecord.terminal:type_name -> liveagent.gateway.v2.ChatIngressTerminal + 148, // 173: liveagent.gateway.v2.ChatIngressRecord.heartbeat:type_name -> liveagent.gateway.v2.ChatIngressHeartbeat + 152, // 174: liveagent.gateway.v2.ChatIngressResume.runs:type_name -> liveagent.gateway.v2.ChatIngressRunResume + 3, // 175: liveagent.gateway.v2.ChatIngressAck.action:type_name -> liveagent.gateway.v2.ChatIngressAck.Action + 176, // [176:176] is the sub-list for method output_type + 176, // [176:176] is the sub-list for method input_type + 176, // [176:176] is the sub-list for extension type_name + 176, // [176:176] is the sub-list for extension extendee + 0, // [0:176] is the sub-list for field type_name } func init() { file_proto_v2_gateway_proto_init() } @@ -13759,6 +14021,7 @@ func file_proto_v2_gateway_proto_init() { (*GatewayEnvelope_ManagedProcessRequest)(nil), (*GatewayEnvelope_HistoryBranch)(nil), (*GatewayEnvelope_ProviderUsage)(nil), + (*GatewayEnvelope_ChatFileOpen)(nil), } file_proto_v2_gateway_proto_msgTypes[1].OneofWrappers = []any{ (*AgentEnvelope_ChatEvent)(nil), @@ -13821,6 +14084,7 @@ func file_proto_v2_gateway_proto_init() { (*AgentEnvelope_ChatIngressBatch)(nil), (*AgentEnvelope_ChatIngressResume)(nil), (*AgentEnvelope_ChatIngressFragment)(nil), + (*AgentEnvelope_ChatFileOpenResp)(nil), (*AgentEnvelope_Error)(nil), } file_proto_v2_gateway_proto_msgTypes[15].OneofWrappers = []any{} @@ -13828,7 +14092,9 @@ func file_proto_v2_gateway_proto_init() { file_proto_v2_gateway_proto_msgTypes[79].OneofWrappers = []any{} file_proto_v2_gateway_proto_msgTypes[106].OneofWrappers = []any{} file_proto_v2_gateway_proto_msgTypes[117].OneofWrappers = []any{} - file_proto_v2_gateway_proto_msgTypes[140].OneofWrappers = []any{ + file_proto_v2_gateway_proto_msgTypes[124].OneofWrappers = []any{} + file_proto_v2_gateway_proto_msgTypes[125].OneofWrappers = []any{} + file_proto_v2_gateway_proto_msgTypes[142].OneofWrappers = []any{ (*ChatIngressRecord_Delta)(nil), (*ChatIngressRecord_Checkpoint)(nil), (*ChatIngressRecord_Terminal)(nil), @@ -13840,7 +14106,7 @@ func file_proto_v2_gateway_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_v2_gateway_proto_rawDesc), len(file_proto_v2_gateway_proto_rawDesc)), NumEnums: 4, - NumMessages: 149, + NumMessages: 151, NumExtensions: 0, NumServices: 0, }, diff --git a/crates/agent-gateway/internal/protocol/pbws/guard.go b/crates/agent-gateway/internal/protocol/pbws/guard.go index e4bf553f4..26edd31a8 100644 --- a/crates/agent-gateway/internal/protocol/pbws/guard.go +++ b/crates/agent-gateway/internal/protocol/pbws/guard.go @@ -64,6 +64,8 @@ func vetAgentRequest(sm session.AgentView, env *gatewayv2.GatewayEnvelope) error *gatewayv2.GatewayEnvelope_FsReadWorkspaceImage, *gatewayv2.GatewayEnvelope_ChatQueue: return nil + case *gatewayv2.GatewayEnvelope_ChatFileOpen: + return vetChatFileOpen(payload.ChatFileOpen) // ---- 带功能门控 / 限额的直通臂 ---- case *gatewayv2.GatewayEnvelope_GitRequest: @@ -105,6 +107,35 @@ func vetAgentRequest(sm session.AgentView, env *gatewayv2.GatewayEnvelope) error } } +func vetChatFileOpen(req *gatewayv2.ChatFileOpenRequest) error { + if req == nil || strings.TrimSpace(req.GetConversationId()) == "" || len(req.GetConversationId()) > 256 { + return errors.New("conversation is unavailable") + } + if strings.TrimSpace(req.GetWorkdir()) == "" || strings.TrimSpace(req.GetPath()) == "" { + return errors.New("linked file request is incomplete") + } + if len(req.GetWorkdir()) > 32768 || len(req.GetPath()) > 32768 { + return errors.New("linked file request is too large") + } + switch strings.TrimSpace(req.GetSource()) { + case "absolute", "relative", "file-url": + default: + return errors.New("linked file source is invalid") + } + if (req.Line != nil && req.GetLine() == 0) || + (req.EndLine != nil && req.GetEndLine() == 0) || + (req.Column != nil && req.GetColumn() == 0) { + return errors.New("linked file location is invalid") + } + if req.Line == nil && (req.EndLine != nil || req.Column != nil) { + return errors.New("linked file location is invalid") + } + if req.Line != nil && req.EndLine != nil && req.GetEndLine() < req.GetLine() { + return errors.New("linked file location is invalid") + } + return nil +} + // gitActionIsWrite 判定 git 直通请求是否为写操作:写操作受桌面端 Remote 设置 // enable_web_git 门控,读操作(status/log/diff 等)始终放行。 func gitActionIsWrite(action string) bool { diff --git a/crates/agent-gateway/internal/protocol/pbws/guard_test.go b/crates/agent-gateway/internal/protocol/pbws/guard_test.go index df3095862..9e4a6dc0f 100644 --- a/crates/agent-gateway/internal/protocol/pbws/guard_test.go +++ b/crates/agent-gateway/internal/protocol/pbws/guard_test.go @@ -21,3 +21,42 @@ func TestVetAgentRequestAllowsProviderUsage(t *testing.T) { t.Fatalf("vetAgentRequest() error = %v", err) } } + +func TestVetAgentRequestAllowsValidChatFileOpen(t *testing.T) { + line := uint32(12) + column := uint32(4) + env := &gatewayv2.GatewayEnvelope{ + Payload: &gatewayv2.GatewayEnvelope_ChatFileOpen{ + ChatFileOpen: &gatewayv2.ChatFileOpenRequest{ + ConversationId: "conversation-1", + Workdir: `C:\work`, + Path: `src\a.ts`, + Source: "relative", + Line: &line, + Column: &column, + }, + }, + } + + if err := vetAgentRequest(session.AgentView{}, env); err != nil { + t.Fatalf("vetAgentRequest() error = %v", err) + } +} + +func TestVetAgentRequestRejectsMalformedChatFileOpen(t *testing.T) { + zero := uint32(0) + tests := []*gatewayv2.ChatFileOpenRequest{ + nil, + {ConversationId: "", Workdir: "/work", Path: "a.ts", Source: "relative"}, + {ConversationId: "conversation-1", Workdir: "/work", Path: "a.ts", Source: "javascript"}, + {ConversationId: "conversation-1", Workdir: "/work", Path: "a.ts", Source: "relative", Line: &zero}, + } + for _, request := range tests { + env := &gatewayv2.GatewayEnvelope{ + Payload: &gatewayv2.GatewayEnvelope_ChatFileOpen{ChatFileOpen: request}, + } + if err := vetAgentRequest(session.AgentView{}, env); err == nil { + t.Fatalf("vetAgentRequest(%+v) unexpectedly succeeded", request) + } + } +} diff --git a/crates/agent-gateway/proto/v2/gateway.proto b/crates/agent-gateway/proto/v2/gateway.proto index 90898edda..c78c35e99 100644 --- a/crates/agent-gateway/proto/v2/gateway.proto +++ b/crates/agent-gateway/proto/v2/gateway.proto @@ -60,6 +60,7 @@ message GatewayEnvelope { ManagedProcessRequest managed_process_request = 91; HistoryBranchRequest history_branch = 92; ProviderUsageRequest provider_usage = 93; + ChatFileOpenRequest chat_file_open = 94; } // Legacy tunnel control/frame payloads (pre-rewrite protocol) and the @@ -132,6 +133,7 @@ message AgentEnvelope { ChatIngressBatch chat_ingress_batch = 95; ChatIngressResume chat_ingress_resume = 96; ChatIngressFragment chat_ingress_fragment = 97; + ChatFileOpenResponse chat_file_open_resp = 98; ErrorResponse error = 99; } @@ -1064,6 +1066,28 @@ message FsReadWorkspaceImageResponse { string content_hash = 6; } +message ChatFileOpenRequest { + string conversation_id = 1; + string workdir = 2; + string path = 3; + string source = 4; + optional uint32 line = 5; + optional uint32 end_line = 6; + optional uint32 column = 7; + bool open_in_file_manager = 8; +} + +message ChatFileOpenResponse { + string action = 1; + string kind = 2; + string workdir = 3; + string path = 4; + optional uint32 line = 5; + optional uint32 end_line = 6; + optional uint32 column = 7; + bool outside_workspace = 8; +} + message FsWriteTextRequest { string workdir = 1; string path = 2; diff --git a/crates/agent-gateway/web/src/app/GatewayApp.tsx b/crates/agent-gateway/web/src/app/GatewayApp.tsx index 3e31f2faf..7106e10a1 100644 --- a/crates/agent-gateway/web/src/app/GatewayApp.tsx +++ b/crates/agent-gateway/web/src/app/GatewayApp.tsx @@ -27,6 +27,7 @@ import { useConfirmDialog } from "@/components/ui/confirm-dialog"; import { ScrollArea } from "@/components/ui/scroll-area"; import { LocaleContext, t as translate } from "@/i18n"; import { registerAskUserQuestionAnswerHandler } from "@/lib/chat/askUserQuestionBridge"; +import type { ChatFileLink } from "@/lib/chat/chatFileLinks"; import type { ChatHistorySummary } from "@/lib/chat/chatHistory"; import { buildModelOptions } from "@/lib/chat/chatPageHelpers"; import type { HistoryMessageRef } from "@/lib/chat/conversationState"; @@ -40,6 +41,7 @@ import { trimLeadingHeadlessEntries, } from "@/lib/chat/historyWindow"; import type { CodeMentionReference } from "@/lib/chat/mentionReferences"; +import { openChatFileLink } from "@/lib/chat/openChatFileLink"; import { isChatRuntimeProtocolIncompatible } from "@/lib/chat/runtimeCompatibility"; import { createActivityStore } from "@/lib/chat/stream/activityStore"; import { @@ -4138,6 +4140,83 @@ export default function GatewayApp() { }), [handleChangedFileOpenDiff, handleChangedFileReveal, handleOpenWorkspaceFile], ); + const handleOpenChatFileLink = useCallback( + (link: ChatFileLink) => { + const conversationWorkdir = displayedConversationWorkdir.trim(); + if (!displayedConversationId || !conversationWorkdir) { + addNotify("error", "The conversation working directory is unavailable."); + return; + } + const request = { + ...link, + conversationId: displayedConversationId, + workdir: conversationWorkdir, + }; + void openChatFileLink(request) + .then(async (result) => { + if (result.action === "opened" || result.action === "revealed") return; + const resultWorkdir = result.workdir?.trim() ?? ""; + const resultPath = result.path?.trim() ?? ""; + if (!resultWorkdir || !resultPath) { + addNotify("error", "The linked file could not be opened."); + return; + } + if (result.action === "directory") { + if (workspaceProjectPathKey(resultWorkdir) === terminalProjectPathKey) { + handleChangedFileReveal(resultPath); + return; + } + const fallback = await openChatFileLink({ ...request, openInFileManager: true }); + if (fallback.action !== "opened") { + addNotify("error", "The linked directory could not be opened."); + } + return; + } + const workspaceRequest = { + projectPathKey: workspaceProjectPathKey(resultWorkdir), + workdir: resultWorkdir, + path: resultPath, + }; + if ( + !result.outsideWorkspace && + workspaceRequest.projectPathKey === terminalProjectPathKey + ) { + handleChangedFileReveal(resultPath); + } + if (result.action === "preview") { + openWorkspaceFilePreview(workspaceRequest); + return; + } + openWorkspaceEditorFile({ + ...workspaceRequest, + line: result.line, + endLine: result.endLine, + column: result.column, + }); + }) + .catch((error: unknown) => { + const message = asErrorMessage(error, "The linked file could not be opened."); + const normalized = message.toLowerCase(); + addNotify( + "error", + normalized.includes("timed out") || + normalized.includes("offline") || + normalized.includes("not connected") + ? "The device that owns this conversation is offline or did not respond." + : message, + ); + }); + }, + [ + addNotify, + displayedConversationId, + displayedConversationWorkdir, + handleChangedFileReveal, + openWorkspaceEditorFile, + openWorkspaceFilePreview, + terminalProjectPathKey, + ], + ); // RightDockPanel is memo'd: every callback handed to it must be stable or // the memo boundary is void (see the panel-side context useMemo). const handleChatTranscriptWidthChange = useCallback( @@ -4765,6 +4844,7 @@ export default function GatewayApp() { showUsage={isAgentDevExecutionMode} usageContextWindow={currentModelContextWindow} workspaceRoot={displayedConversationWorkdir} + onOpenFileLink={handleOpenChatFileLink} gitClient={gitClient} onLoadUploadedImagePreview={handleLoadUploadedImagePreview} onResendFromEdit={handleResendFromEdit} diff --git a/crates/agent-gateway/web/src/components/GatewayTranscript.tsx b/crates/agent-gateway/web/src/components/GatewayTranscript.tsx index 1140cbaee..f3a110549 100644 --- a/crates/agent-gateway/web/src/components/GatewayTranscript.tsx +++ b/crates/agent-gateway/web/src/components/GatewayTranscript.tsx @@ -14,6 +14,7 @@ import { import { ImagePreview, type ImagePreviewSlide } from "@/components/chat/ImagePreview"; import { Markdown } from "@/components/Markdown"; import { useLocale } from "@/i18n/LocaleContext"; +import type { ChatFileLink } from "@/lib/chat/chatFileLinks"; import { normalizeLiveToolStatus, VIBING_STATUS } from "@/lib/chat/chatPageHelpers"; import type { HistoryMessageRef } from "@/lib/chat/conversationState"; import { getRoundText, getRoundToolTrace } from "@/lib/chat/uiMessages"; @@ -114,6 +115,7 @@ type GatewayTranscriptProps = { usageContextWindow?: number; workspaceRoot?: string; gitClient?: GitClient | null; + onOpenFileLink?: (link: ChatFileLink) => void; onLoadUploadedImagePreview?: UploadedImagePreviewLoader; onResendFromEdit?: ( messageRef: HistoryMessageRef, @@ -1193,6 +1195,7 @@ const GatewayTranscriptListRegion = memo(function GatewayTranscriptListRegion(pr usageContextWindow?: number; workspaceRoot?: string; gitClient?: GitClient | null; + onOpenFileLink?: (link: ChatFileLink) => void; onLoadUploadedImagePreview?: UploadedImagePreviewLoader; onResendFromEdit?: ( messageRef: HistoryMessageRef, @@ -1226,6 +1229,7 @@ const GatewayTranscriptListRegion = memo(function GatewayTranscriptListRegion(pr usageContextWindow, workspaceRoot, gitClient, + onOpenFileLink, onLoadUploadedImagePreview, onResendFromEdit, onBranchConversation, @@ -1721,6 +1725,8 @@ const GatewayTranscriptListRegion = memo(function GatewayTranscriptListRegion(pr renderMode={rowRenderMode(row)} readOnly={readOnly} redactToolContent={redactToolContent} + workdir={workspaceRoot} + onOpenFileLink={onOpenFileLink} /> {shouldShowLiveStatus ? : null} {isLatestLiveStreaming && @@ -1809,6 +1815,7 @@ export function GatewayTranscript({ usageContextWindow, workspaceRoot, gitClient, + onOpenFileLink, onLoadUploadedImagePreview, onResendFromEdit, onBranchConversation, @@ -1888,6 +1895,7 @@ export function GatewayTranscript({ usageContextWindow={usageContextWindow} workspaceRoot={workspaceRoot} gitClient={gitClient} + onOpenFileLink={onOpenFileLink} onLoadUploadedImagePreview={onLoadUploadedImagePreview} onResendFromEdit={onResendFromEdit} onBranchConversation={onBranchConversation} diff --git a/crates/agent-gateway/web/src/components/Markdown.tsx b/crates/agent-gateway/web/src/components/Markdown.tsx index dc5f5d0d3..30aad7a19 100644 --- a/crates/agent-gateway/web/src/components/Markdown.tsx +++ b/crates/agent-gateway/web/src/components/Markdown.tsx @@ -25,6 +25,12 @@ import { type StreamdownTranslations, } from "streamdown"; import { useLocale } from "../i18n"; +import { + type ChatFileLink, + decodeChatFileLinkPayload, + encodeChatFileLink, + parseChatFileLink, +} from "../lib/chat/chatFileLinks"; import { getCollapsedCodeBlockPreview, resolveCodeBlockRenderPolicy, @@ -34,7 +40,132 @@ import { cn } from "../lib/shared/utils"; import { Check, ChevronDown, ChevronUp, Copy, ExternalLink, X } from "./icons"; import { Button } from "./ui/button"; -type MarkdownProps = { +const CHAT_FILE_NODE_DATA_KEY = "liveagentChatFileLink"; +const LIVEAGENT_FILE_PROTOCOL = "liveagent-file:"; + +type ChatFileHastNode = { + children?: ChatFileHastNode[]; + data?: Record; + properties?: Record; + position?: { start?: { offset?: number } }; + type?: string; + tagName?: string; +}; + +type ChatFileVFile = { value?: unknown }; + +function visitChatFileElements(node: ChatFileHastNode, visitor: (node: ChatFileHastNode) => void) { + if (node.type === "element") visitor(node); + for (const child of node.children ?? []) visitChatFileElements(child, visitor); +} + +export function rewriteChatFileLinks() { + return (tree: unknown, file?: ChatFileVFile) => { + visitChatFileElements(tree as ChatFileHastNode, (node) => { + if (node.tagName !== "a") return; + const source = typeof file?.value === "string" ? file.value : ""; + const sourceOffset = node.position?.start?.offset; + // Raw HTML anchors keep their source position after rehype-raw. They + // must not acquire the trusted marker that only Markdown links receive. + if (sourceOffset !== undefined && source[sourceOffset] === "<") return; + const href = typeof node.properties?.href === "string" ? node.properties.href : ""; + const parsed = parseChatFileLink(href); + if (!parsed) return; + const internalHref = encodeChatFileLink(parsed); + node.properties = { ...node.properties, href: internalHref }; + node.data = { ...node.data, [CHAT_FILE_NODE_DATA_KEY]: internalHref }; + }); + }; +} + +type ChatFileMdastNode = { + children?: ChatFileMdastNode[]; + position?: { start?: { offset?: number }; end?: { offset?: number } }; + type?: string; + url?: string; + value?: string; +}; + +const CHAT_FILE_MARKDOWN_LINK_PATTERN = /\[([^\]\n]+)\]\(([^)\n]+)\)/g; +const SKIPPED_CHAT_FILE_MDAST_NODES = new Set(["code", "html", "image", "inlineCode", "link"]); +const COMMONMARK_ESCAPABLE_CHARACTER = /[!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]/; + +function mapDecodedSourceOffsets(sourceValue: string, value: string) { + let decoded = ""; + const rawOffsets: number[] = []; + for (let index = 0; index < sourceValue.length; index += 1) { + const next = sourceValue[index + 1]; + if (sourceValue[index] === "\\" && next && COMMONMARK_ESCAPABLE_CHARACTER.test(next)) { + decoded += next; + rawOffsets.push(index + 1); + index += 1; + } else { + decoded += sourceValue[index]; + rawOffsets.push(index); + } + } + return decoded === value ? rawOffsets : null; +} + +function rewriteChatFileTextNode(node: ChatFileMdastNode, source: string) { + const value = node.value ?? ""; + const sourceStart = node.position?.start?.offset; + const sourceEnd = node.position?.end?.offset; + const sourceValue = + sourceStart !== undefined && sourceEnd !== undefined + ? source.slice(sourceStart, sourceEnd) + : ""; + const rawOffsets = mapDecodedSourceOffsets(sourceValue, value); + if (!rawOffsets) return null; + const nodes: ChatFileMdastNode[] = []; + let cursor = 0; + for (const match of value.matchAll(CHAT_FILE_MARKDOWN_LINK_PATTERN)) { + const index = match.index ?? 0; + const sourceIndex = rawOffsets[index]; + let backslashes = 0; + for (let offset = sourceIndex - 1; offset >= 0 && sourceValue[offset] === "\\"; offset -= 1) { + backslashes += 1; + } + if (backslashes % 2 === 1) continue; + const destination = match[2].trim(); + if (!parseChatFileLink(destination)) continue; + if (index > cursor) nodes.push({ type: "text", value: value.slice(cursor, index) }); + nodes.push({ + type: "link", + url: destination, + children: [{ type: "text", value: match[1] }], + }); + cursor = index + match[0].length; + } + if (cursor === 0) return null; + if (cursor < value.length) nodes.push({ type: "text", value: value.slice(cursor) }); + return nodes; +} + +function rewriteChatFileMarkdownChildren(node: ChatFileMdastNode, source: string) { + if (!node.children || SKIPPED_CHAT_FILE_MDAST_NODES.has(node.type ?? "")) return; + for (let index = 0; index < node.children.length; index += 1) { + const child = node.children[index]; + if (child.type === "text" && typeof child.value === "string") { + const replacement = rewriteChatFileTextNode(child, source); + if (!replacement) continue; + node.children.splice(index, 1, ...replacement); + index += replacement.length - 1; + continue; + } + rewriteChatFileMarkdownChildren(child, source); + } +} + +export function remarkChatFileLinks() { + return (tree: unknown, file?: ChatFileVFile) => + rewriteChatFileMarkdownChildren( + tree as ChatFileMdastNode, + typeof file?.value === "string" ? file.value : "", + ); +} + +export type MarkdownProps = { content: string; className?: string; // Fixed render mode: content born from a live stream renders in Streamdown @@ -56,33 +187,77 @@ type MarkdownProps = { // against the page origin before they reach custom components. Sanitize // still runs, so scriptable protocols (javascript: etc.) never get through. preserveRelativeUrls?: boolean; + // Chat file links are only rewritten when this explicit user-action callback + // is present. Other Markdown surfaces keep Streamdown's normal link policy. + workdir?: string; + onOpenFileLink?: (link: ChatFileLink) => void; }; const streamdownPlugins = { code, math, mermaid, cjk }; const remarkPlugins = [...Object.values(defaultRemarkPlugins), remarkBreaks]; +const chatRemarkPlugins = [...remarkPlugins, remarkChatFileLinks]; type StreamdownRehypePlugins = NonNullable["rehypePlugins"]>; -// raw + sanitize from the default chain (raw → sanitize → harden), with data: -// image sources additionally allowed so embedded data-URI images render. -const relativeUrlRehypePlugins = (() => { +function createSanitizedRehypePlugins(options: { + allowDataImages: boolean; + preserveRelativeUrls: boolean; + rewriteFileLinks: boolean; +}) { const sanitize = defaultRehypePlugins.sanitize; if (!Array.isArray(sanitize)) { - return [defaultRehypePlugins.raw, sanitize] as StreamdownRehypePlugins; + return [ + defaultRehypePlugins.raw, + ...(options.rewriteFileLinks ? [rewriteChatFileLinks] : []), + sanitize, + ...(options.preserveRelativeUrls ? [] : [defaultRehypePlugins.harden]), + ] as StreamdownRehypePlugins; } const schema = (sanitize[1] ?? {}) as { protocols?: Record }; const srcProtocols = schema.protocols?.src; + const hrefProtocols = schema.protocols?.href; const protocols = { ...schema.protocols, - src: Array.isArray(srcProtocols) - ? [...new Set([...srcProtocols, "data"])] - : ["http", "https", "data"], + ...(options.rewriteFileLinks + ? { + href: Array.isArray(hrefProtocols) + ? [...new Set([...hrefProtocols, "liveagent-file"])] + : ["http", "https", "mailto", "liveagent-file"], + } + : {}), + ...(options.allowDataImages + ? { + src: Array.isArray(srcProtocols) + ? [...new Set([...srcProtocols, "data"])] + : ["http", "https", "data"], + } + : {}), }; return [ defaultRehypePlugins.raw, + ...(options.rewriteFileLinks ? [rewriteChatFileLinks] : []), [sanitize[0], { ...schema, protocols }], + ...(options.preserveRelativeUrls ? [] : [defaultRehypePlugins.harden]), ] as StreamdownRehypePlugins; -})(); +} + +// Workspace previews intentionally skip harden so relative assets reach their +// custom renderer. Chat surfaces always use raw → rewrite → sanitize → harden. +export const relativeUrlRehypePlugins = createSanitizedRehypePlugins({ + allowDataImages: true, + preserveRelativeUrls: true, + rewriteFileLinks: false, +}); +export const chatFileRehypePlugins = createSanitizedRehypePlugins({ + allowDataImages: false, + preserveRelativeUrls: false, + rewriteFileLinks: true, +}); +const relativeChatFileRehypePlugins = createSanitizedRehypePlugins({ + allowDataImages: true, + preserveRelativeUrls: true, + rewriteFileLinks: true, +}); type MarkdownImageFallbackProps = ComponentProps<"img"> & ExtraProps; type MarkdownAnchorFallbackProps = ComponentProps<"a"> & ExtraProps; @@ -93,8 +268,46 @@ type StreamdownCodeChildProps = { "data-block"?: string; }; +type MarkdownFileLinkProps = ComponentProps<"a"> & + ExtraProps & { + onOpenFileLink: (link: ChatFileLink) => void; + workdir?: string; + }; + const DEFAULT_CODE_BLOCK_LANGUAGE = "markdown"; +function readRewrittenChatFileLink(props: MarkdownAnchorFallbackProps) { + const href = typeof props.href === "string" ? props.href : ""; + const marker = (props.node as ChatFileHastNode | undefined)?.data?.[CHAT_FILE_NODE_DATA_KEY]; + if (!href.startsWith(LIVEAGENT_FILE_PROTOCOL) || marker !== href) return null; + const parsed = decodeChatFileLinkPayload(href.slice(LIVEAGENT_FILE_PROTOCOL.length)); + if (!parsed || encodeChatFileLink(parsed) !== href) return null; + return parsed; +} + +export function MarkdownFileLink(props: MarkdownFileLinkProps) { + const { children, title, onOpenFileLink, workdir } = props; + const parsed = readRewrittenChatFileLink(props); + if (!parsed) return ; + const label = + typeof title === "string" && title.trim() + ? title.trim() + : parsed.source === "relative" && workdir + ? `${workdir.replace(/[\\/]+$/, "")}/${parsed.path.replace(/^[\\/]+/, "")}` + : parsed.path; + return ( + + ); +} + function MarkdownImageFallback(props: MarkdownImageFallbackProps) { const { alt, title } = props; const label = @@ -140,6 +353,46 @@ export const markdownReadOnlyComponents: Components = { a: MarkdownReadOnlyLink, }; +function MarkdownExternalLink(props: MarkdownAnchorFallbackProps) { + const { children, className, href, title } = props; + const [modalOpen, setModalOpen] = useState(false); + if (!href) return ; + const incomplete = href === "streamdown:incomplete-link"; + return ( + <> + + setModalOpen(false)} + onConfirm={() => window.open(href, "_blank", "noreferrer")} + /> + + ); +} + +export function MarkdownLink(props: MarkdownFileLinkProps) { + if (readRewrittenChatFileLink(props)) return ; + if (typeof props.href === "string" && props.href.startsWith(LIVEAGENT_FILE_PROTOCOL)) { + return ; + } + return ; +} + async function copyCodeBlockText(text: string) { try { await navigator.clipboard.writeText(text); @@ -435,17 +688,33 @@ export const Markdown = memo(function Markdown(props: MarkdownProps) { readOnly = false, componentOverrides, preserveRelativeUrls = false, + workdir, + onOpenFileLink, } = props; const streaming = renderMode === "streaming"; const normalizedContent = useMemo( () => normalizeLatexDelimiters(content, streaming && showCaret), [content, showCaret, streaming], ); - const baseComponents = readOnly ? markdownReadOnlyComponents : markdownComponents; - const components = useMemo( - () => (componentOverrides ? { ...baseComponents, ...componentOverrides } : baseComponents), - [baseComponents, componentOverrides], - ); + const components = useMemo(() => { + const baseComponents = readOnly ? markdownReadOnlyComponents : markdownComponents; + const fileLinkComponents: Components = + !readOnly && onOpenFileLink + ? { + a: (linkProps) => ( + + ), + } + : {}; + return { ...baseComponents, ...fileLinkComponents, ...componentOverrides }; + }, [componentOverrides, onOpenFileLink, readOnly, workdir]); + const rehypePlugins = onOpenFileLink + ? preserveRelativeUrls + ? relativeChatFileRehypePlugins + : chatFileRehypePlugins + : preserveRelativeUrls + ? relativeUrlRehypePlugins + : undefined; return (
@@ -454,16 +723,12 @@ export const Markdown = memo(function Markdown(props: MarkdownProps) { "chat-markdown max-w-none break-words", MARKDOWN_EMBED_CLASSNAME, streaming ? "chat-markdown--streaming" : "chat-markdown--static", - // Streamdown's memo equality does not include `caret` in its check, - // so toggling the caret prop alone does not invalidate the render. - // Mirror the visibility into a className modifier to force a re-render - // that recomputes the inline `--streamdown-caret` style. showCaret ? "chat-markdown--caret-on" : "chat-markdown--caret-off", className, )} plugins={streamdownPlugins} - remarkPlugins={remarkPlugins} - {...(preserveRelativeUrls ? { rehypePlugins: relativeUrlRehypePlugins } : {})} + remarkPlugins={onOpenFileLink ? chatRemarkPlugins : remarkPlugins} + {...(rehypePlugins ? { rehypePlugins } : {})} components={components} mode={streaming ? "streaming" : "static"} dir="auto" diff --git a/crates/agent-gateway/web/src/components/workspace-editor/WorkspaceCodeEditorOverlay.tsx b/crates/agent-gateway/web/src/components/workspace-editor/WorkspaceCodeEditorOverlay.tsx index 7f414a9c2..478fd3807 100644 --- a/crates/agent-gateway/web/src/components/workspace-editor/WorkspaceCodeEditorOverlay.tsx +++ b/crates/agent-gateway/web/src/components/workspace-editor/WorkspaceCodeEditorOverlay.tsx @@ -68,6 +68,9 @@ export type WorkspaceCodeEditorOpenRequest = { projectPathKey: string; workdir: string; path: string; + line?: number; + endLine?: number; + column?: number; }; type ReadEditableTextResponse = { @@ -300,6 +303,7 @@ export function WorkspaceCodeEditorOverlay(props: WorkspaceCodeEditorOverlayProp const modelsRef = useRef(new Map()); const viewStatesRef = useRef(new Map()); const editorModelKeyRef = useRef(""); + const linkedLocationKeyRef = useRef(""); const activeKeyRef = useRef(""); const openRequestIdRef = useRef(null); const closeRequestIdRef = useRef(null); @@ -799,6 +803,34 @@ export function WorkspaceCodeEditorOverlay(props: WorkspaceCodeEditorOverlayProp editorModelKeyRef.current = activeTab.key; }, [activeTab]); + const activeTabKey = activeTab?.key; + useEffect(() => { + const editor = editorRef.current; + if (!editor || !activeTabKey || !openRequest?.line) return; + if (activeTabKey !== editorTabKey(openRequest.projectPathKey, openRequest.path)) return; + const locationKey = `${openRequest.id}\u0000${activeTabKey}`; + if (linkedLocationKeyRef.current === locationKey) return; + const model = editor.getModel(); + if (!model) return; + const line = Math.min(Math.max(1, openRequest.line), model.getLineCount()); + const endLine = Math.min(Math.max(line, openRequest.endLine ?? line), model.getLineCount()); + const column = Math.min(Math.max(1, openRequest.column ?? 1), model.getLineMaxColumn(line)); + const endColumn = openRequest.endLine ? model.getLineMaxColumn(endLine) : column; + const range = new monaco.Range(line, column, endLine, endColumn); + editor.setSelection(range); + editor.revealRangeInCenter(range); + editor.focus(); + linkedLocationKeyRef.current = locationKey; + }, [ + activeTabKey, + openRequest?.column, + openRequest?.endLine, + openRequest?.id, + openRequest?.line, + openRequest?.path, + openRequest?.projectPathKey, + ]); + useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape") { diff --git a/crates/agent-gateway/web/src/lib/chat/chatFileLinks.ts b/crates/agent-gateway/web/src/lib/chat/chatFileLinks.ts new file mode 100644 index 000000000..7cf63d3b9 --- /dev/null +++ b/crates/agent-gateway/web/src/lib/chat/chatFileLinks.ts @@ -0,0 +1,201 @@ +export type ChatFileLink = { + path: string; + line?: number; + endLine?: number; + column?: number; + source: "absolute" | "relative" | "file-url"; +}; + +const WINDOWS_DRIVE_PATH_PATTERN = /^[a-zA-Z]:[\\/]/; +const WINDOWS_UNC_PATH_PATTERN = /^(?:\\\\|\/\/)/; +const FILE_URL_PATTERN = /^file:\/\//i; +const ABSOLUTE_POSIX_PATH_PATTERN = /^\//; +const URI_SCHEME_PATTERN = /^[a-zA-Z][a-zA-Z\d+.-]*:/; +const LOCATION_FRAGMENT_PATTERN = /^#L([1-9]\d*)(?:-L?([1-9]\d*))?$/i; +const LOCATION_SUFFIX_PATTERN = /:([1-9]\d*)(?::([1-9]\d*))?$/; +const INTERNAL_PAYLOAD_VERSION = "1"; +const MAX_LOCATION_VALUE = 0xffff_ffff; + +function parseLocationNumber(value: string) { + const parsed = Number(value); + return Number.isSafeInteger(parsed) && parsed > 0 && parsed <= MAX_LOCATION_VALUE ? parsed : null; +} + +function safeDecode(value: string) { + try { + return decodeURIComponent(value); + } catch { + return value; + } +} + +function normalizePath(path: string) { + const decoded = safeDecode(path); + const isUnc = /^(?:\\\\|\/\/[^/])/.test(decoded); + const collapsed = decoded.replace(/\\/g, "/").replace(/\/{2,}/g, "/"); + return isUnc ? `//${collapsed.replace(/^\/+/, "")}` : collapsed; +} + +function isAbsolutePath(path: string) { + return ( + WINDOWS_DRIVE_PATH_PATTERN.test(path) || + WINDOWS_UNC_PATH_PATTERN.test(path) || + ABSOLUTE_POSIX_PATH_PATTERN.test(path) + ); +} + +function parseTrailingLocation(value: string) { + let path = value.trim(); + let line: number | undefined; + let endLine: number | undefined; + let column: number | undefined; + + const hashIndex = path.lastIndexOf("#"); + const hashMatch = hashIndex >= 0 ? path.slice(hashIndex).match(LOCATION_FRAGMENT_PATTERN) : null; + if (hashMatch) { + line = parseLocationNumber(hashMatch[1]) ?? undefined; + if (line === undefined) return { path: "", line, endLine, column }; + if (hashMatch[2]) { + endLine = parseLocationNumber(hashMatch[2]) ?? undefined; + if (endLine === undefined) return { path: "", line, endLine, column }; + } + path = path.slice(0, hashIndex); + } + + const lineMatch = line === undefined ? path.match(LOCATION_SUFFIX_PATTERN) : null; + if (lineMatch?.index !== undefined) { + line = parseLocationNumber(lineMatch[1]) ?? undefined; + if (line === undefined) return { path: "", line, endLine, column }; + if (lineMatch[2]) { + column = parseLocationNumber(lineMatch[2]) ?? undefined; + if (column === undefined) return { path: "", line, endLine, column }; + } + path = path.slice(0, lineMatch.index); + } + + return { path, line, endLine, column }; +} + +function createChatFileLink( + path: string, + source: ChatFileLink["source"], + location: Omit, +): ChatFileLink { + return { + path, + ...(location.line === undefined ? {} : { line: location.line }), + ...(location.endLine === undefined ? {} : { endLine: location.endLine }), + ...(location.column === undefined ? {} : { column: location.column }), + source, + }; +} + +function isSafeRelativePath(path: string) { + return ( + Boolean(path) && + path !== "." && + path !== ".." && + !path.startsWith("#") && + !path.includes("\0") && + !path.includes("\n") && + !path.includes("\r") && + !URI_SCHEME_PATTERN.test(path) + ); +} + +export function parseChatFileLink(raw: string): ChatFileLink | null { + const input = raw.trim(); + if (!input) return null; + + if (FILE_URL_PATTERN.test(input)) { + try { + const url = new URL(input); + if (url.protocol !== "file:") return null; + const hashLocation = url.hash.match(LOCATION_FRAGMENT_PATTERN); + const hashLine = hashLocation ? parseLocationNumber(hashLocation[1]) : null; + const hashEndLine = hashLocation?.[2] ? parseLocationNumber(hashLocation[2]) : null; + if (hashLocation && (hashLine === null || (hashLocation[2] && hashEndLine === null))) { + return null; + } + const locationFromHash = hashLocation + ? { + line: hashLine ?? undefined, + ...(hashEndLine === null ? {} : { endLine: hashEndLine }), + } + : {}; + const pathWithHost = url.host ? `//${safeDecode(url.host)}${url.pathname}` : url.pathname; + const trailing = parseTrailingLocation(pathWithHost); + const normalized = normalizePath(trailing.path).replace(/^\/([a-zA-Z]:)/, "$1"); + if (!normalized || !isAbsolutePath(normalized)) return null; + return createChatFileLink(normalized, "file-url", { + ...trailing, + ...locationFromHash, + }); + } catch { + return null; + } + } + + const { path, line, endLine, column } = parseTrailingLocation(input); + const normalized = normalizePath(path); + + if (isAbsolutePath(normalized)) { + return createChatFileLink(normalized, "absolute", { line, endLine, column }); + } + + if (isSafeRelativePath(normalized)) { + return createChatFileLink(normalized, "relative", { line, endLine, column }); + } + + return null; +} + +export function isChatFileLinkTarget(raw: string) { + return Boolean(parseChatFileLink(raw)); +} + +export function encodeChatFileLink(link: ChatFileLink) { + const params = new URLSearchParams(); + params.set("v", INTERNAL_PAYLOAD_VERSION); + params.set("path", link.path); + if (link.line !== undefined) params.set("line", String(link.line)); + if (link.endLine !== undefined) params.set("endLine", String(link.endLine)); + if (link.column !== undefined) params.set("column", String(link.column)); + params.set("source", link.source); + return `liveagent-file:${params.toString()}`; +} + +export function decodeChatFileLinkPayload(payload: string): ChatFileLink | null { + const params = new URLSearchParams(payload); + const allowedKeys = new Set(["v", "path", "line", "endLine", "column", "source"]); + for (const key of params.keys()) { + if (!allowedKeys.has(key) || params.getAll(key).length !== 1) return null; + } + if (params.get("v") !== INTERNAL_PAYLOAD_VERSION) return null; + const path = params.get("path") ?? ""; + const source = params.get("source"); + if (!path || (source !== "absolute" && source !== "relative" && source !== "file-url")) + return null; + + const normalized = normalizePath(path); + const sourceMatches = + source === "relative" + ? isSafeRelativePath(normalized) && !isAbsolutePath(normalized) + : isAbsolutePath(normalized); + if (!sourceMatches) return null; + + const parseLocationValue = (key: "line" | "endLine" | "column") => { + const value = params.get(key); + if (value === null) return undefined; + if (!/^[1-9]\d*$/.test(value)) return null; + return parseLocationNumber(value); + }; + const line = parseLocationValue("line"); + const endLine = parseLocationValue("endLine"); + const column = parseLocationValue("column"); + if (line === null || endLine === null || column === null) return null; + if ((endLine !== undefined || column !== undefined) && line === undefined) return null; + if (line !== undefined && endLine !== undefined && endLine < line) return null; + + return createChatFileLink(normalized, source, { line, endLine, column }); +} diff --git a/crates/agent-gateway/web/src/lib/chat/openChatFileLink.ts b/crates/agent-gateway/web/src/lib/chat/openChatFileLink.ts new file mode 100644 index 000000000..1071b913a --- /dev/null +++ b/crates/agent-gateway/web/src/lib/chat/openChatFileLink.ts @@ -0,0 +1,33 @@ +import { invoke } from "@tauri-apps/api/core"; + +import type { ChatFileLink } from "./chatFileLinks"; + +export type OpenChatFileLinkParams = ChatFileLink & { + conversationId: string; + workdir: string; + openInFileManager?: boolean; +}; + +export type OpenChatFileLinkResult = { + action: "directory" | "editor" | "opened" | "preview" | "revealed"; + kind: "directory" | "file"; + workdir?: string; + path?: string; + line?: number; + endLine?: number; + column?: number; + outsideWorkspace: boolean; +}; + +export function openChatFileLink(params: OpenChatFileLinkParams) { + return invoke("open_chat_file_link", { + conversation_id: params.conversationId, + workdir: params.workdir, + path: params.path, + source: params.source, + line: params.line, + end_line: params.endLine, + column: params.column, + open_in_file_manager: params.openInFileManager, + }); +} diff --git a/crates/agent-gateway/web/src/lib/gatewaySocket.ts b/crates/agent-gateway/web/src/lib/gatewaySocket.ts index c7c3bee3e..4576ce46f 100644 --- a/crates/agent-gateway/web/src/lib/gatewaySocket.ts +++ b/crates/agent-gateway/web/src/lib/gatewaySocket.ts @@ -205,6 +205,17 @@ type FsReadWorkspaceImageResponse = { contentHash: string; }; +export type ChatFileOpenResponse = { + action: "directory" | "editor" | "opened" | "preview" | "revealed"; + kind: "directory" | "file"; + workdir?: string; + path?: string; + line?: number; + endLine?: number; + column?: number; + outsideWorkspace: boolean; +}; + type FsCreateDirResponse = { path: string; kind: "dir"; @@ -2651,6 +2662,28 @@ export class GatewayWebSocketClient { }); } + async openChatFile(params: { + conversationId: string; + workdir: string; + path: string; + source: string; + line?: number; + endLine?: number; + column?: number; + openInFileManager?: boolean; + }): Promise { + return this.request("chat.file_open", { + conversation_id: params.conversationId, + workdir: params.workdir, + path: params.path, + source: params.source, + line: params.line, + end_line: params.endLine, + column: params.column, + open_in_file_manager: params.openInFileManager, + }); + } + async createDir(workdir: string, path: string): Promise { return this.request("fs.create_dir", { workdir, path }); } @@ -3811,6 +3844,16 @@ export type GatewayWebSocketClientLike = { }): Promise; readEditableTextFile(workdir: string, path: string): Promise; readWorkspaceImageFile(workdir: string, path: string): Promise; + openChatFile(params: { + conversationId: string; + workdir: string; + path: string; + source: string; + line?: number; + endLine?: number; + column?: number; + openInFileManager?: boolean; + }): Promise; createDir(workdir: string, path: string): Promise; renamePath(workdir: string, fromPath: string, toPath: string): Promise; deletePath(workdir: string, path: string): Promise; diff --git a/crates/agent-gateway/web/src/lib/gatewaySocketV2/adapters.ts b/crates/agent-gateway/web/src/lib/gatewaySocketV2/adapters.ts index 8e23940f0..f4b8e976f 100644 --- a/crates/agent-gateway/web/src/lib/gatewaySocketV2/adapters.ts +++ b/crates/agent-gateway/web/src/lib/gatewaySocketV2/adapters.ts @@ -32,6 +32,7 @@ import type { import { CancelChatRequestSchema, ChatCommandRequestSchema, + ChatFileOpenRequestSchema, ChatMessageRefSchema, ChatQueueRequestSchema, ChatRequestSchema, @@ -148,6 +149,11 @@ function optBool(value: unknown): boolean | undefined { return typeof value === "boolean" ? value : undefined; } +function optPositiveU32(value: unknown): number | undefined { + const parsed = Number(value); + return Number.isInteger(parsed) && parsed > 0 && parsed <= 0xffff_ffff ? parsed : undefined; +} + // 64 位整数出站边界:number → bigint。 function toI64(value: unknown): bigint { const parsed = Number(value); @@ -739,6 +745,20 @@ function agentRequestPayload(type: string, body: J): GatewayEnvelope["payload"] path: trimStr(body.path), }), }; + case "chat.file_open": + return { + case: "chatFileOpen", + value: create(ChatFileOpenRequestSchema, { + conversationId: trimStr(body.conversation_id), + workdir: trimStr(body.workdir), + path: trimStr(body.path), + source: trimStr(body.source), + line: optPositiveU32(body.line), + endLine: optPositiveU32(body.end_line), + column: optPositiveU32(body.column), + openInFileManager: bool(body.open_in_file_manager), + }), + }; default: throw new Error(`unsupported gateway request type: ${type}`); } @@ -1139,6 +1159,17 @@ function decodeAgentResponse(envelope: AgentEnvelope, options: { agentOnline: bo mtimeMs: num(payload.value.mtimeMs), contentHash: payload.value.contentHash, }; + case "chatFileOpenResp": + return { + action: payload.value.action, + kind: payload.value.kind, + workdir: payload.value.workdir || undefined, + path: payload.value.path || undefined, + line: payload.value.line, + endLine: payload.value.endLine, + column: payload.value.column, + outsideWorkspace: payload.value.outsideWorkspace, + }; case "fsWriteTextResp": return { path: payload.value.path, diff --git a/crates/agent-gateway/web/src/lib/proto/gen/proto/v2/gateway_pb.ts b/crates/agent-gateway/web/src/lib/proto/gen/proto/v2/gateway_pb.ts index c3105ba31..53c961f51 100644 --- a/crates/agent-gateway/web/src/lib/proto/gen/proto/v2/gateway_pb.ts +++ b/crates/agent-gateway/web/src/lib/proto/gen/proto/v2/gateway_pb.ts @@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file proto/v2/gateway.proto. */ export const file_proto_v2_gateway: GenFile = /*@__PURE__*/ - fileDesc("ChZwcm90by92Mi9nYXRld2F5LnByb3RvEhRsaXZlYWdlbnQuZ2F0ZXdheS52MiLxGgoPR2F0ZXdheUVudmVsb3BlEhIKCnJlcXVlc3RfaWQYASABKAkSEQoJdGltZXN0YW1wGAIgASgDEkAKDGNoYXRfY29tbWFuZBgKIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRDb21tYW5kUmVxdWVzdEgAEj4KC2Nyb25fbWFuYWdlGBQgASgLMicubGl2ZWFnZW50LmdhdGV3YXkudjIuQ3Jvbk1hbmFnZVJlcXVlc3RIABJACgxoaXN0b3J5X2xpc3QYHiABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5TGlzdFJlcXVlc3RIABI+CgtoaXN0b3J5X2dldBgfIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlHZXRSZXF1ZXN0SAASRAoOaGlzdG9yeV9yZW5hbWUYICABKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5UmVuYW1lUmVxdWVzdEgAEkQKDmhpc3RvcnlfZGVsZXRlGCEgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeURlbGV0ZVJlcXVlc3RIABJECg5oaXN0b3J5X3ByZWZpeBgiIAEoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlQcmVmaXhSZXF1ZXN0SAASPgoLaGlzdG9yeV9waW4YIyABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5UGluUmVxdWVzdEgAEkkKEWhpc3Rvcnlfc2hhcmVfZ2V0GCQgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVNoYXJlR2V0UmVxdWVzdEgAEkkKEWhpc3Rvcnlfc2hhcmVfc2V0GCUgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVNoYXJlU2V0UmVxdWVzdEgAElEKFWhpc3Rvcnlfc2hhcmVfcmVzb2x2ZRgmIAEoCzIwLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlTaGFyZVJlc29sdmVSZXF1ZXN0SAASSAoQaGlzdG9yeV93b3JrZGlycxgnIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlXb3JrZGlyc1JlcXVlc3RIABJCCg1wcm92aWRlcl9saXN0GCggASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuUHJvdmlkZXJMaXN0UmVxdWVzdEgAEkAKDHNldHRpbmdzX2dldBgpIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNldHRpbmdzR2V0UmVxdWVzdEgAEkYKD3NldHRpbmdzX3VwZGF0ZRgqIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNldHRpbmdzVXBkYXRlUmVxdWVzdEgAEkcKEHNraWxsX2ZpbGVzX2xpc3QYKyABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Ta2lsbEZpbGVzTGlzdFJlcXVlc3RIABJNChNza2lsbF9tZXRhZGF0YV9yZWFkGCwgASgLMi4ubGl2ZWFnZW50LmdhdGV3YXkudjIuU2tpbGxNZXRhZGF0YVJlYWRSZXF1ZXN0SAASRQoPc2tpbGxfdGV4dF9yZWFkGC0gASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuU2tpbGxUZXh0UmVhZFJlcXVlc3RIABJJChFmaWxlX21lbnRpb25fbGlzdBguIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZpbGVNZW50aW9uTGlzdFJlcXVlc3RIABJRChV1cGxvYWRfcmVhZGFibGVfZmlsZXMYLyABKAsyMC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5VcGxvYWRSZWFkYWJsZUZpbGVzUmVxdWVzdEgAEjgKCGZzX3Jvb3RzGDAgASgLMiQubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNSb290c1JlcXVlc3RIABI/Cgxmc19saXN0X2RpcnMYMSABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc0xpc3REaXJzUmVxdWVzdEgAEjEKBHBpbmcYMiABKAsyIS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5QaW5nUmVxdWVzdEgAElMKFnVwbG9hZGVkX2ltYWdlX3ByZXZpZXcYMyABKAsyMS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5VcGxvYWRlZEltYWdlUHJldmlld1JlcXVlc3RIABJCCg1tZW1vcnlfbWFuYWdlGDQgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuTWVtb3J5TWFuYWdlUmVxdWVzdEgAEkAKDHNraWxsX21hbmFnZRg1IAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNraWxsTWFuYWdlUmVxdWVzdEgAElYKGGZzX2NyZWF0ZV9wcm9qZWN0X2ZvbGRlchg2IAEoCzIyLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzQ3JlYXRlUHJvamVjdEZvbGRlclJlcXVlc3RIABJBChB0ZXJtaW5hbF9yZXF1ZXN0GDcgASgLMiUubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxSZXF1ZXN0SAASNgoHZnNfbGlzdBg4IAEoCzIjLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzTGlzdFJlcXVlc3RIABJBCg1mc193cml0ZV90ZXh0GDkgASgLMigubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNXcml0ZVRleHRSZXF1ZXN0SAASQQoNZnNfY3JlYXRlX2Rpchg6IAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzQ3JlYXRlRGlyUmVxdWVzdEgAEjoKCWZzX3JlbmFtZRg7IAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzUmVuYW1lUmVxdWVzdEgAEjoKCWZzX2RlbGV0ZRg8IAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzRGVsZXRlUmVxdWVzdEgAEjcKC2dpdF9yZXF1ZXN0GD0gASgLMiAubGl2ZWFnZW50LmdhdGV3YXkudjIuR2l0UmVxdWVzdEgAElAKFWZzX3JlYWRfZWRpdGFibGVfdGV4dBg+IAEoCzIvLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzUmVhZEVkaXRhYmxlVGV4dFJlcXVlc3RIABJUChdmc19yZWFkX3dvcmtzcGFjZV9pbWFnZRg/IAEoCzIxLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzUmVhZFdvcmtzcGFjZUltYWdlUmVxdWVzdEgAEjkKDHNmdHBfcmVxdWVzdBhAIAEoCzIhLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNmdHBSZXF1ZXN0SAASRgoPcHJvdmlkZXJfbW9kZWxzGEEgASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuUHJvdmlkZXJNb2RlbHNSZXF1ZXN0SAASXwodc2V0dGluZ3NfcmVzZXRfc3NoX2tub3duX2hvc3QYSCABKAsyNi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZXR0aW5nc1Jlc2V0U3NoS25vd25Ib3N0UmVxdWVzdEgAEjwKCmNoYXRfcXVldWUYSSABKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0UXVldWVSZXF1ZXN0SAASQAoQY2hhdF9pbmdyZXNzX2FjaxhLIAEoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRJbmdyZXNzQWNrSAASQQoMdHVubmVsX3N0YXRlGFAgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsU3RhdGVTbmFwc2hvdEgAEj8KD3R1bm5lbF9tdXRhdGlvbhhRIAEoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYyLlR1bm5lbE11dGF0aW9uSAASOQoMdHVubmVsX2ZyYW1lGFIgASgLMiEubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsRnJhbWVIABJGCg93b3Jrc3BhY2Vfd2F0Y2gYWiABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Xb3Jrc3BhY2VXYXRjaFJlcXVlc3RIABJOChdtYW5hZ2VkX3Byb2Nlc3NfcmVxdWVzdBhbIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLk1hbmFnZWRQcm9jZXNzUmVxdWVzdEgAEkQKDmhpc3RvcnlfYnJhbmNoGFwgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeUJyYW5jaFJlcXVlc3RIABJECg5wcm92aWRlcl91c2FnZRhdIAEoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYyLlByb3ZpZGVyVXNhZ2VSZXF1ZXN0SABCCQoHcGF5bG9hZEoECEMQREoECEQQRUoECEUQRkoECEoQSyLPIwoNQWdlbnRFbnZlbG9wZRISCgpyZXF1ZXN0X2lkGAEgASgJEhEKCXRpbWVzdGFtcBgCIAEoAxI1CgpjaGF0X2V2ZW50GAogASgLMh8ubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEV2ZW50SAASRAoQY3Jvbl9tYW5hZ2VfcmVzcBgUIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNyb25NYW5hZ2VSZXNwb25zZUgAEkYKEWhpc3RvcnlfbGlzdF9yZXNwGB4gASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeUxpc3RSZXNwb25zZUgAEkQKEGhpc3RvcnlfZ2V0X3Jlc3AYHyABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5R2V0UmVzcG9uc2VIABJKChNoaXN0b3J5X3JlbmFtZV9yZXNwGCAgASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVJlbmFtZVJlc3BvbnNlSAASSgoTaGlzdG9yeV9kZWxldGVfcmVzcBghIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlEZWxldGVSZXNwb25zZUgAEj4KDGhpc3Rvcnlfc3luYxgiIAEoCzImLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlTeW5jRXZlbnRIABJKChNoaXN0b3J5X3ByZWZpeF9yZXNwGCMgASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVByZWZpeFJlc3BvbnNlSAASRAoQaGlzdG9yeV9waW5fcmVzcBgkIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlQaW5SZXNwb25zZUgAEk8KFmhpc3Rvcnlfc2hhcmVfZ2V0X3Jlc3AYJSABKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5U2hhcmVHZXRSZXNwb25zZUgAEk8KFmhpc3Rvcnlfc2hhcmVfc2V0X3Jlc3AYJiABKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5U2hhcmVTZXRSZXNwb25zZUgAElcKGmhpc3Rvcnlfc2hhcmVfcmVzb2x2ZV9yZXNwGCcgASgLMjEubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVNoYXJlUmVzb2x2ZVJlc3BvbnNlSAASTgoVaGlzdG9yeV93b3JrZGlyc19yZXNwGDggASgLMi0ubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVdvcmtkaXJzUmVzcG9uc2VIABJIChJwcm92aWRlcl9saXN0X3Jlc3AYKCABKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Qcm92aWRlckxpc3RSZXNwb25zZUgAEkYKEXNldHRpbmdzX2dldF9yZXNwGCkgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuU2V0dGluZ3NHZXRSZXNwb25zZUgAEkwKFHNldHRpbmdzX3VwZGF0ZV9yZXNwGCogASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuU2V0dGluZ3NVcGRhdGVSZXNwb25zZUgAEkAKDXNldHRpbmdzX3N5bmMYKyABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZXR0aW5nc1N5bmNFdmVudEgAEk0KFXNraWxsX2ZpbGVzX2xpc3RfcmVzcBgsIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNraWxsRmlsZXNMaXN0UmVzcG9uc2VIABJTChhza2lsbF9tZXRhZGF0YV9yZWFkX3Jlc3AYLSABKAsyLy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Ta2lsbE1ldGFkYXRhUmVhZFJlc3BvbnNlSAASSwoUc2tpbGxfdGV4dF9yZWFkX3Jlc3AYLiABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Ta2lsbFRleHRSZWFkUmVzcG9uc2VIABJPChZmaWxlX21lbnRpb25fbGlzdF9yZXNwGC8gASgLMi0ubGl2ZWFnZW50LmdhdGV3YXkudjIuRmlsZU1lbnRpb25MaXN0UmVzcG9uc2VIABJXChp1cGxvYWRfcmVhZGFibGVfZmlsZXNfcmVzcBgwIAEoCzIxLmxpdmVhZ2VudC5nYXRld2F5LnYyLlVwbG9hZFJlYWRhYmxlRmlsZXNSZXNwb25zZUgAEj4KDWZzX3Jvb3RzX3Jlc3AYMSABKAsyJS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc1Jvb3RzUmVzcG9uc2VIABIyCgRwb25nGDIgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuUG9uZ1Jlc3BvbnNlSAASRQoRZnNfbGlzdF9kaXJzX3Jlc3AYMyABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc0xpc3REaXJzUmVzcG9uc2VIABJZCht1cGxvYWRlZF9pbWFnZV9wcmV2aWV3X3Jlc3AYNCABKAsyMi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5VcGxvYWRlZEltYWdlUHJldmlld1Jlc3BvbnNlSAASSAoSbWVtb3J5X21hbmFnZV9yZXNwGDUgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuTWVtb3J5TWFuYWdlUmVzcG9uc2VIABJGChFza2lsbF9tYW5hZ2VfcmVzcBg2IAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNraWxsTWFuYWdlUmVzcG9uc2VIABJcCh1mc19jcmVhdGVfcHJvamVjdF9mb2xkZXJfcmVzcBg3IAEoCzIzLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzQ3JlYXRlUHJvamVjdEZvbGRlclJlc3BvbnNlSAASQwoRdGVybWluYWxfcmVzcG9uc2UYOSABKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFJlc3BvbnNlSAASPQoOdGVybWluYWxfZXZlbnQYOiABKAsyIy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbEV2ZW50SAASPAoMZnNfbGlzdF9yZXNwGDsgASgLMiQubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNMaXN0UmVzcG9uc2VIABJHChJmc193cml0ZV90ZXh0X3Jlc3AYPCABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc1dyaXRlVGV4dFJlc3BvbnNlSAASRwoSZnNfY3JlYXRlX2Rpcl9yZXNwGD0gASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNDcmVhdGVEaXJSZXNwb25zZUgAEkAKDmZzX3JlbmFtZV9yZXNwGD4gASgLMiYubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNSZW5hbWVSZXNwb25zZUgAEkAKDmZzX2RlbGV0ZV9yZXNwGD8gASgLMiYubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNEZWxldGVSZXNwb25zZUgAEjkKDGdpdF9yZXNwb25zZRhAIAEoCzIhLmxpdmVhZ2VudC5nYXRld2F5LnYyLkdpdFJlc3BvbnNlSAASVgoaZnNfcmVhZF9lZGl0YWJsZV90ZXh0X3Jlc3AYQSABKAsyMC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc1JlYWRFZGl0YWJsZVRleHRSZXNwb25zZUgAEloKHGZzX3JlYWRfd29ya3NwYWNlX2ltYWdlX3Jlc3AYQiABKAsyMi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc1JlYWRXb3Jrc3BhY2VJbWFnZVJlc3BvbnNlSAASOwoNc2Z0cF9yZXNwb25zZRhJIAEoCzIiLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNmdHBSZXNwb25zZUgAEjUKCnNmdHBfZXZlbnQYSiABKAsyHy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZnRwRXZlbnRIABJCCg9jaGF0X3F1ZXVlX3Jlc3AYSyABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0UXVldWVSZXNwb25zZUgAEkAKEGNoYXRfcXVldWVfZXZlbnQYTCABKAsyJC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0UXVldWVFdmVudEgAEj4KDGNoYXRfY29udHJvbBhGIAEoCzImLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRDb250cm9sRXZlbnRIABJCCg5ydW50aW1lX3N0YXR1cxhHIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLlJ1bnRpbWVTdGF0dXNFdmVudEgAEmUKInNldHRpbmdzX3Jlc2V0X3NzaF9rbm93bl9ob3N0X3Jlc3AYSCABKAsyNy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZXR0aW5nc1Jlc2V0U3NoS25vd25Ib3N0UmVzcG9uc2VIABJKChVjaGF0X3J1bnRpbWVfc25hcHNob3QYTSABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0UnVudGltZVNuYXBzaG90SAASTAoUcHJvdmlkZXJfbW9kZWxzX3Jlc3AYTyABKAsyLC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Qcm92aWRlck1vZGVsc1Jlc3BvbnNlSAASQgoOdHVubmVsX2Rlc2lyZWQYUCABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxEZXNpcmVkU3RhdGVIABJMChZ0dW5uZWxfbXV0YXRpb25fcmVzdWx0GFEgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsTXV0YXRpb25SZXN1bHRIABI5Cgx0dW5uZWxfZnJhbWUYUiABKAsyIS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxGcmFtZUgAEkYKE3R1bm5lbF9wcm9iZV9yZXBvcnQYUyABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxQcm9iZVJlcG9ydEgAEkoKEndvcmtzcGFjZV9hY3Rpdml0eRhaIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYyLldvcmtzcGFjZUFjdGl2aXR5RXZlbnRIABJQChhtYW5hZ2VkX3Byb2Nlc3NfcmVzcG9uc2UYWyABKAsyLC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5NYW5hZ2VkUHJvY2Vzc1Jlc3BvbnNlSAASUAoYbWFuYWdlZF9wcm9jZXNzX3NuYXBzaG90GFwgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuTWFuYWdlZFByb2Nlc3NTbmFwc2hvdEgAEkoKE2hpc3RvcnlfYnJhbmNoX3Jlc3AYXSABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5QnJhbmNoUmVzcG9uc2VIABJKChNwcm92aWRlcl91c2FnZV9yZXNwGF4gASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuUHJvdmlkZXJVc2FnZVJlc3BvbnNlSAASRAoSY2hhdF9pbmdyZXNzX2JhdGNoGF8gASgLMiYubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEluZ3Jlc3NCYXRjaEgAEkYKE2NoYXRfaW5ncmVzc19yZXN1bWUYYCABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0SW5ncmVzc1Jlc3VtZUgAEkoKFWNoYXRfaW5ncmVzc19mcmFnbWVudBhhIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRJbmdyZXNzRnJhZ21lbnRIABI0CgVlcnJvchhjIAEoCzIjLmxpdmVhZ2VudC5nYXRld2F5LnYyLkVycm9yUmVzcG9uc2VIAEIJCgdwYXlsb2FkSgQIQxBESgQIRBBFSgQIRRBGSgQIThBPIlUKEUNoYXRTZWxlY3RlZE1vZGVsEhoKEmN1c3RvbV9wcm92aWRlcl9pZBgBIAEoCRINCgVtb2RlbBgCIAEoCRIVCg1wcm92aWRlcl90eXBlGAMgASgJImUKE0NoYXRSdW50aW1lQ29udHJvbHMSGAoQdGhpbmtpbmdfZW5hYmxlZBgBIAEoCBIhChluYXRpdmVfd2ViX3NlYXJjaF9lbmFibGVkGAIgASgIEhEKCXJlYXNvbmluZxgDIAEoCSJ1ChBDaGF0VXBsb2FkZWRGaWxlEhUKDXJlbGF0aXZlX3BhdGgYASABKAkSFQoNYWJzb2x1dGVfcGF0aBgCIAEoCRIRCglmaWxlX25hbWUYAyABKAkSDAoEa2luZBgEIAEoCRISCgpzaXplX2J5dGVzGAUgASgDIksKElVwbG9hZFJlYWRhYmxlRmlsZRIRCglmaWxlX25hbWUYASABKAkSEQoJbWltZV90eXBlGAIgASgJEg8KB2NvbnRlbnQYAyABKAwiZgoaVXBsb2FkUmVhZGFibGVGaWxlc1JlcXVlc3QSDwoHd29ya2RpchgBIAEoCRI3CgVmaWxlcxgCIAMoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLlVwbG9hZFJlYWRhYmxlRmlsZSJlChtVcGxvYWRSZWFkYWJsZUZpbGVzUmVzcG9uc2USNQoFZmlsZXMYASADKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0VXBsb2FkZWRGaWxlEg8KB3NraXBwZWQYAiADKAkiRQobVXBsb2FkZWRJbWFnZVByZXZpZXdSZXF1ZXN0Eg8KB3dvcmtkaXIYASABKAkSFQoNYWJzb2x1dGVfcGF0aBgCIAEoCSI/ChxVcGxvYWRlZEltYWdlUHJldmlld1Jlc3BvbnNlEhEKCW1pbWVfdHlwZRgBIAEoCRIMCgRkYXRhGAIgASgJInsKClR1bm5lbFNwZWMSCgoCaWQYASABKAkSEQoJc2x1Z19oaW50GAIgASgJEgwKBG5hbWUYAyABKAkSEgoKdGFyZ2V0X3VybBgEIAEoCRISCgpleHBpcmVzX2F0GAUgASgDEhgKEHByb2plY3RfcGF0aF9rZXkYBiABKAkiWQoSVHVubmVsRGVzaXJlZFN0YXRlEjEKB3R1bm5lbHMYASADKAsyIC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxTcGVjEhAKCHJldmlzaW9uGAIgASgEImYKDFR1bm5lbEhlYWx0aBIOCgZzdGF0dXMYASABKAkSEwoLaHR0cF9zdGF0dXMYAiABKA0SDQoFZXJyb3IYAyABKAkSEgoKY2hlY2tlZF9hdBgEIAEoAxIOCgZydHRfbXMYBSABKA0i8AEKDFR1bm5lbFN0YXR1cxIKCgJpZBgBIAEoCRIMCgRzbHVnGAIgASgJEgwKBG5hbWUYAyABKAkSEgoKdGFyZ2V0X3VybBgEIAEoCRITCgtwdWJsaWNfcGF0aBgFIAEoCRISCgpjcmVhdGVkX2F0GAYgASgDEhIKCmV4cGlyZXNfYXQYByABKAMSGgoSYWN0aXZlX2Nvbm5lY3Rpb25zGAggASgNEhgKEHByb2plY3RfcGF0aF9rZXkYCSABKAkSMQoFbG9jYWwYCiABKAsyIi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxIZWFsdGgipQEKE1R1bm5lbFN0YXRlU25hcHNob3QSMwoHdHVubmVscxgBIAMoCzIiLmxpdmVhZ2VudC5nYXRld2F5LnYyLlR1bm5lbFN0YXR1cxIQCghyZXZpc2lvbhgCIAEoBBIUCgxhZ2VudF9vbmxpbmUYAyABKAgSMQoFcmVsYXkYBCABKAsyIi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxIZWFsdGgimQEKDlR1bm5lbE11dGF0aW9uEg4KBmFjdGlvbhgBIAEoCRIRCgl0dW5uZWxfaWQYAiABKAkSEgoKdGFyZ2V0X3VybBgDIAEoCRIMCgRuYW1lGAQgASgJEhgKC3R0bF9zZWNvbmRzGAUgASgNSACIAQESGAoQcHJvamVjdF9wYXRoX2tleRgGIAEoCUIOCgxfdHRsX3NlY29uZHMiVAoUVHVubmVsTXV0YXRpb25SZXN1bHQSEQoJdHVubmVsX2lkGAEgASgJEhIKCmVycm9yX2NvZGUYAiABKAkSFQoNZXJyb3JfbWVzc2FnZRgDIAEoCSJZChFUdW5uZWxQcm9iZVJlc3VsdBIRCgl0dW5uZWxfaWQYASABKAkSMQoFbG9jYWwYAiABKAsyIi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxIZWFsdGgiTQoRVHVubmVsUHJvYmVSZXBvcnQSOAoHcmVzdWx0cxgBIAMoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYyLlR1bm5lbFByb2JlUmVzdWx0IisKDFR1bm5lbEhlYWRlchIMCgRuYW1lGAEgASgJEg0KBXZhbHVlGAIgASgJIvUCCgtUdW5uZWxGcmFtZRIRCglzdHJlYW1faWQYASABKAkSMwoEa2luZBgCIAEoDjIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLlR1bm5lbEZyYW1lS2luZBISCgp0YXJnZXRfdXJsGAMgASgJEg4KBm1ldGhvZBgEIAEoCRIMCgRwYXRoGAUgASgJEjMKB2hlYWRlcnMYBiADKAsyIi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxIZWFkZXISDgoGc3RhdHVzGAcgASgNEgwKBGJvZHkYCCABKAwSDQoFZXJyb3IYCSABKAkSQgoPd3NfbWVzc2FnZV90eXBlGAogASgOMikubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsV3NNZXNzYWdlVHlwZRIWCg53c19zdWJwcm90b2NvbBgLIAEoCRIVCg13c19jbG9zZV9jb2RlGAwgASgNEhcKD3dzX2Nsb3NlX3JlYXNvbhgNIAEoCSIpChVXb3Jrc3BhY2VXYXRjaFJlcXVlc3QSEAoId29ya2RpcnMYASADKAkifgoWV29ya3NwYWNlQWN0aXZpdHlFdmVudBIPCgd3b3JrZGlyGAEgASgJEhAKCHJldmlzaW9uGAIgASgEEgoKAmZzGAMgASgIEgsKA2dpdBgEIAEoCBIVCg1jaGFuZ2VkX3BhdGhzGAUgAygJEhEKCXRydW5jYXRlZBgGIAEoCCKWAgoUTWFuYWdlZFByb2Nlc3NSZWNvcmQSCgoCaWQYASABKAkSDQoFbGFiZWwYAiABKAkSDwoHY29tbWFuZBgDIAEoCRILCgNjd2QYBCABKAkSDQoFc2hlbGwYBSABKAkSCwoDcGlkGAYgASgNEhAKCGxvZ19wYXRoGAcgASgJEhIKCnN0YXJ0ZWRfYXQYCCABKAMSGAoLZmluaXNoZWRfYXQYCSABKANIAIgBARIWCglleGl0X2NvZGUYCiABKAVIAYgBARIPCgdydW5uaW5nGAsgASgIEhAKCGlzb2xhdGVkGAwgASgIEhAKCHJlc3RvcmVkGA0gASgIQg4KDF9maW5pc2hlZF9hdEIMCgpfZXhpdF9jb2RlImkKFk1hbmFnZWRQcm9jZXNzU25hcHNob3QSPQoJcHJvY2Vzc2VzGAEgAygLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuTWFuYWdlZFByb2Nlc3NSZWNvcmQSEAoIcmV2aXNpb24YAiABKAQiTgoVTWFuYWdlZFByb2Nlc3NSZXF1ZXN0Eg4KBmFjdGlvbhgBIAEoCRISCgpwcm9jZXNzX2lkGAIgASgJEhEKCW1heF9ieXRlcxgDIAEoDSK3AQoWTWFuYWdlZFByb2Nlc3NSZXNwb25zZRIOCgZhY3Rpb24YASABKAkSPgoIc25hcHNob3QYAiABKAsyLC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5NYW5hZ2VkUHJvY2Vzc1NuYXBzaG90EhMKC2xvZ19jb250ZW50GAMgASgJEhAKCGxvZ19wYXRoGAQgASgJEhUKDWxvZ190cnVuY2F0ZWQYBSABKAgSDwoHc3RvcHBlZBgGIAEoCCI5ChNNZW1vcnlNYW5hZ2VSZXF1ZXN0Eg8KB2NvbW1hbmQYASABKAkSEQoJYXJnc19qc29uGAIgASgJIisKFE1lbW9yeU1hbmFnZVJlc3BvbnNlEhMKC3Jlc3VsdF9qc29uGAEgASgJIpgDCg9UZXJtaW5hbFJlcXVlc3QSDgoGYWN0aW9uGAEgASgJEhIKCnNlc3Npb25faWQYAiABKAkSGAoQcHJvamVjdF9wYXRoX2tleRgDIAEoCRILCgNjd2QYBCABKAkSDQoFc2hlbGwYBSABKAkSDQoFdGl0bGUYBiABKAkSDAoEZGF0YRgHIAEoCRIMCgRjb2xzGAggASgNEgwKBHJvd3MYCSABKA0SEQoJbWF4X2J5dGVzGAogASgNEhMKC3NzaF9ob3N0X2lkGAsgASgJEhEKCXByb21wdF9pZBgMIAEoCRIVCg1wcm9tcHRfYW5zd2VyGA0gASgJEhYKDnRydXN0X2hvc3Rfa2V5GA4gASgIEhQKDHNmdHBfZW5hYmxlZBgPIAEoCBIOCgZ0YWJfaWQYECABKAkSEAoIdGFiX2tpbmQYESABKAkSEwoLcmVtb3RlX2hvc3QYEiABKAkSEwoLcmVtb3RlX3BvcnQYEyABKA0SEgoKbG9jYWxfcG9ydBgUIAEoDRISCgpmb3J3YXJkX2lkGBUgASgJIrICCg9UZXJtaW5hbFNlc3Npb24SCgoCaWQYASABKAkSGAoQcHJvamVjdF9wYXRoX2tleRgCIAEoCRILCgNjd2QYAyABKAkSDQoFc2hlbGwYBCABKAkSDQoFdGl0bGUYBSABKAkSCwoDcGlkGAYgASgNEgwKBGNvbHMYByABKA0SDAoEcm93cxgIIAEoDRISCgpjcmVhdGVkX2F0GAkgASgEEhIKCnVwZGF0ZWRfYXQYCiABKAQSEwoLZmluaXNoZWRfYXQYCyABKAQSEQoJZXhpdF9jb2RlGAwgASgFEg8KB3J1bm5pbmcYDSABKAgSDAoEa2luZBgOIAEoCRI2CgNzc2gYDyABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNzaE1ldGFkYXRhItsBChNUZXJtaW5hbFNzaE1ldGFkYXRhEg8KB2hvc3RfaWQYASABKAkSEQoJaG9zdF9uYW1lGAIgASgJEhAKCHVzZXJuYW1lGAMgASgJEgwKBGhvc3QYBCABKAkSDAoEcG9ydBgFIAEoDRIRCglhdXRoX3R5cGUYBiABKAkSDgoGc3RhdHVzGAcgASgJEhkKEXJlY29ubmVjdF9hdHRlbXB0GAggASgNEh4KFnJlY29ubmVjdF9tYXhfYXR0ZW1wdHMYCSABKA0SFAoMc2Z0cF9lbmFibGVkGAogASgIIvcBCgtTZnRwUmVxdWVzdBIOCgZhY3Rpb24YASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIYChBwcm9qZWN0X3BhdGhfa2V5GAMgASgJEg8KB3dvcmtkaXIYBCABKAkSEgoKbG9jYWxfcGF0aBgFIAEoCRITCgtyZW1vdGVfcGF0aBgGIAEoCRIRCglmcm9tX3BhdGgYByABKAkSDwoHdG9fcGF0aBgIIAEoCRIRCglkaXJlY3Rpb24YCSABKAkSEwoLdGFyZ2V0X3BhdGgYCiABKAkSEQoJcmVjdXJzaXZlGAsgASgIEhEKCW92ZXJ3cml0ZRgMIAEoCCJYCglTZnRwRW50cnkSDAoEcGF0aBgBIAEoCRIMCgRuYW1lGAIgASgJEgwKBGtpbmQYAyABKAkSEgoKc2l6ZV9ieXRlcxgEIAEoBBINCgVtdGltZRgFIAEoBCLyAQoMU2Z0cFRyYW5zZmVyEgoKAmlkGAEgASgJEhIKCnNlc3Npb25faWQYAiABKAkSEQoJZGlyZWN0aW9uGAMgASgJEg4KBnN0YXR1cxgEIAEoCRITCgtzb3VyY2VfcGF0aBgFIAEoCRITCgt0YXJnZXRfcGF0aBgGIAEoCRIUCgxjdXJyZW50X3BhdGgYByABKAkSEgoKYnl0ZXNfZG9uZRgIIAEoBBITCgtieXRlc190b3RhbBgJIAEoBBISCgpmaWxlc19kb25lGAogASgNEhMKC2ZpbGVzX3RvdGFsGAsgASgNEg0KBWVycm9yGAwgASgJItQBCgxTZnRwUmVzcG9uc2USDgoGYWN0aW9uGAEgASgJEgwKBHBhdGgYAiABKAkSMAoHZW50cmllcxgDIAMoCzIfLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNmdHBFbnRyeRIuCgVlbnRyeRgEIAEoCzIfLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNmdHBFbnRyeRIOCgZleGlzdHMYBSABKAgSNAoIdHJhbnNmZXIYBiABKAsyIi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZnRwVHJhbnNmZXIiTwoJU2Z0cEV2ZW50EgwKBGtpbmQYASABKAkSNAoIdHJhbnNmZXIYAiABKAsyIi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZnRwVHJhbnNmZXIiwQEKEVRlcm1pbmFsU3NoUHJvbXB0EgoKAmlkGAEgASgJEgwKBGtpbmQYAiABKAkSDwoHaG9zdF9pZBgDIAEoCRIRCglob3N0X25hbWUYBCABKAkSDAoEaG9zdBgFIAEoCRIMCgRwb3J0GAYgASgNEg8KB21lc3NhZ2UYByABKAkSGgoSZmluZ2VycHJpbnRfc2hhMjU2GAggASgJEhAKCGtleV90eXBlGAkgASgJEhMKC2Fuc3dlcl9lY2hvGAogASgIIkEKE1Rlcm1pbmFsU2hlbGxPcHRpb24SCgoCaWQYASABKAkSDQoFbGFiZWwYAiABKAkSDwoHY29tbWFuZBgDIAEoCSKAAQoOVGVybWluYWxTc2hUYWISCgoCaWQYASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIYChBwcm9qZWN0X3BhdGhfa2V5GAMgASgJEgwKBGtpbmQYBCABKAkSEgoKY3JlYXRlZF9hdBgFIAEoBBISCgp1cGRhdGVkX2F0GAYgASgEIn8KF1Rlcm1pbmFsU3NoVGFic1NuYXBzaG90EhgKEHByb2plY3RfcGF0aF9rZXkYASABKAkSMgoEdGFicxgCIAMoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU3NoVGFiEhAKCHJldmlzaW9uGAQgASgESgQIAxAEIv0BChdUZXJtaW5hbFNzaExvY2FsRm9yd2FyZBIKCgJpZBgBIAEoCRISCgpzZXNzaW9uX2lkGAIgASgJEhgKEHByb2plY3RfcGF0aF9rZXkYAyABKAkSEgoKbG9jYWxfaG9zdBgEIAEoCRISCgpsb2NhbF9wb3J0GAUgASgNEg8KB2FkZHJlc3MYBiABKAkSEwoLcmVtb3RlX2hvc3QYByABKAkSEwoLcmVtb3RlX3BvcnQYCCABKA0SDgoGc3RhdHVzGAkgASgJEhIKCmNyZWF0ZWRfYXQYCiABKAQSEgoKdXBkYXRlZF9hdBgLIAEoBBINCgVlcnJvchgMIAEoCSJ1CiBUZXJtaW5hbFNzaExvY2FsRm9yd2FyZHNTbmFwc2hvdBI/Cghmb3J3YXJkcxgBIAMoCzItLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU3NoTG9jYWxGb3J3YXJkEhAKCHJldmlzaW9uGAIgASgEIn8KHVRlcm1pbmFsU3NoTG9jYWxGb3J3YXJkQWN0aW9uEj4KB2ZvcndhcmQYASABKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNzaExvY2FsRm9yd2FyZBIQCghyZXZpc2lvbhgCIAEoBBIMCgRraW5kGAMgASgJIqcFChBUZXJtaW5hbFJlc3BvbnNlEg4KBmFjdGlvbhgBIAEoCRI3CghzZXNzaW9ucxgCIAMoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU2Vzc2lvbhI2CgdzZXNzaW9uGAMgASgLMiUubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTZXNzaW9uEg4KBm91dHB1dBgEIAEoDBIRCgl0cnVuY2F0ZWQYBSABKAgSQAoNc2hlbGxfb3B0aW9ucxgGIAMoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU2hlbGxPcHRpb24SFQoNZGVmYXVsdF9zaGVsbBgHIAEoCRIbChNvdXRwdXRfc3RhcnRfb2Zmc2V0GAggASgEEhkKEW91dHB1dF9lbmRfb2Zmc2V0GAkgASgEEjsKCnNzaF9wcm9tcHQYCiABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNzaFByb21wdBISCgpsYXRlbmN5X21zGAsgASgNEj8KCHNzaF90YWJzGAwgASgLMi0ubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTc2hUYWJzU25hcHNob3QSUgoSc3NoX2xvY2FsX2ZvcndhcmRzGA0gASgLMjYubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTc2hMb2NhbEZvcndhcmRzU25hcHNob3QSTgoRc3NoX2xvY2FsX2ZvcndhcmQYDiABKAsyMy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNzaExvY2FsRm9yd2FyZEFjdGlvbhIoCiBzc2hfbG9jYWxfZm9yd2FyZF9wb3J0X2F2YWlsYWJsZRgPIAEoCCLaAgoNVGVybWluYWxFdmVudBIMCgRraW5kGAEgASgJEhIKCnNlc3Npb25faWQYAiABKAkSGAoQcHJvamVjdF9wYXRoX2tleRgDIAEoCRI2CgdzZXNzaW9uGAQgASgLMiUubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTZXNzaW9uEgwKBGRhdGEYBSABKAwSGwoTb3V0cHV0X3N0YXJ0X29mZnNldBgGIAEoBBIZChFvdXRwdXRfZW5kX29mZnNldBgHIAEoBBI/Cghzc2hfdGFicxgIIAEoCzItLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU3NoVGFic1NuYXBzaG90Ek4KEXNzaF9sb2NhbF9mb3J3YXJkGAkgASgLMjMubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTc2hMb2NhbEZvcndhcmRBY3Rpb24isgIKE1Rlcm1pbmFsU3RyZWFtRnJhbWUSDAoEa2luZBgBIAEoCRIRCglzdHJlYW1faWQYAiABKAkSEgoKc2Vzc2lvbl9pZBgDIAEoCRIYChBwcm9qZWN0X3BhdGhfa2V5GAQgASgJEgsKA3NlcRgFIAEoBBIUCgxzdGFydF9vZmZzZXQYBiABKAQSEgoKZW5kX29mZnNldBgHIAEoBBIMCgRjb2xzGAggASgNEgwKBHJvd3MYCSABKA0SEQoJbWF4X2J5dGVzGAogASgNEhEKCXRydW5jYXRlZBgLIAEoCBINCgVlcnJvchgMIAEoCRI2CgdzZXNzaW9uGA0gASgLMiUubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTZXNzaW9uEgwKBGRhdGEYDiABKAwiQAoKR2l0UmVxdWVzdBIOCgZhY3Rpb24YASABKAkSDwoHd29ya2RpchgCIAEoCRIRCglhcmdzX2pzb24YAyABKAkiMgoLR2l0UmVzcG9uc2USDgoGYWN0aW9uGAEgASgJEhMKC3Jlc3VsdF9qc29uGAIgASgJIvYCCgtDaGF0UmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDwoHbWVzc2FnZRgCIAEoCRI/Cg5zZWxlY3RlZF9tb2RlbBgDIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRTZWxlY3RlZE1vZGVsEhYKDmV4ZWN1dGlvbl9tb2RlGAQgASgJEg8KB3dvcmtkaXIYBSABKAkSHQoVc2VsZWN0ZWRfc3lzdGVtX3Rvb2xzGAYgAygJEj4KDnVwbG9hZGVkX2ZpbGVzGAcgAygLMiYubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdFVwbG9hZGVkRmlsZRIZChFjbGllbnRfcmVxdWVzdF9pZBgIIAEoCRJDChBydW50aW1lX2NvbnRyb2xzGAkgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdFJ1bnRpbWVDb250cm9scxIUCgxxdWV1ZV9wb2xpY3kYCiABKAkiigEKDkNoYXRNZXNzYWdlUmVmEhUKDXNlZ21lbnRfaW5kZXgYASABKAUSFQoNbWVzc2FnZV9pbmRleBgCIAEoBRISCgpzZWdtZW50X2lkGAMgASgJEhIKCm1lc3NhZ2VfaWQYBCABKAkSDAoEcm9sZRgFIAEoCRIUCgxjb250ZW50X2hhc2gYBiABKAkiPAoRQ2FuY2VsQ2hhdFJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEg4KBnJ1bl9pZBgCIAEoCSLPAQoSQ2hhdENvbW1hbmRSZXF1ZXN0EgwKBHR5cGUYASABKAkSMgoHcmVxdWVzdBgCIAEoCzIhLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRSZXF1ZXN0Ej4KEGJhc2VfbWVzc2FnZV9yZWYYAyABKAsyJC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0TWVzc2FnZVJlZhI3CgZjYW5jZWwYBCABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DYW5jZWxDaGF0UmVxdWVzdCK4AQoQQ2hhdFF1ZXVlUmVxdWVzdBIOCgZhY3Rpb24YASABKAkSFwoPY29udmVyc2F0aW9uX2lkGAIgASgJEg8KB2l0ZW1faWQYAyABKAkSEQoJZGlyZWN0aW9uGAQgASgJEhAKCHJldmlzaW9uGAUgASgEEhIKCmRyYWZ0X2pzb24YBiABKAkSGwoTdXBsb2FkZWRfZmlsZXNfanNvbhgHIAEoCRIUCgxyZXF1ZXN0X2pzb24YCCABKAkihgEKEUNoYXRRdWV1ZVJlc3BvbnNlEhAKCGFjY2VwdGVkGAEgASgIEg8KB21lc3NhZ2UYAiABKAkSFQoNc25hcHNob3RfanNvbhgDIAEoCRIRCglpdGVtX2pzb24YBCABKAkSEgoKZXJyb3JfY29kZRgFIAEoCRIQCghyZXZpc2lvbhgGIAEoBCJSCg5DaGF0UXVldWVFdmVudBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSFQoNc25hcHNob3RfanNvbhgCIAEoCRIQCghyZXZpc2lvbhgDIAEoBCKFAgoJQ2hhdEV2ZW50EjsKBHR5cGUYASABKA4yLS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0RXZlbnQuQ2hhdEV2ZW50VHlwZRIXCg9jb252ZXJzYXRpb25faWQYAiABKAkSDAoEZGF0YRgDIAEoCSKTAQoNQ2hhdEV2ZW50VHlwZRIJCgVUT0tFThAAEgwKCFRISU5LSU5HEAESDQoJVE9PTF9DQUxMEAISDwoLVE9PTF9SRVNVTFQQAxIICgRET05FEAQSCQoFRVJST1IQBRIPCgtUT09MX1NUQVRVUxAGEhEKDUhPU1RFRF9TRUFSQ0gQBxIQCgxVU0VSX01FU1NBR0UQCCK8AQoQQ2hhdENvbnRyb2xFdmVudBISCgpyZXF1ZXN0X2lkGAEgASgJEhkKEWNsaWVudF9yZXF1ZXN0X2lkGAIgASgJEhcKD2NvbnZlcnNhdGlvbl9pZBgDIAEoCRIRCglydW5fZXBvY2gYBCABKAMSDAoEdHlwZRgFIAEoCRINCgVzdGF0ZRgGIAEoCRISCgplcnJvcl9jb2RlGAcgASgJEg8KB21lc3NhZ2UYCCABKAkSCwoDc2VxGAkgASgDIvwBChNDaGF0UnVudGltZVNuYXBzaG90EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIOCgZydW5faWQYAiABKAkSGQoRY2xpZW50X3JlcXVlc3RfaWQYAyABKAkSEQoJd29ya2VyX2lkGAQgASgJEg0KBXN0YXRlGAUgASgJEgsKA2N3ZBgGIAEoCRISCgp1cGRhdGVkX2F0GAcgASgDEhAKCHJldmlzaW9uGAggASgDEhQKDGVudHJpZXNfanNvbhgJIAEoCRITCgt0b29sX3N0YXR1cxgKIAEoCRIhChl0b29sX3N0YXR1c19pc19jb21wYWN0aW9uGAsgASgIIuoBChJSdW50aW1lU3RhdHVzRXZlbnQSEQoJd29ya2VyX2lkGAEgASgJEg0KBXN0YXRlGAIgASgJEg8KB3Zpc2libGUYAyABKAgSGAoQYWN0aXZlX3J1bl9jb3VudBgEIAEoDRIRCgl0aW1lc3RhbXAYBSABKAMSOAoLYWN0aXZlX3J1bnMYBiADKAsyIy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0UnVuUmVwb3J0EjoKDWZpbmlzaGVkX3J1bnMYByADKAsyIy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0UnVuUmVwb3J0IoABCg1DaGF0UnVuUmVwb3J0Eg4KBnJ1bl9pZBgBIAEoCRIXCg9jb252ZXJzYXRpb25faWQYAiABKAkSDQoFc3RhdGUYAyABKAkSEgoKZXJyb3JfY29kZRgEIAEoCRIPCgdtZXNzYWdlGAUgASgJEhIKCnVwZGF0ZWRfYXQYBiABKAMiRwoRQ3Jvbk1hbmFnZVJlcXVlc3QSDgoGYWN0aW9uGAEgASgJEg8KB3Rhc2tfaWQYAiABKAkSEQoJdGFza19qc29uGAMgASgJIjkKEkNyb25NYW5hZ2VSZXNwb25zZRIOCgZhY3Rpb24YASABKAkSEwoLcmVzdWx0X2pzb24YAiABKAkiVQoSSGlzdG9yeUxpc3RSZXF1ZXN0EgwKBHBhZ2UYASABKAUSEQoJcGFnZV9zaXplGAIgASgFEgsKA2N3ZBgDIAEoCRIRCgljd2RfZW1wdHkYBCABKAgibAoTSGlzdG9yeUxpc3RSZXNwb25zZRJACg1jb252ZXJzYXRpb25zGAEgAygLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ29udmVyc2F0aW9uU3VtbWFyeRITCgt0b3RhbF9jb3VudBgCIAEoBSKKAgoTQ29udmVyc2F0aW9uU3VtbWFyeRIKCgJpZBgBIAEoCRINCgV0aXRsZRgCIAEoCRISCgpjcmVhdGVkX2F0GAMgASgDEhIKCnVwZGF0ZWRfYXQYBCABKAMSFQoNbWVzc2FnZV9jb3VudBgFIAEoBRITCgtwcm92aWRlcl9pZBgGIAEoCRINCgVtb2RlbBgHIAEoCRISCgpzZXNzaW9uX2lkGAggASgJEgsKA2N3ZBgJIAEoCRIRCglpc19waW5uZWQYCiABKAgSEQoJcGlubmVkX2F0GAsgASgDEhEKCWlzX3NoYXJlZBgMIAEoCBIbChNzZWxlY3RlZF9tb2RlbF9qc29uGA0gASgJIkIKEUhpc3RvcnlHZXRSZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIUCgxtYXhfbWVzc2FnZXMYAiABKAUi1AEKEkhpc3RvcnlHZXRSZXNwb25zZRIXCg9jb252ZXJzYXRpb25faWQYASABKAkSFQoNbWVzc2FnZXNfanNvbhgCIAEoCRIbChN0b3RhbF9tZXNzYWdlX2NvdW50GAMgASgFEh4KFnJldHVybmVkX21lc3NhZ2VfY291bnQYBCABKAUSEAoIaGFzX21vcmUYBSABKAgSPwoMY29udmVyc2F0aW9uGAYgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ29udmVyc2F0aW9uU3VtbWFyeSKFAQoUSGlzdG9yeVByZWZpeFJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEhQKDG1heF9tZXNzYWdlcxgCIAEoBRI+ChBiYXNlX21lc3NhZ2VfcmVmGAMgASgLMiQubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdE1lc3NhZ2VSZWYi1wEKFUhpc3RvcnlQcmVmaXhSZXNwb25zZRIXCg9jb252ZXJzYXRpb25faWQYASABKAkSFQoNbWVzc2FnZXNfanNvbhgCIAEoCRIbChN0b3RhbF9tZXNzYWdlX2NvdW50GAMgASgFEh4KFnJldHVybmVkX21lc3NhZ2VfY291bnQYBCABKAUSEAoIaGFzX21vcmUYBSABKAgSPwoMY29udmVyc2F0aW9uGAYgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ29udmVyc2F0aW9uU3VtbWFyeSI+ChRIaXN0b3J5UmVuYW1lUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDQoFdGl0bGUYAiABKAkiWAoVSGlzdG9yeVJlbmFtZVJlc3BvbnNlEj8KDGNvbnZlcnNhdGlvbhgBIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNvbnZlcnNhdGlvblN1bW1hcnkibwoUSGlzdG9yeUJyYW5jaFJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEj4KEGJhc2VfbWVzc2FnZV9yZWYYAiABKAsyJC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0TWVzc2FnZVJlZiJYChVIaXN0b3J5QnJhbmNoUmVzcG9uc2USPwoMY29udmVyc2F0aW9uGAEgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ29udmVyc2F0aW9uU3VtbWFyeSI/ChFIaXN0b3J5UGluUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSEQoJaXNfcGlubmVkGAIgASgIIlUKEkhpc3RvcnlQaW5SZXNwb25zZRI/Cgxjb252ZXJzYXRpb24YASABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Db252ZXJzYXRpb25TdW1tYXJ5IpIBChJIaXN0b3J5U2hhcmVTdGF0dXMSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEg8KB2VuYWJsZWQYAiABKAgSDQoFdG9rZW4YAyABKAkSEgoKY3JlYXRlZF9hdBgEIAEoAxISCgp1cGRhdGVkX2F0GAUgASgDEhsKE3JlZGFjdF90b29sX2NvbnRlbnQYBiABKAgiMQoWSGlzdG9yeVNoYXJlR2V0UmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkiUgoXSGlzdG9yeVNoYXJlR2V0UmVzcG9uc2USNwoFc2hhcmUYASABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5U2hhcmVTdGF0dXMifAoWSGlzdG9yeVNoYXJlU2V0UmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDwoHZW5hYmxlZBgCIAEoCBIgChNyZWRhY3RfdG9vbF9jb250ZW50GAMgASgISACIAQFCFgoUX3JlZGFjdF90b29sX2NvbnRlbnQiUgoXSGlzdG9yeVNoYXJlU2V0UmVzcG9uc2USNwoFc2hhcmUYASABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5U2hhcmVTdGF0dXMiKwoaSGlzdG9yeVNoYXJlUmVzb2x2ZVJlcXVlc3QSDQoFdG9rZW4YASABKAkiyAEKG0hpc3RvcnlTaGFyZVJlc29sdmVSZXNwb25zZRIXCg9jb252ZXJzYXRpb25faWQYASABKAkSFQoNbWVzc2FnZXNfanNvbhgCIAEoCRIbChN0b3RhbF9tZXNzYWdlX2NvdW50GAMgASgFEj8KDGNvbnZlcnNhdGlvbhgEIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNvbnZlcnNhdGlvblN1bW1hcnkSGwoTcmVkYWN0X3Rvb2xfY29udGVudBgFIAEoCCIYChZIaXN0b3J5V29ya2RpcnNSZXF1ZXN0IlUKFUhpc3RvcnlXb3JrZGlyU3VtbWFyeRIMCgRwYXRoGAEgASgJEhoKEmNvbnZlcnNhdGlvbl9jb3VudBgCIAEoBRISCgp1cGRhdGVkX2F0GAMgASgDIlgKF0hpc3RvcnlXb3JrZGlyc1Jlc3BvbnNlEj0KCHdvcmtkaXJzGAEgAygLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVdvcmtkaXJTdW1tYXJ5Ii8KFEhpc3RvcnlEZWxldGVSZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCSIXChVIaXN0b3J5RGVsZXRlUmVzcG9uc2UiegoQSGlzdG9yeVN5bmNFdmVudBIMCgRraW5kGAEgASgJEj8KDGNvbnZlcnNhdGlvbhgCIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNvbnZlcnNhdGlvblN1bW1hcnkSFwoPY29udmVyc2F0aW9uX2lkGAMgASgJIhUKE1Byb3ZpZGVyTGlzdFJlcXVlc3QiLgoUUHJvdmlkZXJMaXN0UmVzcG9uc2USFgoOcHJvdmlkZXJzX2pzb24YASABKAkiFAoSU2V0dGluZ3NHZXRSZXF1ZXN0IiwKE1NldHRpbmdzR2V0UmVzcG9uc2USFQoNc2V0dGluZ3NfanNvbhgBIAEoCSIuChVTZXR0aW5nc1VwZGF0ZVJlcXVlc3QSFQoNc2V0dGluZ3NfanNvbhgBIAEoCSI7ChZTZXR0aW5nc1VwZGF0ZVJlc3BvbnNlEhAKCGFjY2VwdGVkGAEgASgIEg8KB21lc3NhZ2UYAiABKAkiPgogU2V0dGluZ3NSZXNldFNzaEtub3duSG9zdFJlcXVlc3QSDAoEaG9zdBgBIAEoCRIMCgRwb3J0GAIgASgNIjQKIVNldHRpbmdzUmVzZXRTc2hLbm93bkhvc3RSZXNwb25zZRIPCgdkZWxldGVkGAEgASgNIioKEVNldHRpbmdzU3luY0V2ZW50EhUKDXNldHRpbmdzX2pzb24YASABKAkiFwoVU2tpbGxGaWxlc0xpc3RSZXF1ZXN0IkwKFlNraWxsRmlsZXNMaXN0UmVzcG9uc2USEAoIcm9vdF9kaXIYASABKAkSDQoFcGF0aHMYAiADKAkSEQoJdHJ1bmNhdGVkGAMgASgIIigKGFNraWxsTWV0YWRhdGFSZWFkUmVxdWVzdBIMCgRwYXRoGAEgASgJIj4KGVNraWxsTWV0YWRhdGFSZWFkUmVzcG9uc2USDAoEbmFtZRgBIAEoCRITCgtkZXNjcmlwdGlvbhgCIAEoCSJEChRTa2lsbFRleHRSZWFkUmVxdWVzdBIMCgRwYXRoGAEgASgJEg4KBm9mZnNldBgCIAEoDRIOCgZsZW5ndGgYAyABKA0iOwoVU2tpbGxUZXh0UmVhZFJlc3BvbnNlEg8KB2NvbnRlbnQYASABKAkSEQoJdHJ1bmNhdGVkGAIgASgIIioKElNraWxsTWFuYWdlUmVxdWVzdBIUCgxwYXlsb2FkX2pzb24YASABKAkiKgoTU2tpbGxNYW5hZ2VSZXNwb25zZRITCgtyZXN1bHRfanNvbhgBIAEoCSJ3ChZGaWxlTWVudGlvbkxpc3RSZXF1ZXN0Eg8KB3dvcmtkaXIYASABKAkSEwoLbWF4X3Jlc3VsdHMYAiABKA0SDQoFcXVlcnkYAyABKAkSGAoLc2hvd19oaWRkZW4YBCABKAhIAIgBAUIOCgxfc2hvd19oaWRkZW4iPgoQRmlsZU1lbnRpb25FbnRyeRIMCgRwYXRoGAEgASgJEgwKBGtpbmQYAiABKAkSDgoGaGlkZGVuGAMgASgIImUKF0ZpbGVNZW50aW9uTGlzdFJlc3BvbnNlEjcKB2VudHJpZXMYASADKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5GaWxlTWVudGlvbkVudHJ5EhEKCXRydW5jYXRlZBgCIAEoCCI/CgZGc1Jvb3QSCgoCaWQYASABKAkSDAoEcGF0aBgCIAEoCRIMCgRraW5kGAMgASgJEg0KBWxhYmVsGAQgASgJIhAKDkZzUm9vdHNSZXF1ZXN0Ij4KD0ZzUm9vdHNSZXNwb25zZRIrCgVyb290cxgBIAMoCzIcLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzUm9vdCI2ChFGc0xpc3REaXJzUmVxdWVzdBIMCgRwYXRoGAEgASgJEhMKC21heF9yZXN1bHRzGAIgASgNIigKCkZzRGlyRW50cnkSDAoEcGF0aBgBIAEoCRIMCgRuYW1lGAIgASgJImgKEkZzTGlzdERpcnNSZXNwb25zZRIMCgRwYXRoGAEgASgJEjEKB2VudHJpZXMYAiADKAsyIC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc0RpckVudHJ5EhEKCXRydW5jYXRlZBgDIAEoCCI8ChxGc0NyZWF0ZVByb2plY3RGb2xkZXJSZXF1ZXN0Eg4KBnBhcmVudBgBIAEoCRIMCgRuYW1lGAIgASgJIi0KHUZzQ3JlYXRlUHJvamVjdEZvbGRlclJlc3BvbnNlEgwKBHBhdGgYASABKAkijAEKDUZzTGlzdFJlcXVlc3QSDwoHd29ya2RpchgBIAEoCRIMCgRwYXRoGAIgASgJEg0KBWRlcHRoGAMgASgNEg4KBm9mZnNldBgEIAEoDRITCgttYXhfcmVzdWx0cxgFIAEoDRIYCgtzaG93X2hpZGRlbhgGIAEoCEgAiAEBQg4KDF9zaG93X2hpZGRlbiI5CgtGc0xpc3RFbnRyeRIMCgRwYXRoGAEgASgJEgwKBGtpbmQYAiABKAkSDgoGaGlkZGVuGAMgASgIIrkBCg5Gc0xpc3RSZXNwb25zZRIMCgRwYXRoGAEgASgJEhAKCGhhc19wYXRoGAIgASgIEg0KBWRlcHRoGAMgASgNEg4KBm9mZnNldBgEIAEoDRITCgttYXhfcmVzdWx0cxgFIAEoDRINCgV0b3RhbBgGIAEoDRIQCghoYXNfbW9yZRgHIAEoCBIyCgdlbnRyaWVzGAggAygLMiEubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNMaXN0RW50cnkiOgoZRnNSZWFkRWRpdGFibGVUZXh0UmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEgwKBHBhdGgYAiABKAkijAEKGkZzUmVhZEVkaXRhYmxlVGV4dFJlc3BvbnNlEgwKBHBhdGgYASABKAkSDwoHY29udGVudBgCIAEoCRIQCghtdGltZV9tcxgDIAEoBBIUCgxjb250ZW50X2hhc2gYBCABKAkSEgoKc2l6ZV9ieXRlcxgFIAEoBBITCgt0b3RhbF9saW5lcxgGIAEoBCI8ChtGc1JlYWRXb3Jrc3BhY2VJbWFnZVJlcXVlc3QSDwoHd29ya2RpchgBIAEoCRIMCgRwYXRoGAIgASgJIokBChxGc1JlYWRXb3Jrc3BhY2VJbWFnZVJlc3BvbnNlEgwKBHBhdGgYASABKAkSEQoJbWltZV90eXBlGAIgASgJEgwKBGRhdGEYAyABKAkSEgoKc2l6ZV9ieXRlcxgEIAEoBBIQCghtdGltZV9tcxgFIAEoBBIUCgxjb250ZW50X2hhc2gYBiABKAkizgEKEkZzV3JpdGVUZXh0UmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEgwKBHBhdGgYAiABKAkSDwoHY29udGVudBgDIAEoCRIMCgRtb2RlGAQgASgJEhkKEWV4cGVjdGVkX210aW1lX21zGAUgASgEEh0KFWV4cGVjdGVkX2NvbnRlbnRfaGFzaBgGIAEoCRIdChVoYXNfZXhwZWN0ZWRfbXRpbWVfbXMYByABKAgSIQoZaGFzX2V4cGVjdGVkX2NvbnRlbnRfaGFzaBgIIAEoCCKdAQoTRnNXcml0ZVRleHRSZXNwb25zZRIMCgRwYXRoGAEgASgJEgwKBG1vZGUYAiABKAkSFgoOZXhpc3RlZF9iZWZvcmUYAyABKAgSFQoNYnl0ZXNfd3JpdHRlbhgEIAEoBBIQCghtdGltZV9tcxgFIAEoBBIUCgxjb250ZW50X2hhc2gYBiABKAkSEwoLdG90YWxfbGluZXMYByABKAQiMwoSRnNDcmVhdGVEaXJSZXF1ZXN0Eg8KB3dvcmtkaXIYASABKAkSDAoEcGF0aBgCIAEoCSIxChNGc0NyZWF0ZURpclJlc3BvbnNlEgwKBHBhdGgYASABKAkSDAoEa2luZBgCIAEoCSJGCg9Gc1JlbmFtZVJlcXVlc3QSDwoHd29ya2RpchgBIAEoCRIRCglmcm9tX3BhdGgYAiABKAkSDwoHdG9fcGF0aBgDIAEoCSJBChBGc1JlbmFtZVJlc3BvbnNlEhEKCWZyb21fcGF0aBgBIAEoCRIMCgRwYXRoGAIgASgJEgwKBGtpbmQYAyABKAkiMAoPRnNEZWxldGVSZXF1ZXN0Eg8KB3dvcmtkaXIYASABKAkSDAoEcGF0aBgCIAEoCSIuChBGc0RlbGV0ZVJlc3BvbnNlEgwKBHBhdGgYASABKAkSDAoEa2luZBgCIAEoCSIgCgtQaW5nUmVxdWVzdBIRCgl0aW1lc3RhbXAYASABKAMiIQoMUG9uZ1Jlc3BvbnNlEhEKCXRpbWVzdGFtcBgBIAEoAyIuCg1FcnJvclJlc3BvbnNlEgwKBGNvZGUYASABKAUSDwoHbWVzc2FnZRgCIAEoCSJrChVQcm92aWRlck1vZGVsc1JlcXVlc3QSFQoNcHJvdmlkZXJfdHlwZRgBIAEoCRIQCghiYXNlX3VybBgCIAEoCRIPCgdhcGlfa2V5GAMgASgJEhgKEHVzZV9zeXN0ZW1fcHJveHkYBCABKAgiLQoWUHJvdmlkZXJNb2RlbHNSZXNwb25zZRITCgttb2RlbHNfanNvbhgBIAEoCSJRChRQcm92aWRlclVzYWdlUmVxdWVzdBITCgtwcm92aWRlcl9pZBgBIAEoCRIPCgdyZWZyZXNoGAIgASgIEhMKC2NvbmZpZ19qc29uGAMgASgJIiwKFVByb3ZpZGVyVXNhZ2VSZXNwb25zZRITCgtyZXN1bHRfanNvbhgBIAEoCSKIAQoQQ2hhdEluZ3Jlc3NCYXRjaBIOCgZydW5faWQYASABKAkSFwoPY29udmVyc2F0aW9uX2lkGAIgASgJEhEKCWZpcnN0X3NlcRgDIAEoBBI4CgdyZWNvcmRzGAQgAygLMicubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEluZ3Jlc3NSZWNvcmQimgIKEUNoYXRJbmdyZXNzUmVjb3JkEjcKBWRlbHRhGAEgASgLMiYubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEluZ3Jlc3NEZWx0YUgAEkEKCmNoZWNrcG9pbnQYAiABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0SW5ncmVzc0NoZWNrcG9pbnRIABI9Cgh0ZXJtaW5hbBgDIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRJbmdyZXNzVGVybWluYWxIABI/CgloZWFydGJlYXQYBCABKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0SW5ncmVzc0hlYXJ0YmVhdEgAQgkKB3BheWxvYWQiOQoQQ2hhdEluZ3Jlc3NEZWx0YRISCgpldmVudF9qc29uGAEgASgJEhEKCXdvcmtlcl9pZBgCIAEoCSIqChRDaGF0SW5ncmVzc0hlYXJ0YmVhdBISCgp1cGRhdGVkX2F0GAEgASgDIsQBChVDaGF0SW5ncmVzc0NoZWNrcG9pbnQSGgoSY292ZXJzX3Rocm91Z2hfc2VxGAEgASgEEhAKCHJldmlzaW9uGAIgASgEEh0KFWNvbXByZXNzZWRfcHJvamVjdGlvbhgDIAEoDBIaChJ1bmNvbXByZXNzZWRfYnl0ZXMYBCABKAQSDgoGc2hhMjU2GAUgASgJEhgKEGNvbnRlbnRfY29tcGxldGUYBiABKAgSGAoQaGlzdG9yeV9yZXF1aXJlZBgHIAEoCCL8AQoTQ2hhdEluZ3Jlc3NUZXJtaW5hbBIaChJjb3ZlcnNfdGhyb3VnaF9zZXEYASABKAQSEAoIcmV2aXNpb24YAiABKAQSHQoVY29tcHJlc3NlZF9wcm9qZWN0aW9uGAMgASgMEhoKEnVuY29tcHJlc3NlZF9ieXRlcxgEIAEoBBIOCgZzaGEyNTYYBSABKAkSGAoQY29udGVudF9jb21wbGV0ZRgGIAEoCBIYChBoaXN0b3J5X3JlcXVpcmVkGAcgASgIEg0KBXN0YXRlGAggASgJEhIKCmVycm9yX2NvZGUYCSABKAkSFQoNZXJyb3JfbWVzc2FnZRgKIAEoCSJNChFDaGF0SW5ncmVzc1Jlc3VtZRI4CgRydW5zGAEgAygLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEluZ3Jlc3NSdW5SZXN1bWUi1QEKFENoYXRJbmdyZXNzUnVuUmVzdW1lEg4KBnJ1bl9pZBgBIAEoCRIXCg9jb252ZXJzYXRpb25faWQYAiABKAkSFwoPcmVwbGF5X2Zyb21fc2VxGAMgASgEEhoKEnJlcGxheV90aHJvdWdoX3NlcRgEIAEoBBIQCghuZXh0X3NlcRgFIAEoBBIdChVsYXRlc3RfY2hlY2twb2ludF9zZXEYBiABKAQSFAoMdGVybWluYWxfc2VxGAcgASgEEhgKEHRlcm1pbmFsX3BlbmRpbmcYCCABKAgizgEKE0NoYXRJbmdyZXNzRnJhZ21lbnQSDgoGcnVuX2lkGAEgASgJEhcKD2NvbnZlcnNhdGlvbl9pZBgCIAEoCRISCgpzb3VyY2Vfc2VxGAMgASgEEhYKDmZyYWdtZW50X2luZGV4GAQgASgNEhYKDmZyYWdtZW50X2NvdW50GAUgASgNEhwKFGVuY29kZWRfcmVjb3JkX2NodW5rGAYgASgMEhwKFGVuY29kZWRfcmVjb3JkX2J5dGVzGAcgASgEEg4KBnNoYTI1NhgIIAEoCSLcAgoOQ2hhdEluZ3Jlc3NBY2sSDgoGcnVuX2lkGAEgASgJEhcKD2NvbnZlcnNhdGlvbl9pZBgCIAEoCRIZChFjb21taXR0ZWRfdGhyb3VnaBgDIAEoBBIVCg1leHBlY3RlZF9uZXh0GAQgASgEEjsKBmFjdGlvbhgFIAEoDjIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRJbmdyZXNzQWNrLkFjdGlvbhIaChJ0ZXJtaW5hbF9jb21taXR0ZWQYBiABKAgSEgoKZXJyb3JfY29kZRgHIAEoCRIVCg1lcnJvcl9tZXNzYWdlGAggASgJImsKBkFjdGlvbhIWChJBQ1RJT05fVU5TUEVDSUZJRUQQABIMCghDT05USU5VRRABEhgKFFJFUExBWV9GUk9NX0VYUEVDVEVEEAISEwoPU0VORF9DSEVDS1BPSU5UEAMSDAoIUkVKRUNURUQQBCrGBAoPVHVubmVsRnJhbWVLaW5kEiEKHVRVTk5FTF9GUkFNRV9LSU5EX1VOU1BFQ0lGSUVEEAASKAokVFVOTkVMX0ZSQU1FX0tJTkRfSFRUUF9SRVFVRVNUX1NUQVJUEAESJwojVFVOTkVMX0ZSQU1FX0tJTkRfSFRUUF9SRVFVRVNUX0JPRFkQAhImCiJUVU5ORUxfRlJBTUVfS0lORF9IVFRQX1JFUVVFU1RfRU5EEAMSKQolVFVOTkVMX0ZSQU1FX0tJTkRfSFRUUF9SRVNQT05TRV9TVEFSVBAEEigKJFRVTk5FTF9GUkFNRV9LSU5EX0hUVFBfUkVTUE9OU0VfQk9EWRAFEicKI1RVTk5FTF9GUkFNRV9LSU5EX0hUVFBfUkVTUE9OU0VfRU5EEAYSHQoZVFVOTkVMX0ZSQU1FX0tJTkRfV1NfRElBTBAHEiAKHFRVTk5FTF9GUkFNRV9LSU5EX1dTX0RJQUxfT0sQCBIjCh9UVU5ORUxfRlJBTUVfS0lORF9XU19ESUFMX0VSUk9SEAkSHgoaVFVOTkVMX0ZSQU1FX0tJTkRfV1NfRlJBTUUQChIeChpUVU5ORUxfRlJBTUVfS0lORF9XU19DTE9TRRALEhsKF1RVTk5FTF9GUkFNRV9LSU5EX0VSUk9SEAwSHAoYVFVOTkVMX0ZSQU1FX0tJTkRfQ0FOQ0VMEA0SGgoWVFVOTkVMX0ZSQU1FX0tJTkRfUElORxAOEhoKFlRVTk5FTF9GUkFNRV9LSU5EX1BPTkcQDyqBAQoTVHVubmVsV3NNZXNzYWdlVHlwZRImCiJUVU5ORUxfV1NfTUVTU0FHRV9UWVBFX1VOU1BFQ0lGSUVEEAASHwobVFVOTkVMX1dTX01FU1NBR0VfVFlQRV9URVhUEAESIQodVFVOTkVMX1dTX01FU1NBR0VfVFlQRV9CSU5BUlkQAkJAWj5naXRodWIuY29tL2xpdmVhZ2VudC9hZ2VudC1nYXRld2F5L2ludGVybmFsL3Byb3RvL3YyO2dhdGV3YXl2MmIGcHJvdG8z"); + fileDesc("ChZwcm90by92Mi9nYXRld2F5LnByb3RvEhRsaXZlYWdlbnQuZ2F0ZXdheS52MiK2GwoPR2F0ZXdheUVudmVsb3BlEhIKCnJlcXVlc3RfaWQYASABKAkSEQoJdGltZXN0YW1wGAIgASgDEkAKDGNoYXRfY29tbWFuZBgKIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRDb21tYW5kUmVxdWVzdEgAEj4KC2Nyb25fbWFuYWdlGBQgASgLMicubGl2ZWFnZW50LmdhdGV3YXkudjIuQ3Jvbk1hbmFnZVJlcXVlc3RIABJACgxoaXN0b3J5X2xpc3QYHiABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5TGlzdFJlcXVlc3RIABI+CgtoaXN0b3J5X2dldBgfIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlHZXRSZXF1ZXN0SAASRAoOaGlzdG9yeV9yZW5hbWUYICABKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5UmVuYW1lUmVxdWVzdEgAEkQKDmhpc3RvcnlfZGVsZXRlGCEgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeURlbGV0ZVJlcXVlc3RIABJECg5oaXN0b3J5X3ByZWZpeBgiIAEoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlQcmVmaXhSZXF1ZXN0SAASPgoLaGlzdG9yeV9waW4YIyABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5UGluUmVxdWVzdEgAEkkKEWhpc3Rvcnlfc2hhcmVfZ2V0GCQgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVNoYXJlR2V0UmVxdWVzdEgAEkkKEWhpc3Rvcnlfc2hhcmVfc2V0GCUgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVNoYXJlU2V0UmVxdWVzdEgAElEKFWhpc3Rvcnlfc2hhcmVfcmVzb2x2ZRgmIAEoCzIwLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlTaGFyZVJlc29sdmVSZXF1ZXN0SAASSAoQaGlzdG9yeV93b3JrZGlycxgnIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlXb3JrZGlyc1JlcXVlc3RIABJCCg1wcm92aWRlcl9saXN0GCggASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuUHJvdmlkZXJMaXN0UmVxdWVzdEgAEkAKDHNldHRpbmdzX2dldBgpIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNldHRpbmdzR2V0UmVxdWVzdEgAEkYKD3NldHRpbmdzX3VwZGF0ZRgqIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNldHRpbmdzVXBkYXRlUmVxdWVzdEgAEkcKEHNraWxsX2ZpbGVzX2xpc3QYKyABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Ta2lsbEZpbGVzTGlzdFJlcXVlc3RIABJNChNza2lsbF9tZXRhZGF0YV9yZWFkGCwgASgLMi4ubGl2ZWFnZW50LmdhdGV3YXkudjIuU2tpbGxNZXRhZGF0YVJlYWRSZXF1ZXN0SAASRQoPc2tpbGxfdGV4dF9yZWFkGC0gASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuU2tpbGxUZXh0UmVhZFJlcXVlc3RIABJJChFmaWxlX21lbnRpb25fbGlzdBguIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZpbGVNZW50aW9uTGlzdFJlcXVlc3RIABJRChV1cGxvYWRfcmVhZGFibGVfZmlsZXMYLyABKAsyMC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5VcGxvYWRSZWFkYWJsZUZpbGVzUmVxdWVzdEgAEjgKCGZzX3Jvb3RzGDAgASgLMiQubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNSb290c1JlcXVlc3RIABI/Cgxmc19saXN0X2RpcnMYMSABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc0xpc3REaXJzUmVxdWVzdEgAEjEKBHBpbmcYMiABKAsyIS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5QaW5nUmVxdWVzdEgAElMKFnVwbG9hZGVkX2ltYWdlX3ByZXZpZXcYMyABKAsyMS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5VcGxvYWRlZEltYWdlUHJldmlld1JlcXVlc3RIABJCCg1tZW1vcnlfbWFuYWdlGDQgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuTWVtb3J5TWFuYWdlUmVxdWVzdEgAEkAKDHNraWxsX21hbmFnZRg1IAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNraWxsTWFuYWdlUmVxdWVzdEgAElYKGGZzX2NyZWF0ZV9wcm9qZWN0X2ZvbGRlchg2IAEoCzIyLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzQ3JlYXRlUHJvamVjdEZvbGRlclJlcXVlc3RIABJBChB0ZXJtaW5hbF9yZXF1ZXN0GDcgASgLMiUubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxSZXF1ZXN0SAASNgoHZnNfbGlzdBg4IAEoCzIjLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzTGlzdFJlcXVlc3RIABJBCg1mc193cml0ZV90ZXh0GDkgASgLMigubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNXcml0ZVRleHRSZXF1ZXN0SAASQQoNZnNfY3JlYXRlX2Rpchg6IAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzQ3JlYXRlRGlyUmVxdWVzdEgAEjoKCWZzX3JlbmFtZRg7IAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzUmVuYW1lUmVxdWVzdEgAEjoKCWZzX2RlbGV0ZRg8IAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzRGVsZXRlUmVxdWVzdEgAEjcKC2dpdF9yZXF1ZXN0GD0gASgLMiAubGl2ZWFnZW50LmdhdGV3YXkudjIuR2l0UmVxdWVzdEgAElAKFWZzX3JlYWRfZWRpdGFibGVfdGV4dBg+IAEoCzIvLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzUmVhZEVkaXRhYmxlVGV4dFJlcXVlc3RIABJUChdmc19yZWFkX3dvcmtzcGFjZV9pbWFnZRg/IAEoCzIxLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzUmVhZFdvcmtzcGFjZUltYWdlUmVxdWVzdEgAEjkKDHNmdHBfcmVxdWVzdBhAIAEoCzIhLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNmdHBSZXF1ZXN0SAASRgoPcHJvdmlkZXJfbW9kZWxzGEEgASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuUHJvdmlkZXJNb2RlbHNSZXF1ZXN0SAASXwodc2V0dGluZ3NfcmVzZXRfc3NoX2tub3duX2hvc3QYSCABKAsyNi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZXR0aW5nc1Jlc2V0U3NoS25vd25Ib3N0UmVxdWVzdEgAEjwKCmNoYXRfcXVldWUYSSABKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0UXVldWVSZXF1ZXN0SAASQAoQY2hhdF9pbmdyZXNzX2FjaxhLIAEoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRJbmdyZXNzQWNrSAASQQoMdHVubmVsX3N0YXRlGFAgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsU3RhdGVTbmFwc2hvdEgAEj8KD3R1bm5lbF9tdXRhdGlvbhhRIAEoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYyLlR1bm5lbE11dGF0aW9uSAASOQoMdHVubmVsX2ZyYW1lGFIgASgLMiEubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsRnJhbWVIABJGCg93b3Jrc3BhY2Vfd2F0Y2gYWiABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Xb3Jrc3BhY2VXYXRjaFJlcXVlc3RIABJOChdtYW5hZ2VkX3Byb2Nlc3NfcmVxdWVzdBhbIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLk1hbmFnZWRQcm9jZXNzUmVxdWVzdEgAEkQKDmhpc3RvcnlfYnJhbmNoGFwgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeUJyYW5jaFJlcXVlc3RIABJECg5wcm92aWRlcl91c2FnZRhdIAEoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYyLlByb3ZpZGVyVXNhZ2VSZXF1ZXN0SAASQwoOY2hhdF9maWxlX29wZW4YXiABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0RmlsZU9wZW5SZXF1ZXN0SABCCQoHcGF5bG9hZEoECEMQREoECEQQRUoECEUQRkoECEoQSyKaJAoNQWdlbnRFbnZlbG9wZRISCgpyZXF1ZXN0X2lkGAEgASgJEhEKCXRpbWVzdGFtcBgCIAEoAxI1CgpjaGF0X2V2ZW50GAogASgLMh8ubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEV2ZW50SAASRAoQY3Jvbl9tYW5hZ2VfcmVzcBgUIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNyb25NYW5hZ2VSZXNwb25zZUgAEkYKEWhpc3RvcnlfbGlzdF9yZXNwGB4gASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeUxpc3RSZXNwb25zZUgAEkQKEGhpc3RvcnlfZ2V0X3Jlc3AYHyABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5R2V0UmVzcG9uc2VIABJKChNoaXN0b3J5X3JlbmFtZV9yZXNwGCAgASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVJlbmFtZVJlc3BvbnNlSAASSgoTaGlzdG9yeV9kZWxldGVfcmVzcBghIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlEZWxldGVSZXNwb25zZUgAEj4KDGhpc3Rvcnlfc3luYxgiIAEoCzImLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlTeW5jRXZlbnRIABJKChNoaXN0b3J5X3ByZWZpeF9yZXNwGCMgASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVByZWZpeFJlc3BvbnNlSAASRAoQaGlzdG9yeV9waW5fcmVzcBgkIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlQaW5SZXNwb25zZUgAEk8KFmhpc3Rvcnlfc2hhcmVfZ2V0X3Jlc3AYJSABKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5U2hhcmVHZXRSZXNwb25zZUgAEk8KFmhpc3Rvcnlfc2hhcmVfc2V0X3Jlc3AYJiABKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5U2hhcmVTZXRSZXNwb25zZUgAElcKGmhpc3Rvcnlfc2hhcmVfcmVzb2x2ZV9yZXNwGCcgASgLMjEubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVNoYXJlUmVzb2x2ZVJlc3BvbnNlSAASTgoVaGlzdG9yeV93b3JrZGlyc19yZXNwGDggASgLMi0ubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVdvcmtkaXJzUmVzcG9uc2VIABJIChJwcm92aWRlcl9saXN0X3Jlc3AYKCABKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Qcm92aWRlckxpc3RSZXNwb25zZUgAEkYKEXNldHRpbmdzX2dldF9yZXNwGCkgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuU2V0dGluZ3NHZXRSZXNwb25zZUgAEkwKFHNldHRpbmdzX3VwZGF0ZV9yZXNwGCogASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuU2V0dGluZ3NVcGRhdGVSZXNwb25zZUgAEkAKDXNldHRpbmdzX3N5bmMYKyABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZXR0aW5nc1N5bmNFdmVudEgAEk0KFXNraWxsX2ZpbGVzX2xpc3RfcmVzcBgsIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNraWxsRmlsZXNMaXN0UmVzcG9uc2VIABJTChhza2lsbF9tZXRhZGF0YV9yZWFkX3Jlc3AYLSABKAsyLy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Ta2lsbE1ldGFkYXRhUmVhZFJlc3BvbnNlSAASSwoUc2tpbGxfdGV4dF9yZWFkX3Jlc3AYLiABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Ta2lsbFRleHRSZWFkUmVzcG9uc2VIABJPChZmaWxlX21lbnRpb25fbGlzdF9yZXNwGC8gASgLMi0ubGl2ZWFnZW50LmdhdGV3YXkudjIuRmlsZU1lbnRpb25MaXN0UmVzcG9uc2VIABJXChp1cGxvYWRfcmVhZGFibGVfZmlsZXNfcmVzcBgwIAEoCzIxLmxpdmVhZ2VudC5nYXRld2F5LnYyLlVwbG9hZFJlYWRhYmxlRmlsZXNSZXNwb25zZUgAEj4KDWZzX3Jvb3RzX3Jlc3AYMSABKAsyJS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc1Jvb3RzUmVzcG9uc2VIABIyCgRwb25nGDIgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuUG9uZ1Jlc3BvbnNlSAASRQoRZnNfbGlzdF9kaXJzX3Jlc3AYMyABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc0xpc3REaXJzUmVzcG9uc2VIABJZCht1cGxvYWRlZF9pbWFnZV9wcmV2aWV3X3Jlc3AYNCABKAsyMi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5VcGxvYWRlZEltYWdlUHJldmlld1Jlc3BvbnNlSAASSAoSbWVtb3J5X21hbmFnZV9yZXNwGDUgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuTWVtb3J5TWFuYWdlUmVzcG9uc2VIABJGChFza2lsbF9tYW5hZ2VfcmVzcBg2IAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNraWxsTWFuYWdlUmVzcG9uc2VIABJcCh1mc19jcmVhdGVfcHJvamVjdF9mb2xkZXJfcmVzcBg3IAEoCzIzLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzQ3JlYXRlUHJvamVjdEZvbGRlclJlc3BvbnNlSAASQwoRdGVybWluYWxfcmVzcG9uc2UYOSABKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFJlc3BvbnNlSAASPQoOdGVybWluYWxfZXZlbnQYOiABKAsyIy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbEV2ZW50SAASPAoMZnNfbGlzdF9yZXNwGDsgASgLMiQubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNMaXN0UmVzcG9uc2VIABJHChJmc193cml0ZV90ZXh0X3Jlc3AYPCABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc1dyaXRlVGV4dFJlc3BvbnNlSAASRwoSZnNfY3JlYXRlX2Rpcl9yZXNwGD0gASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNDcmVhdGVEaXJSZXNwb25zZUgAEkAKDmZzX3JlbmFtZV9yZXNwGD4gASgLMiYubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNSZW5hbWVSZXNwb25zZUgAEkAKDmZzX2RlbGV0ZV9yZXNwGD8gASgLMiYubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNEZWxldGVSZXNwb25zZUgAEjkKDGdpdF9yZXNwb25zZRhAIAEoCzIhLmxpdmVhZ2VudC5nYXRld2F5LnYyLkdpdFJlc3BvbnNlSAASVgoaZnNfcmVhZF9lZGl0YWJsZV90ZXh0X3Jlc3AYQSABKAsyMC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc1JlYWRFZGl0YWJsZVRleHRSZXNwb25zZUgAEloKHGZzX3JlYWRfd29ya3NwYWNlX2ltYWdlX3Jlc3AYQiABKAsyMi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc1JlYWRXb3Jrc3BhY2VJbWFnZVJlc3BvbnNlSAASOwoNc2Z0cF9yZXNwb25zZRhJIAEoCzIiLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNmdHBSZXNwb25zZUgAEjUKCnNmdHBfZXZlbnQYSiABKAsyHy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZnRwRXZlbnRIABJCCg9jaGF0X3F1ZXVlX3Jlc3AYSyABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0UXVldWVSZXNwb25zZUgAEkAKEGNoYXRfcXVldWVfZXZlbnQYTCABKAsyJC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0UXVldWVFdmVudEgAEj4KDGNoYXRfY29udHJvbBhGIAEoCzImLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRDb250cm9sRXZlbnRIABJCCg5ydW50aW1lX3N0YXR1cxhHIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLlJ1bnRpbWVTdGF0dXNFdmVudEgAEmUKInNldHRpbmdzX3Jlc2V0X3NzaF9rbm93bl9ob3N0X3Jlc3AYSCABKAsyNy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZXR0aW5nc1Jlc2V0U3NoS25vd25Ib3N0UmVzcG9uc2VIABJKChVjaGF0X3J1bnRpbWVfc25hcHNob3QYTSABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0UnVudGltZVNuYXBzaG90SAASTAoUcHJvdmlkZXJfbW9kZWxzX3Jlc3AYTyABKAsyLC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Qcm92aWRlck1vZGVsc1Jlc3BvbnNlSAASQgoOdHVubmVsX2Rlc2lyZWQYUCABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxEZXNpcmVkU3RhdGVIABJMChZ0dW5uZWxfbXV0YXRpb25fcmVzdWx0GFEgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsTXV0YXRpb25SZXN1bHRIABI5Cgx0dW5uZWxfZnJhbWUYUiABKAsyIS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxGcmFtZUgAEkYKE3R1bm5lbF9wcm9iZV9yZXBvcnQYUyABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxQcm9iZVJlcG9ydEgAEkoKEndvcmtzcGFjZV9hY3Rpdml0eRhaIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYyLldvcmtzcGFjZUFjdGl2aXR5RXZlbnRIABJQChhtYW5hZ2VkX3Byb2Nlc3NfcmVzcG9uc2UYWyABKAsyLC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5NYW5hZ2VkUHJvY2Vzc1Jlc3BvbnNlSAASUAoYbWFuYWdlZF9wcm9jZXNzX3NuYXBzaG90GFwgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuTWFuYWdlZFByb2Nlc3NTbmFwc2hvdEgAEkoKE2hpc3RvcnlfYnJhbmNoX3Jlc3AYXSABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5QnJhbmNoUmVzcG9uc2VIABJKChNwcm92aWRlcl91c2FnZV9yZXNwGF4gASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuUHJvdmlkZXJVc2FnZVJlc3BvbnNlSAASRAoSY2hhdF9pbmdyZXNzX2JhdGNoGF8gASgLMiYubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEluZ3Jlc3NCYXRjaEgAEkYKE2NoYXRfaW5ncmVzc19yZXN1bWUYYCABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0SW5ncmVzc1Jlc3VtZUgAEkoKFWNoYXRfaW5ncmVzc19mcmFnbWVudBhhIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRJbmdyZXNzRnJhZ21lbnRIABJJChNjaGF0X2ZpbGVfb3Blbl9yZXNwGGIgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEZpbGVPcGVuUmVzcG9uc2VIABI0CgVlcnJvchhjIAEoCzIjLmxpdmVhZ2VudC5nYXRld2F5LnYyLkVycm9yUmVzcG9uc2VIAEIJCgdwYXlsb2FkSgQIQxBESgQIRBBFSgQIRRBGSgQIThBPIlUKEUNoYXRTZWxlY3RlZE1vZGVsEhoKEmN1c3RvbV9wcm92aWRlcl9pZBgBIAEoCRINCgVtb2RlbBgCIAEoCRIVCg1wcm92aWRlcl90eXBlGAMgASgJImUKE0NoYXRSdW50aW1lQ29udHJvbHMSGAoQdGhpbmtpbmdfZW5hYmxlZBgBIAEoCBIhChluYXRpdmVfd2ViX3NlYXJjaF9lbmFibGVkGAIgASgIEhEKCXJlYXNvbmluZxgDIAEoCSJ1ChBDaGF0VXBsb2FkZWRGaWxlEhUKDXJlbGF0aXZlX3BhdGgYASABKAkSFQoNYWJzb2x1dGVfcGF0aBgCIAEoCRIRCglmaWxlX25hbWUYAyABKAkSDAoEa2luZBgEIAEoCRISCgpzaXplX2J5dGVzGAUgASgDIksKElVwbG9hZFJlYWRhYmxlRmlsZRIRCglmaWxlX25hbWUYASABKAkSEQoJbWltZV90eXBlGAIgASgJEg8KB2NvbnRlbnQYAyABKAwiZgoaVXBsb2FkUmVhZGFibGVGaWxlc1JlcXVlc3QSDwoHd29ya2RpchgBIAEoCRI3CgVmaWxlcxgCIAMoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLlVwbG9hZFJlYWRhYmxlRmlsZSJlChtVcGxvYWRSZWFkYWJsZUZpbGVzUmVzcG9uc2USNQoFZmlsZXMYASADKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0VXBsb2FkZWRGaWxlEg8KB3NraXBwZWQYAiADKAkiRQobVXBsb2FkZWRJbWFnZVByZXZpZXdSZXF1ZXN0Eg8KB3dvcmtkaXIYASABKAkSFQoNYWJzb2x1dGVfcGF0aBgCIAEoCSI/ChxVcGxvYWRlZEltYWdlUHJldmlld1Jlc3BvbnNlEhEKCW1pbWVfdHlwZRgBIAEoCRIMCgRkYXRhGAIgASgJInsKClR1bm5lbFNwZWMSCgoCaWQYASABKAkSEQoJc2x1Z19oaW50GAIgASgJEgwKBG5hbWUYAyABKAkSEgoKdGFyZ2V0X3VybBgEIAEoCRISCgpleHBpcmVzX2F0GAUgASgDEhgKEHByb2plY3RfcGF0aF9rZXkYBiABKAkiWQoSVHVubmVsRGVzaXJlZFN0YXRlEjEKB3R1bm5lbHMYASADKAsyIC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxTcGVjEhAKCHJldmlzaW9uGAIgASgEImYKDFR1bm5lbEhlYWx0aBIOCgZzdGF0dXMYASABKAkSEwoLaHR0cF9zdGF0dXMYAiABKA0SDQoFZXJyb3IYAyABKAkSEgoKY2hlY2tlZF9hdBgEIAEoAxIOCgZydHRfbXMYBSABKA0i8AEKDFR1bm5lbFN0YXR1cxIKCgJpZBgBIAEoCRIMCgRzbHVnGAIgASgJEgwKBG5hbWUYAyABKAkSEgoKdGFyZ2V0X3VybBgEIAEoCRITCgtwdWJsaWNfcGF0aBgFIAEoCRISCgpjcmVhdGVkX2F0GAYgASgDEhIKCmV4cGlyZXNfYXQYByABKAMSGgoSYWN0aXZlX2Nvbm5lY3Rpb25zGAggASgNEhgKEHByb2plY3RfcGF0aF9rZXkYCSABKAkSMQoFbG9jYWwYCiABKAsyIi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxIZWFsdGgipQEKE1R1bm5lbFN0YXRlU25hcHNob3QSMwoHdHVubmVscxgBIAMoCzIiLmxpdmVhZ2VudC5nYXRld2F5LnYyLlR1bm5lbFN0YXR1cxIQCghyZXZpc2lvbhgCIAEoBBIUCgxhZ2VudF9vbmxpbmUYAyABKAgSMQoFcmVsYXkYBCABKAsyIi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxIZWFsdGgimQEKDlR1bm5lbE11dGF0aW9uEg4KBmFjdGlvbhgBIAEoCRIRCgl0dW5uZWxfaWQYAiABKAkSEgoKdGFyZ2V0X3VybBgDIAEoCRIMCgRuYW1lGAQgASgJEhgKC3R0bF9zZWNvbmRzGAUgASgNSACIAQESGAoQcHJvamVjdF9wYXRoX2tleRgGIAEoCUIOCgxfdHRsX3NlY29uZHMiVAoUVHVubmVsTXV0YXRpb25SZXN1bHQSEQoJdHVubmVsX2lkGAEgASgJEhIKCmVycm9yX2NvZGUYAiABKAkSFQoNZXJyb3JfbWVzc2FnZRgDIAEoCSJZChFUdW5uZWxQcm9iZVJlc3VsdBIRCgl0dW5uZWxfaWQYASABKAkSMQoFbG9jYWwYAiABKAsyIi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxIZWFsdGgiTQoRVHVubmVsUHJvYmVSZXBvcnQSOAoHcmVzdWx0cxgBIAMoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYyLlR1bm5lbFByb2JlUmVzdWx0IisKDFR1bm5lbEhlYWRlchIMCgRuYW1lGAEgASgJEg0KBXZhbHVlGAIgASgJIvUCCgtUdW5uZWxGcmFtZRIRCglzdHJlYW1faWQYASABKAkSMwoEa2luZBgCIAEoDjIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLlR1bm5lbEZyYW1lS2luZBISCgp0YXJnZXRfdXJsGAMgASgJEg4KBm1ldGhvZBgEIAEoCRIMCgRwYXRoGAUgASgJEjMKB2hlYWRlcnMYBiADKAsyIi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxIZWFkZXISDgoGc3RhdHVzGAcgASgNEgwKBGJvZHkYCCABKAwSDQoFZXJyb3IYCSABKAkSQgoPd3NfbWVzc2FnZV90eXBlGAogASgOMikubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsV3NNZXNzYWdlVHlwZRIWCg53c19zdWJwcm90b2NvbBgLIAEoCRIVCg13c19jbG9zZV9jb2RlGAwgASgNEhcKD3dzX2Nsb3NlX3JlYXNvbhgNIAEoCSIpChVXb3Jrc3BhY2VXYXRjaFJlcXVlc3QSEAoId29ya2RpcnMYASADKAkifgoWV29ya3NwYWNlQWN0aXZpdHlFdmVudBIPCgd3b3JrZGlyGAEgASgJEhAKCHJldmlzaW9uGAIgASgEEgoKAmZzGAMgASgIEgsKA2dpdBgEIAEoCBIVCg1jaGFuZ2VkX3BhdGhzGAUgAygJEhEKCXRydW5jYXRlZBgGIAEoCCKWAgoUTWFuYWdlZFByb2Nlc3NSZWNvcmQSCgoCaWQYASABKAkSDQoFbGFiZWwYAiABKAkSDwoHY29tbWFuZBgDIAEoCRILCgNjd2QYBCABKAkSDQoFc2hlbGwYBSABKAkSCwoDcGlkGAYgASgNEhAKCGxvZ19wYXRoGAcgASgJEhIKCnN0YXJ0ZWRfYXQYCCABKAMSGAoLZmluaXNoZWRfYXQYCSABKANIAIgBARIWCglleGl0X2NvZGUYCiABKAVIAYgBARIPCgdydW5uaW5nGAsgASgIEhAKCGlzb2xhdGVkGAwgASgIEhAKCHJlc3RvcmVkGA0gASgIQg4KDF9maW5pc2hlZF9hdEIMCgpfZXhpdF9jb2RlImkKFk1hbmFnZWRQcm9jZXNzU25hcHNob3QSPQoJcHJvY2Vzc2VzGAEgAygLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuTWFuYWdlZFByb2Nlc3NSZWNvcmQSEAoIcmV2aXNpb24YAiABKAQiTgoVTWFuYWdlZFByb2Nlc3NSZXF1ZXN0Eg4KBmFjdGlvbhgBIAEoCRISCgpwcm9jZXNzX2lkGAIgASgJEhEKCW1heF9ieXRlcxgDIAEoDSK3AQoWTWFuYWdlZFByb2Nlc3NSZXNwb25zZRIOCgZhY3Rpb24YASABKAkSPgoIc25hcHNob3QYAiABKAsyLC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5NYW5hZ2VkUHJvY2Vzc1NuYXBzaG90EhMKC2xvZ19jb250ZW50GAMgASgJEhAKCGxvZ19wYXRoGAQgASgJEhUKDWxvZ190cnVuY2F0ZWQYBSABKAgSDwoHc3RvcHBlZBgGIAEoCCI5ChNNZW1vcnlNYW5hZ2VSZXF1ZXN0Eg8KB2NvbW1hbmQYASABKAkSEQoJYXJnc19qc29uGAIgASgJIisKFE1lbW9yeU1hbmFnZVJlc3BvbnNlEhMKC3Jlc3VsdF9qc29uGAEgASgJIpgDCg9UZXJtaW5hbFJlcXVlc3QSDgoGYWN0aW9uGAEgASgJEhIKCnNlc3Npb25faWQYAiABKAkSGAoQcHJvamVjdF9wYXRoX2tleRgDIAEoCRILCgNjd2QYBCABKAkSDQoFc2hlbGwYBSABKAkSDQoFdGl0bGUYBiABKAkSDAoEZGF0YRgHIAEoCRIMCgRjb2xzGAggASgNEgwKBHJvd3MYCSABKA0SEQoJbWF4X2J5dGVzGAogASgNEhMKC3NzaF9ob3N0X2lkGAsgASgJEhEKCXByb21wdF9pZBgMIAEoCRIVCg1wcm9tcHRfYW5zd2VyGA0gASgJEhYKDnRydXN0X2hvc3Rfa2V5GA4gASgIEhQKDHNmdHBfZW5hYmxlZBgPIAEoCBIOCgZ0YWJfaWQYECABKAkSEAoIdGFiX2tpbmQYESABKAkSEwoLcmVtb3RlX2hvc3QYEiABKAkSEwoLcmVtb3RlX3BvcnQYEyABKA0SEgoKbG9jYWxfcG9ydBgUIAEoDRISCgpmb3J3YXJkX2lkGBUgASgJIrICCg9UZXJtaW5hbFNlc3Npb24SCgoCaWQYASABKAkSGAoQcHJvamVjdF9wYXRoX2tleRgCIAEoCRILCgNjd2QYAyABKAkSDQoFc2hlbGwYBCABKAkSDQoFdGl0bGUYBSABKAkSCwoDcGlkGAYgASgNEgwKBGNvbHMYByABKA0SDAoEcm93cxgIIAEoDRISCgpjcmVhdGVkX2F0GAkgASgEEhIKCnVwZGF0ZWRfYXQYCiABKAQSEwoLZmluaXNoZWRfYXQYCyABKAQSEQoJZXhpdF9jb2RlGAwgASgFEg8KB3J1bm5pbmcYDSABKAgSDAoEa2luZBgOIAEoCRI2CgNzc2gYDyABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNzaE1ldGFkYXRhItsBChNUZXJtaW5hbFNzaE1ldGFkYXRhEg8KB2hvc3RfaWQYASABKAkSEQoJaG9zdF9uYW1lGAIgASgJEhAKCHVzZXJuYW1lGAMgASgJEgwKBGhvc3QYBCABKAkSDAoEcG9ydBgFIAEoDRIRCglhdXRoX3R5cGUYBiABKAkSDgoGc3RhdHVzGAcgASgJEhkKEXJlY29ubmVjdF9hdHRlbXB0GAggASgNEh4KFnJlY29ubmVjdF9tYXhfYXR0ZW1wdHMYCSABKA0SFAoMc2Z0cF9lbmFibGVkGAogASgIIvcBCgtTZnRwUmVxdWVzdBIOCgZhY3Rpb24YASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIYChBwcm9qZWN0X3BhdGhfa2V5GAMgASgJEg8KB3dvcmtkaXIYBCABKAkSEgoKbG9jYWxfcGF0aBgFIAEoCRITCgtyZW1vdGVfcGF0aBgGIAEoCRIRCglmcm9tX3BhdGgYByABKAkSDwoHdG9fcGF0aBgIIAEoCRIRCglkaXJlY3Rpb24YCSABKAkSEwoLdGFyZ2V0X3BhdGgYCiABKAkSEQoJcmVjdXJzaXZlGAsgASgIEhEKCW92ZXJ3cml0ZRgMIAEoCCJYCglTZnRwRW50cnkSDAoEcGF0aBgBIAEoCRIMCgRuYW1lGAIgASgJEgwKBGtpbmQYAyABKAkSEgoKc2l6ZV9ieXRlcxgEIAEoBBINCgVtdGltZRgFIAEoBCLyAQoMU2Z0cFRyYW5zZmVyEgoKAmlkGAEgASgJEhIKCnNlc3Npb25faWQYAiABKAkSEQoJZGlyZWN0aW9uGAMgASgJEg4KBnN0YXR1cxgEIAEoCRITCgtzb3VyY2VfcGF0aBgFIAEoCRITCgt0YXJnZXRfcGF0aBgGIAEoCRIUCgxjdXJyZW50X3BhdGgYByABKAkSEgoKYnl0ZXNfZG9uZRgIIAEoBBITCgtieXRlc190b3RhbBgJIAEoBBISCgpmaWxlc19kb25lGAogASgNEhMKC2ZpbGVzX3RvdGFsGAsgASgNEg0KBWVycm9yGAwgASgJItQBCgxTZnRwUmVzcG9uc2USDgoGYWN0aW9uGAEgASgJEgwKBHBhdGgYAiABKAkSMAoHZW50cmllcxgDIAMoCzIfLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNmdHBFbnRyeRIuCgVlbnRyeRgEIAEoCzIfLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNmdHBFbnRyeRIOCgZleGlzdHMYBSABKAgSNAoIdHJhbnNmZXIYBiABKAsyIi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZnRwVHJhbnNmZXIiTwoJU2Z0cEV2ZW50EgwKBGtpbmQYASABKAkSNAoIdHJhbnNmZXIYAiABKAsyIi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZnRwVHJhbnNmZXIiwQEKEVRlcm1pbmFsU3NoUHJvbXB0EgoKAmlkGAEgASgJEgwKBGtpbmQYAiABKAkSDwoHaG9zdF9pZBgDIAEoCRIRCglob3N0X25hbWUYBCABKAkSDAoEaG9zdBgFIAEoCRIMCgRwb3J0GAYgASgNEg8KB21lc3NhZ2UYByABKAkSGgoSZmluZ2VycHJpbnRfc2hhMjU2GAggASgJEhAKCGtleV90eXBlGAkgASgJEhMKC2Fuc3dlcl9lY2hvGAogASgIIkEKE1Rlcm1pbmFsU2hlbGxPcHRpb24SCgoCaWQYASABKAkSDQoFbGFiZWwYAiABKAkSDwoHY29tbWFuZBgDIAEoCSKAAQoOVGVybWluYWxTc2hUYWISCgoCaWQYASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIYChBwcm9qZWN0X3BhdGhfa2V5GAMgASgJEgwKBGtpbmQYBCABKAkSEgoKY3JlYXRlZF9hdBgFIAEoBBISCgp1cGRhdGVkX2F0GAYgASgEIn8KF1Rlcm1pbmFsU3NoVGFic1NuYXBzaG90EhgKEHByb2plY3RfcGF0aF9rZXkYASABKAkSMgoEdGFicxgCIAMoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU3NoVGFiEhAKCHJldmlzaW9uGAQgASgESgQIAxAEIv0BChdUZXJtaW5hbFNzaExvY2FsRm9yd2FyZBIKCgJpZBgBIAEoCRISCgpzZXNzaW9uX2lkGAIgASgJEhgKEHByb2plY3RfcGF0aF9rZXkYAyABKAkSEgoKbG9jYWxfaG9zdBgEIAEoCRISCgpsb2NhbF9wb3J0GAUgASgNEg8KB2FkZHJlc3MYBiABKAkSEwoLcmVtb3RlX2hvc3QYByABKAkSEwoLcmVtb3RlX3BvcnQYCCABKA0SDgoGc3RhdHVzGAkgASgJEhIKCmNyZWF0ZWRfYXQYCiABKAQSEgoKdXBkYXRlZF9hdBgLIAEoBBINCgVlcnJvchgMIAEoCSJ1CiBUZXJtaW5hbFNzaExvY2FsRm9yd2FyZHNTbmFwc2hvdBI/Cghmb3J3YXJkcxgBIAMoCzItLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU3NoTG9jYWxGb3J3YXJkEhAKCHJldmlzaW9uGAIgASgEIn8KHVRlcm1pbmFsU3NoTG9jYWxGb3J3YXJkQWN0aW9uEj4KB2ZvcndhcmQYASABKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNzaExvY2FsRm9yd2FyZBIQCghyZXZpc2lvbhgCIAEoBBIMCgRraW5kGAMgASgJIqcFChBUZXJtaW5hbFJlc3BvbnNlEg4KBmFjdGlvbhgBIAEoCRI3CghzZXNzaW9ucxgCIAMoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU2Vzc2lvbhI2CgdzZXNzaW9uGAMgASgLMiUubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTZXNzaW9uEg4KBm91dHB1dBgEIAEoDBIRCgl0cnVuY2F0ZWQYBSABKAgSQAoNc2hlbGxfb3B0aW9ucxgGIAMoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU2hlbGxPcHRpb24SFQoNZGVmYXVsdF9zaGVsbBgHIAEoCRIbChNvdXRwdXRfc3RhcnRfb2Zmc2V0GAggASgEEhkKEW91dHB1dF9lbmRfb2Zmc2V0GAkgASgEEjsKCnNzaF9wcm9tcHQYCiABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNzaFByb21wdBISCgpsYXRlbmN5X21zGAsgASgNEj8KCHNzaF90YWJzGAwgASgLMi0ubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTc2hUYWJzU25hcHNob3QSUgoSc3NoX2xvY2FsX2ZvcndhcmRzGA0gASgLMjYubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTc2hMb2NhbEZvcndhcmRzU25hcHNob3QSTgoRc3NoX2xvY2FsX2ZvcndhcmQYDiABKAsyMy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNzaExvY2FsRm9yd2FyZEFjdGlvbhIoCiBzc2hfbG9jYWxfZm9yd2FyZF9wb3J0X2F2YWlsYWJsZRgPIAEoCCLaAgoNVGVybWluYWxFdmVudBIMCgRraW5kGAEgASgJEhIKCnNlc3Npb25faWQYAiABKAkSGAoQcHJvamVjdF9wYXRoX2tleRgDIAEoCRI2CgdzZXNzaW9uGAQgASgLMiUubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTZXNzaW9uEgwKBGRhdGEYBSABKAwSGwoTb3V0cHV0X3N0YXJ0X29mZnNldBgGIAEoBBIZChFvdXRwdXRfZW5kX29mZnNldBgHIAEoBBI/Cghzc2hfdGFicxgIIAEoCzItLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU3NoVGFic1NuYXBzaG90Ek4KEXNzaF9sb2NhbF9mb3J3YXJkGAkgASgLMjMubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTc2hMb2NhbEZvcndhcmRBY3Rpb24isgIKE1Rlcm1pbmFsU3RyZWFtRnJhbWUSDAoEa2luZBgBIAEoCRIRCglzdHJlYW1faWQYAiABKAkSEgoKc2Vzc2lvbl9pZBgDIAEoCRIYChBwcm9qZWN0X3BhdGhfa2V5GAQgASgJEgsKA3NlcRgFIAEoBBIUCgxzdGFydF9vZmZzZXQYBiABKAQSEgoKZW5kX29mZnNldBgHIAEoBBIMCgRjb2xzGAggASgNEgwKBHJvd3MYCSABKA0SEQoJbWF4X2J5dGVzGAogASgNEhEKCXRydW5jYXRlZBgLIAEoCBINCgVlcnJvchgMIAEoCRI2CgdzZXNzaW9uGA0gASgLMiUubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTZXNzaW9uEgwKBGRhdGEYDiABKAwiQAoKR2l0UmVxdWVzdBIOCgZhY3Rpb24YASABKAkSDwoHd29ya2RpchgCIAEoCRIRCglhcmdzX2pzb24YAyABKAkiMgoLR2l0UmVzcG9uc2USDgoGYWN0aW9uGAEgASgJEhMKC3Jlc3VsdF9qc29uGAIgASgJIvYCCgtDaGF0UmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDwoHbWVzc2FnZRgCIAEoCRI/Cg5zZWxlY3RlZF9tb2RlbBgDIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRTZWxlY3RlZE1vZGVsEhYKDmV4ZWN1dGlvbl9tb2RlGAQgASgJEg8KB3dvcmtkaXIYBSABKAkSHQoVc2VsZWN0ZWRfc3lzdGVtX3Rvb2xzGAYgAygJEj4KDnVwbG9hZGVkX2ZpbGVzGAcgAygLMiYubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdFVwbG9hZGVkRmlsZRIZChFjbGllbnRfcmVxdWVzdF9pZBgIIAEoCRJDChBydW50aW1lX2NvbnRyb2xzGAkgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdFJ1bnRpbWVDb250cm9scxIUCgxxdWV1ZV9wb2xpY3kYCiABKAkiigEKDkNoYXRNZXNzYWdlUmVmEhUKDXNlZ21lbnRfaW5kZXgYASABKAUSFQoNbWVzc2FnZV9pbmRleBgCIAEoBRISCgpzZWdtZW50X2lkGAMgASgJEhIKCm1lc3NhZ2VfaWQYBCABKAkSDAoEcm9sZRgFIAEoCRIUCgxjb250ZW50X2hhc2gYBiABKAkiPAoRQ2FuY2VsQ2hhdFJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEg4KBnJ1bl9pZBgCIAEoCSLPAQoSQ2hhdENvbW1hbmRSZXF1ZXN0EgwKBHR5cGUYASABKAkSMgoHcmVxdWVzdBgCIAEoCzIhLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRSZXF1ZXN0Ej4KEGJhc2VfbWVzc2FnZV9yZWYYAyABKAsyJC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0TWVzc2FnZVJlZhI3CgZjYW5jZWwYBCABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DYW5jZWxDaGF0UmVxdWVzdCK4AQoQQ2hhdFF1ZXVlUmVxdWVzdBIOCgZhY3Rpb24YASABKAkSFwoPY29udmVyc2F0aW9uX2lkGAIgASgJEg8KB2l0ZW1faWQYAyABKAkSEQoJZGlyZWN0aW9uGAQgASgJEhAKCHJldmlzaW9uGAUgASgEEhIKCmRyYWZ0X2pzb24YBiABKAkSGwoTdXBsb2FkZWRfZmlsZXNfanNvbhgHIAEoCRIUCgxyZXF1ZXN0X2pzb24YCCABKAkihgEKEUNoYXRRdWV1ZVJlc3BvbnNlEhAKCGFjY2VwdGVkGAEgASgIEg8KB21lc3NhZ2UYAiABKAkSFQoNc25hcHNob3RfanNvbhgDIAEoCRIRCglpdGVtX2pzb24YBCABKAkSEgoKZXJyb3JfY29kZRgFIAEoCRIQCghyZXZpc2lvbhgGIAEoBCJSCg5DaGF0UXVldWVFdmVudBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSFQoNc25hcHNob3RfanNvbhgCIAEoCRIQCghyZXZpc2lvbhgDIAEoBCKFAgoJQ2hhdEV2ZW50EjsKBHR5cGUYASABKA4yLS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0RXZlbnQuQ2hhdEV2ZW50VHlwZRIXCg9jb252ZXJzYXRpb25faWQYAiABKAkSDAoEZGF0YRgDIAEoCSKTAQoNQ2hhdEV2ZW50VHlwZRIJCgVUT0tFThAAEgwKCFRISU5LSU5HEAESDQoJVE9PTF9DQUxMEAISDwoLVE9PTF9SRVNVTFQQAxIICgRET05FEAQSCQoFRVJST1IQBRIPCgtUT09MX1NUQVRVUxAGEhEKDUhPU1RFRF9TRUFSQ0gQBxIQCgxVU0VSX01FU1NBR0UQCCK8AQoQQ2hhdENvbnRyb2xFdmVudBISCgpyZXF1ZXN0X2lkGAEgASgJEhkKEWNsaWVudF9yZXF1ZXN0X2lkGAIgASgJEhcKD2NvbnZlcnNhdGlvbl9pZBgDIAEoCRIRCglydW5fZXBvY2gYBCABKAMSDAoEdHlwZRgFIAEoCRINCgVzdGF0ZRgGIAEoCRISCgplcnJvcl9jb2RlGAcgASgJEg8KB21lc3NhZ2UYCCABKAkSCwoDc2VxGAkgASgDIvwBChNDaGF0UnVudGltZVNuYXBzaG90EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIOCgZydW5faWQYAiABKAkSGQoRY2xpZW50X3JlcXVlc3RfaWQYAyABKAkSEQoJd29ya2VyX2lkGAQgASgJEg0KBXN0YXRlGAUgASgJEgsKA2N3ZBgGIAEoCRISCgp1cGRhdGVkX2F0GAcgASgDEhAKCHJldmlzaW9uGAggASgDEhQKDGVudHJpZXNfanNvbhgJIAEoCRITCgt0b29sX3N0YXR1cxgKIAEoCRIhChl0b29sX3N0YXR1c19pc19jb21wYWN0aW9uGAsgASgIIuoBChJSdW50aW1lU3RhdHVzRXZlbnQSEQoJd29ya2VyX2lkGAEgASgJEg0KBXN0YXRlGAIgASgJEg8KB3Zpc2libGUYAyABKAgSGAoQYWN0aXZlX3J1bl9jb3VudBgEIAEoDRIRCgl0aW1lc3RhbXAYBSABKAMSOAoLYWN0aXZlX3J1bnMYBiADKAsyIy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0UnVuUmVwb3J0EjoKDWZpbmlzaGVkX3J1bnMYByADKAsyIy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0UnVuUmVwb3J0IoABCg1DaGF0UnVuUmVwb3J0Eg4KBnJ1bl9pZBgBIAEoCRIXCg9jb252ZXJzYXRpb25faWQYAiABKAkSDQoFc3RhdGUYAyABKAkSEgoKZXJyb3JfY29kZRgEIAEoCRIPCgdtZXNzYWdlGAUgASgJEhIKCnVwZGF0ZWRfYXQYBiABKAMiRwoRQ3Jvbk1hbmFnZVJlcXVlc3QSDgoGYWN0aW9uGAEgASgJEg8KB3Rhc2tfaWQYAiABKAkSEQoJdGFza19qc29uGAMgASgJIjkKEkNyb25NYW5hZ2VSZXNwb25zZRIOCgZhY3Rpb24YASABKAkSEwoLcmVzdWx0X2pzb24YAiABKAkiVQoSSGlzdG9yeUxpc3RSZXF1ZXN0EgwKBHBhZ2UYASABKAUSEQoJcGFnZV9zaXplGAIgASgFEgsKA2N3ZBgDIAEoCRIRCgljd2RfZW1wdHkYBCABKAgibAoTSGlzdG9yeUxpc3RSZXNwb25zZRJACg1jb252ZXJzYXRpb25zGAEgAygLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ29udmVyc2F0aW9uU3VtbWFyeRITCgt0b3RhbF9jb3VudBgCIAEoBSKKAgoTQ29udmVyc2F0aW9uU3VtbWFyeRIKCgJpZBgBIAEoCRINCgV0aXRsZRgCIAEoCRISCgpjcmVhdGVkX2F0GAMgASgDEhIKCnVwZGF0ZWRfYXQYBCABKAMSFQoNbWVzc2FnZV9jb3VudBgFIAEoBRITCgtwcm92aWRlcl9pZBgGIAEoCRINCgVtb2RlbBgHIAEoCRISCgpzZXNzaW9uX2lkGAggASgJEgsKA2N3ZBgJIAEoCRIRCglpc19waW5uZWQYCiABKAgSEQoJcGlubmVkX2F0GAsgASgDEhEKCWlzX3NoYXJlZBgMIAEoCBIbChNzZWxlY3RlZF9tb2RlbF9qc29uGA0gASgJIkIKEUhpc3RvcnlHZXRSZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIUCgxtYXhfbWVzc2FnZXMYAiABKAUi1AEKEkhpc3RvcnlHZXRSZXNwb25zZRIXCg9jb252ZXJzYXRpb25faWQYASABKAkSFQoNbWVzc2FnZXNfanNvbhgCIAEoCRIbChN0b3RhbF9tZXNzYWdlX2NvdW50GAMgASgFEh4KFnJldHVybmVkX21lc3NhZ2VfY291bnQYBCABKAUSEAoIaGFzX21vcmUYBSABKAgSPwoMY29udmVyc2F0aW9uGAYgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ29udmVyc2F0aW9uU3VtbWFyeSKFAQoUSGlzdG9yeVByZWZpeFJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEhQKDG1heF9tZXNzYWdlcxgCIAEoBRI+ChBiYXNlX21lc3NhZ2VfcmVmGAMgASgLMiQubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdE1lc3NhZ2VSZWYi1wEKFUhpc3RvcnlQcmVmaXhSZXNwb25zZRIXCg9jb252ZXJzYXRpb25faWQYASABKAkSFQoNbWVzc2FnZXNfanNvbhgCIAEoCRIbChN0b3RhbF9tZXNzYWdlX2NvdW50GAMgASgFEh4KFnJldHVybmVkX21lc3NhZ2VfY291bnQYBCABKAUSEAoIaGFzX21vcmUYBSABKAgSPwoMY29udmVyc2F0aW9uGAYgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ29udmVyc2F0aW9uU3VtbWFyeSI+ChRIaXN0b3J5UmVuYW1lUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDQoFdGl0bGUYAiABKAkiWAoVSGlzdG9yeVJlbmFtZVJlc3BvbnNlEj8KDGNvbnZlcnNhdGlvbhgBIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNvbnZlcnNhdGlvblN1bW1hcnkibwoUSGlzdG9yeUJyYW5jaFJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEj4KEGJhc2VfbWVzc2FnZV9yZWYYAiABKAsyJC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0TWVzc2FnZVJlZiJYChVIaXN0b3J5QnJhbmNoUmVzcG9uc2USPwoMY29udmVyc2F0aW9uGAEgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ29udmVyc2F0aW9uU3VtbWFyeSI/ChFIaXN0b3J5UGluUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSEQoJaXNfcGlubmVkGAIgASgIIlUKEkhpc3RvcnlQaW5SZXNwb25zZRI/Cgxjb252ZXJzYXRpb24YASABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Db252ZXJzYXRpb25TdW1tYXJ5IpIBChJIaXN0b3J5U2hhcmVTdGF0dXMSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEg8KB2VuYWJsZWQYAiABKAgSDQoFdG9rZW4YAyABKAkSEgoKY3JlYXRlZF9hdBgEIAEoAxISCgp1cGRhdGVkX2F0GAUgASgDEhsKE3JlZGFjdF90b29sX2NvbnRlbnQYBiABKAgiMQoWSGlzdG9yeVNoYXJlR2V0UmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkiUgoXSGlzdG9yeVNoYXJlR2V0UmVzcG9uc2USNwoFc2hhcmUYASABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5U2hhcmVTdGF0dXMifAoWSGlzdG9yeVNoYXJlU2V0UmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDwoHZW5hYmxlZBgCIAEoCBIgChNyZWRhY3RfdG9vbF9jb250ZW50GAMgASgISACIAQFCFgoUX3JlZGFjdF90b29sX2NvbnRlbnQiUgoXSGlzdG9yeVNoYXJlU2V0UmVzcG9uc2USNwoFc2hhcmUYASABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5U2hhcmVTdGF0dXMiKwoaSGlzdG9yeVNoYXJlUmVzb2x2ZVJlcXVlc3QSDQoFdG9rZW4YASABKAkiyAEKG0hpc3RvcnlTaGFyZVJlc29sdmVSZXNwb25zZRIXCg9jb252ZXJzYXRpb25faWQYASABKAkSFQoNbWVzc2FnZXNfanNvbhgCIAEoCRIbChN0b3RhbF9tZXNzYWdlX2NvdW50GAMgASgFEj8KDGNvbnZlcnNhdGlvbhgEIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNvbnZlcnNhdGlvblN1bW1hcnkSGwoTcmVkYWN0X3Rvb2xfY29udGVudBgFIAEoCCIYChZIaXN0b3J5V29ya2RpcnNSZXF1ZXN0IlUKFUhpc3RvcnlXb3JrZGlyU3VtbWFyeRIMCgRwYXRoGAEgASgJEhoKEmNvbnZlcnNhdGlvbl9jb3VudBgCIAEoBRISCgp1cGRhdGVkX2F0GAMgASgDIlgKF0hpc3RvcnlXb3JrZGlyc1Jlc3BvbnNlEj0KCHdvcmtkaXJzGAEgAygLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVdvcmtkaXJTdW1tYXJ5Ii8KFEhpc3RvcnlEZWxldGVSZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCSIXChVIaXN0b3J5RGVsZXRlUmVzcG9uc2UiegoQSGlzdG9yeVN5bmNFdmVudBIMCgRraW5kGAEgASgJEj8KDGNvbnZlcnNhdGlvbhgCIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNvbnZlcnNhdGlvblN1bW1hcnkSFwoPY29udmVyc2F0aW9uX2lkGAMgASgJIhUKE1Byb3ZpZGVyTGlzdFJlcXVlc3QiLgoUUHJvdmlkZXJMaXN0UmVzcG9uc2USFgoOcHJvdmlkZXJzX2pzb24YASABKAkiFAoSU2V0dGluZ3NHZXRSZXF1ZXN0IiwKE1NldHRpbmdzR2V0UmVzcG9uc2USFQoNc2V0dGluZ3NfanNvbhgBIAEoCSIuChVTZXR0aW5nc1VwZGF0ZVJlcXVlc3QSFQoNc2V0dGluZ3NfanNvbhgBIAEoCSI7ChZTZXR0aW5nc1VwZGF0ZVJlc3BvbnNlEhAKCGFjY2VwdGVkGAEgASgIEg8KB21lc3NhZ2UYAiABKAkiPgogU2V0dGluZ3NSZXNldFNzaEtub3duSG9zdFJlcXVlc3QSDAoEaG9zdBgBIAEoCRIMCgRwb3J0GAIgASgNIjQKIVNldHRpbmdzUmVzZXRTc2hLbm93bkhvc3RSZXNwb25zZRIPCgdkZWxldGVkGAEgASgNIioKEVNldHRpbmdzU3luY0V2ZW50EhUKDXNldHRpbmdzX2pzb24YASABKAkiFwoVU2tpbGxGaWxlc0xpc3RSZXF1ZXN0IkwKFlNraWxsRmlsZXNMaXN0UmVzcG9uc2USEAoIcm9vdF9kaXIYASABKAkSDQoFcGF0aHMYAiADKAkSEQoJdHJ1bmNhdGVkGAMgASgIIigKGFNraWxsTWV0YWRhdGFSZWFkUmVxdWVzdBIMCgRwYXRoGAEgASgJIj4KGVNraWxsTWV0YWRhdGFSZWFkUmVzcG9uc2USDAoEbmFtZRgBIAEoCRITCgtkZXNjcmlwdGlvbhgCIAEoCSJEChRTa2lsbFRleHRSZWFkUmVxdWVzdBIMCgRwYXRoGAEgASgJEg4KBm9mZnNldBgCIAEoDRIOCgZsZW5ndGgYAyABKA0iOwoVU2tpbGxUZXh0UmVhZFJlc3BvbnNlEg8KB2NvbnRlbnQYASABKAkSEQoJdHJ1bmNhdGVkGAIgASgIIioKElNraWxsTWFuYWdlUmVxdWVzdBIUCgxwYXlsb2FkX2pzb24YASABKAkiKgoTU2tpbGxNYW5hZ2VSZXNwb25zZRITCgtyZXN1bHRfanNvbhgBIAEoCSJ3ChZGaWxlTWVudGlvbkxpc3RSZXF1ZXN0Eg8KB3dvcmtkaXIYASABKAkSEwoLbWF4X3Jlc3VsdHMYAiABKA0SDQoFcXVlcnkYAyABKAkSGAoLc2hvd19oaWRkZW4YBCABKAhIAIgBAUIOCgxfc2hvd19oaWRkZW4iPgoQRmlsZU1lbnRpb25FbnRyeRIMCgRwYXRoGAEgASgJEgwKBGtpbmQYAiABKAkSDgoGaGlkZGVuGAMgASgIImUKF0ZpbGVNZW50aW9uTGlzdFJlc3BvbnNlEjcKB2VudHJpZXMYASADKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5GaWxlTWVudGlvbkVudHJ5EhEKCXRydW5jYXRlZBgCIAEoCCI/CgZGc1Jvb3QSCgoCaWQYASABKAkSDAoEcGF0aBgCIAEoCRIMCgRraW5kGAMgASgJEg0KBWxhYmVsGAQgASgJIhAKDkZzUm9vdHNSZXF1ZXN0Ij4KD0ZzUm9vdHNSZXNwb25zZRIrCgVyb290cxgBIAMoCzIcLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzUm9vdCI2ChFGc0xpc3REaXJzUmVxdWVzdBIMCgRwYXRoGAEgASgJEhMKC21heF9yZXN1bHRzGAIgASgNIigKCkZzRGlyRW50cnkSDAoEcGF0aBgBIAEoCRIMCgRuYW1lGAIgASgJImgKEkZzTGlzdERpcnNSZXNwb25zZRIMCgRwYXRoGAEgASgJEjEKB2VudHJpZXMYAiADKAsyIC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc0RpckVudHJ5EhEKCXRydW5jYXRlZBgDIAEoCCI8ChxGc0NyZWF0ZVByb2plY3RGb2xkZXJSZXF1ZXN0Eg4KBnBhcmVudBgBIAEoCRIMCgRuYW1lGAIgASgJIi0KHUZzQ3JlYXRlUHJvamVjdEZvbGRlclJlc3BvbnNlEgwKBHBhdGgYASABKAkijAEKDUZzTGlzdFJlcXVlc3QSDwoHd29ya2RpchgBIAEoCRIMCgRwYXRoGAIgASgJEg0KBWRlcHRoGAMgASgNEg4KBm9mZnNldBgEIAEoDRITCgttYXhfcmVzdWx0cxgFIAEoDRIYCgtzaG93X2hpZGRlbhgGIAEoCEgAiAEBQg4KDF9zaG93X2hpZGRlbiI5CgtGc0xpc3RFbnRyeRIMCgRwYXRoGAEgASgJEgwKBGtpbmQYAiABKAkSDgoGaGlkZGVuGAMgASgIIrkBCg5Gc0xpc3RSZXNwb25zZRIMCgRwYXRoGAEgASgJEhAKCGhhc19wYXRoGAIgASgIEg0KBWRlcHRoGAMgASgNEg4KBm9mZnNldBgEIAEoDRITCgttYXhfcmVzdWx0cxgFIAEoDRINCgV0b3RhbBgGIAEoDRIQCghoYXNfbW9yZRgHIAEoCBIyCgdlbnRyaWVzGAggAygLMiEubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNMaXN0RW50cnkiOgoZRnNSZWFkRWRpdGFibGVUZXh0UmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEgwKBHBhdGgYAiABKAkijAEKGkZzUmVhZEVkaXRhYmxlVGV4dFJlc3BvbnNlEgwKBHBhdGgYASABKAkSDwoHY29udGVudBgCIAEoCRIQCghtdGltZV9tcxgDIAEoBBIUCgxjb250ZW50X2hhc2gYBCABKAkSEgoKc2l6ZV9ieXRlcxgFIAEoBBITCgt0b3RhbF9saW5lcxgGIAEoBCI8ChtGc1JlYWRXb3Jrc3BhY2VJbWFnZVJlcXVlc3QSDwoHd29ya2RpchgBIAEoCRIMCgRwYXRoGAIgASgJIokBChxGc1JlYWRXb3Jrc3BhY2VJbWFnZVJlc3BvbnNlEgwKBHBhdGgYASABKAkSEQoJbWltZV90eXBlGAIgASgJEgwKBGRhdGEYAyABKAkSEgoKc2l6ZV9ieXRlcxgEIAEoBBIQCghtdGltZV9tcxgFIAEoBBIUCgxjb250ZW50X2hhc2gYBiABKAki2wEKE0NoYXRGaWxlT3BlblJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEg8KB3dvcmtkaXIYAiABKAkSDAoEcGF0aBgDIAEoCRIOCgZzb3VyY2UYBCABKAkSEQoEbGluZRgFIAEoDUgAiAEBEhUKCGVuZF9saW5lGAYgASgNSAGIAQESEwoGY29sdW1uGAcgASgNSAKIAQESHAoUb3Blbl9pbl9maWxlX21hbmFnZXIYCCABKAhCBwoFX2xpbmVCCwoJX2VuZF9saW5lQgkKB19jb2x1bW4izgEKFENoYXRGaWxlT3BlblJlc3BvbnNlEg4KBmFjdGlvbhgBIAEoCRIMCgRraW5kGAIgASgJEg8KB3dvcmtkaXIYAyABKAkSDAoEcGF0aBgEIAEoCRIRCgRsaW5lGAUgASgNSACIAQESFQoIZW5kX2xpbmUYBiABKA1IAYgBARITCgZjb2x1bW4YByABKA1IAogBARIZChFvdXRzaWRlX3dvcmtzcGFjZRgIIAEoCEIHCgVfbGluZUILCglfZW5kX2xpbmVCCQoHX2NvbHVtbiLOAQoSRnNXcml0ZVRleHRSZXF1ZXN0Eg8KB3dvcmtkaXIYASABKAkSDAoEcGF0aBgCIAEoCRIPCgdjb250ZW50GAMgASgJEgwKBG1vZGUYBCABKAkSGQoRZXhwZWN0ZWRfbXRpbWVfbXMYBSABKAQSHQoVZXhwZWN0ZWRfY29udGVudF9oYXNoGAYgASgJEh0KFWhhc19leHBlY3RlZF9tdGltZV9tcxgHIAEoCBIhChloYXNfZXhwZWN0ZWRfY29udGVudF9oYXNoGAggASgIIp0BChNGc1dyaXRlVGV4dFJlc3BvbnNlEgwKBHBhdGgYASABKAkSDAoEbW9kZRgCIAEoCRIWCg5leGlzdGVkX2JlZm9yZRgDIAEoCBIVCg1ieXRlc193cml0dGVuGAQgASgEEhAKCG10aW1lX21zGAUgASgEEhQKDGNvbnRlbnRfaGFzaBgGIAEoCRITCgt0b3RhbF9saW5lcxgHIAEoBCIzChJGc0NyZWF0ZURpclJlcXVlc3QSDwoHd29ya2RpchgBIAEoCRIMCgRwYXRoGAIgASgJIjEKE0ZzQ3JlYXRlRGlyUmVzcG9uc2USDAoEcGF0aBgBIAEoCRIMCgRraW5kGAIgASgJIkYKD0ZzUmVuYW1lUmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEhEKCWZyb21fcGF0aBgCIAEoCRIPCgd0b19wYXRoGAMgASgJIkEKEEZzUmVuYW1lUmVzcG9uc2USEQoJZnJvbV9wYXRoGAEgASgJEgwKBHBhdGgYAiABKAkSDAoEa2luZBgDIAEoCSIwCg9Gc0RlbGV0ZVJlcXVlc3QSDwoHd29ya2RpchgBIAEoCRIMCgRwYXRoGAIgASgJIi4KEEZzRGVsZXRlUmVzcG9uc2USDAoEcGF0aBgBIAEoCRIMCgRraW5kGAIgASgJIiAKC1BpbmdSZXF1ZXN0EhEKCXRpbWVzdGFtcBgBIAEoAyIhCgxQb25nUmVzcG9uc2USEQoJdGltZXN0YW1wGAEgASgDIi4KDUVycm9yUmVzcG9uc2USDAoEY29kZRgBIAEoBRIPCgdtZXNzYWdlGAIgASgJImsKFVByb3ZpZGVyTW9kZWxzUmVxdWVzdBIVCg1wcm92aWRlcl90eXBlGAEgASgJEhAKCGJhc2VfdXJsGAIgASgJEg8KB2FwaV9rZXkYAyABKAkSGAoQdXNlX3N5c3RlbV9wcm94eRgEIAEoCCItChZQcm92aWRlck1vZGVsc1Jlc3BvbnNlEhMKC21vZGVsc19qc29uGAEgASgJIlEKFFByb3ZpZGVyVXNhZ2VSZXF1ZXN0EhMKC3Byb3ZpZGVyX2lkGAEgASgJEg8KB3JlZnJlc2gYAiABKAgSEwoLY29uZmlnX2pzb24YAyABKAkiLAoVUHJvdmlkZXJVc2FnZVJlc3BvbnNlEhMKC3Jlc3VsdF9qc29uGAEgASgJIogBChBDaGF0SW5ncmVzc0JhdGNoEg4KBnJ1bl9pZBgBIAEoCRIXCg9jb252ZXJzYXRpb25faWQYAiABKAkSEQoJZmlyc3Rfc2VxGAMgASgEEjgKB3JlY29yZHMYBCADKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0SW5ncmVzc1JlY29yZCKaAgoRQ2hhdEluZ3Jlc3NSZWNvcmQSNwoFZGVsdGEYASABKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0SW5ncmVzc0RlbHRhSAASQQoKY2hlY2twb2ludBgCIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRJbmdyZXNzQ2hlY2twb2ludEgAEj0KCHRlcm1pbmFsGAMgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEluZ3Jlc3NUZXJtaW5hbEgAEj8KCWhlYXJ0YmVhdBgEIAEoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRJbmdyZXNzSGVhcnRiZWF0SABCCQoHcGF5bG9hZCI5ChBDaGF0SW5ncmVzc0RlbHRhEhIKCmV2ZW50X2pzb24YASABKAkSEQoJd29ya2VyX2lkGAIgASgJIioKFENoYXRJbmdyZXNzSGVhcnRiZWF0EhIKCnVwZGF0ZWRfYXQYASABKAMixAEKFUNoYXRJbmdyZXNzQ2hlY2twb2ludBIaChJjb3ZlcnNfdGhyb3VnaF9zZXEYASABKAQSEAoIcmV2aXNpb24YAiABKAQSHQoVY29tcHJlc3NlZF9wcm9qZWN0aW9uGAMgASgMEhoKEnVuY29tcHJlc3NlZF9ieXRlcxgEIAEoBBIOCgZzaGEyNTYYBSABKAkSGAoQY29udGVudF9jb21wbGV0ZRgGIAEoCBIYChBoaXN0b3J5X3JlcXVpcmVkGAcgASgIIvwBChNDaGF0SW5ncmVzc1Rlcm1pbmFsEhoKEmNvdmVyc190aHJvdWdoX3NlcRgBIAEoBBIQCghyZXZpc2lvbhgCIAEoBBIdChVjb21wcmVzc2VkX3Byb2plY3Rpb24YAyABKAwSGgoSdW5jb21wcmVzc2VkX2J5dGVzGAQgASgEEg4KBnNoYTI1NhgFIAEoCRIYChBjb250ZW50X2NvbXBsZXRlGAYgASgIEhgKEGhpc3RvcnlfcmVxdWlyZWQYByABKAgSDQoFc3RhdGUYCCABKAkSEgoKZXJyb3JfY29kZRgJIAEoCRIVCg1lcnJvcl9tZXNzYWdlGAogASgJIk0KEUNoYXRJbmdyZXNzUmVzdW1lEjgKBHJ1bnMYASADKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0SW5ncmVzc1J1blJlc3VtZSLVAQoUQ2hhdEluZ3Jlc3NSdW5SZXN1bWUSDgoGcnVuX2lkGAEgASgJEhcKD2NvbnZlcnNhdGlvbl9pZBgCIAEoCRIXCg9yZXBsYXlfZnJvbV9zZXEYAyABKAQSGgoScmVwbGF5X3Rocm91Z2hfc2VxGAQgASgEEhAKCG5leHRfc2VxGAUgASgEEh0KFWxhdGVzdF9jaGVja3BvaW50X3NlcRgGIAEoBBIUCgx0ZXJtaW5hbF9zZXEYByABKAQSGAoQdGVybWluYWxfcGVuZGluZxgIIAEoCCLOAQoTQ2hhdEluZ3Jlc3NGcmFnbWVudBIOCgZydW5faWQYASABKAkSFwoPY29udmVyc2F0aW9uX2lkGAIgASgJEhIKCnNvdXJjZV9zZXEYAyABKAQSFgoOZnJhZ21lbnRfaW5kZXgYBCABKA0SFgoOZnJhZ21lbnRfY291bnQYBSABKA0SHAoUZW5jb2RlZF9yZWNvcmRfY2h1bmsYBiABKAwSHAoUZW5jb2RlZF9yZWNvcmRfYnl0ZXMYByABKAQSDgoGc2hhMjU2GAggASgJItwCCg5DaGF0SW5ncmVzc0FjaxIOCgZydW5faWQYASABKAkSFwoPY29udmVyc2F0aW9uX2lkGAIgASgJEhkKEWNvbW1pdHRlZF90aHJvdWdoGAMgASgEEhUKDWV4cGVjdGVkX25leHQYBCABKAQSOwoGYWN0aW9uGAUgASgOMisubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEluZ3Jlc3NBY2suQWN0aW9uEhoKEnRlcm1pbmFsX2NvbW1pdHRlZBgGIAEoCBISCgplcnJvcl9jb2RlGAcgASgJEhUKDWVycm9yX21lc3NhZ2UYCCABKAkiawoGQWN0aW9uEhYKEkFDVElPTl9VTlNQRUNJRklFRBAAEgwKCENPTlRJTlVFEAESGAoUUkVQTEFZX0ZST01fRVhQRUNURUQQAhITCg9TRU5EX0NIRUNLUE9JTlQQAxIMCghSRUpFQ1RFRBAEKsYECg9UdW5uZWxGcmFtZUtpbmQSIQodVFVOTkVMX0ZSQU1FX0tJTkRfVU5TUEVDSUZJRUQQABIoCiRUVU5ORUxfRlJBTUVfS0lORF9IVFRQX1JFUVVFU1RfU1RBUlQQARInCiNUVU5ORUxfRlJBTUVfS0lORF9IVFRQX1JFUVVFU1RfQk9EWRACEiYKIlRVTk5FTF9GUkFNRV9LSU5EX0hUVFBfUkVRVUVTVF9FTkQQAxIpCiVUVU5ORUxfRlJBTUVfS0lORF9IVFRQX1JFU1BPTlNFX1NUQVJUEAQSKAokVFVOTkVMX0ZSQU1FX0tJTkRfSFRUUF9SRVNQT05TRV9CT0RZEAUSJwojVFVOTkVMX0ZSQU1FX0tJTkRfSFRUUF9SRVNQT05TRV9FTkQQBhIdChlUVU5ORUxfRlJBTUVfS0lORF9XU19ESUFMEAcSIAocVFVOTkVMX0ZSQU1FX0tJTkRfV1NfRElBTF9PSxAIEiMKH1RVTk5FTF9GUkFNRV9LSU5EX1dTX0RJQUxfRVJST1IQCRIeChpUVU5ORUxfRlJBTUVfS0lORF9XU19GUkFNRRAKEh4KGlRVTk5FTF9GUkFNRV9LSU5EX1dTX0NMT1NFEAsSGwoXVFVOTkVMX0ZSQU1FX0tJTkRfRVJST1IQDBIcChhUVU5ORUxfRlJBTUVfS0lORF9DQU5DRUwQDRIaChZUVU5ORUxfRlJBTUVfS0lORF9QSU5HEA4SGgoWVFVOTkVMX0ZSQU1FX0tJTkRfUE9ORxAPKoEBChNUdW5uZWxXc01lc3NhZ2VUeXBlEiYKIlRVTk5FTF9XU19NRVNTQUdFX1RZUEVfVU5TUEVDSUZJRUQQABIfChtUVU5ORUxfV1NfTUVTU0FHRV9UWVBFX1RFWFQQARIhCh1UVU5ORUxfV1NfTUVTU0FHRV9UWVBFX0JJTkFSWRACQkBaPmdpdGh1Yi5jb20vbGl2ZWFnZW50L2FnZW50LWdhdGV3YXkvaW50ZXJuYWwvcHJvdG8vdjI7Z2F0ZXdheXYyYgZwcm90bzM"); /** * @generated from message liveagent.gateway.v2.GatewayEnvelope @@ -317,6 +317,12 @@ export type GatewayEnvelope = Message<"liveagent.gateway.v2.GatewayEnvelope"> & */ value: ProviderUsageRequest; case: "providerUsage"; + } | { + /** + * @generated from field: liveagent.gateway.v2.ChatFileOpenRequest chat_file_open = 94; + */ + value: ChatFileOpenRequest; + case: "chatFileOpen"; } | { case: undefined; value?: undefined }; }; @@ -704,6 +710,12 @@ export type AgentEnvelope = Message<"liveagent.gateway.v2.AgentEnvelope"> & { */ value: ChatIngressFragment; case: "chatIngressFragment"; + } | { + /** + * @generated from field: liveagent.gateway.v2.ChatFileOpenResponse chat_file_open_resp = 98; + */ + value: ChatFileOpenResponse; + case: "chatFileOpenResp"; } | { /** * @generated from field: liveagent.gateway.v2.ErrorResponse error = 99; @@ -4855,6 +4867,110 @@ export type FsReadWorkspaceImageResponse = Message<"liveagent.gateway.v2.FsReadW export const FsReadWorkspaceImageResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_proto_v2_gateway, 123); +/** + * @generated from message liveagent.gateway.v2.ChatFileOpenRequest + */ +export type ChatFileOpenRequest = Message<"liveagent.gateway.v2.ChatFileOpenRequest"> & { + /** + * @generated from field: string conversation_id = 1; + */ + conversationId: string; + + /** + * @generated from field: string workdir = 2; + */ + workdir: string; + + /** + * @generated from field: string path = 3; + */ + path: string; + + /** + * @generated from field: string source = 4; + */ + source: string; + + /** + * @generated from field: optional uint32 line = 5; + */ + line?: number | undefined; + + /** + * @generated from field: optional uint32 end_line = 6; + */ + endLine?: number | undefined; + + /** + * @generated from field: optional uint32 column = 7; + */ + column?: number | undefined; + + /** + * @generated from field: bool open_in_file_manager = 8; + */ + openInFileManager: boolean; +}; + +/** + * Describes the message liveagent.gateway.v2.ChatFileOpenRequest. + * Use `create(ChatFileOpenRequestSchema)` to create a new message. + */ +export const ChatFileOpenRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_proto_v2_gateway, 124); + +/** + * @generated from message liveagent.gateway.v2.ChatFileOpenResponse + */ +export type ChatFileOpenResponse = Message<"liveagent.gateway.v2.ChatFileOpenResponse"> & { + /** + * @generated from field: string action = 1; + */ + action: string; + + /** + * @generated from field: string kind = 2; + */ + kind: string; + + /** + * @generated from field: string workdir = 3; + */ + workdir: string; + + /** + * @generated from field: string path = 4; + */ + path: string; + + /** + * @generated from field: optional uint32 line = 5; + */ + line?: number | undefined; + + /** + * @generated from field: optional uint32 end_line = 6; + */ + endLine?: number | undefined; + + /** + * @generated from field: optional uint32 column = 7; + */ + column?: number | undefined; + + /** + * @generated from field: bool outside_workspace = 8; + */ + outsideWorkspace: boolean; +}; + +/** + * Describes the message liveagent.gateway.v2.ChatFileOpenResponse. + * Use `create(ChatFileOpenResponseSchema)` to create a new message. + */ +export const ChatFileOpenResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_proto_v2_gateway, 125); + /** * @generated from message liveagent.gateway.v2.FsWriteTextRequest */ @@ -4905,7 +5021,7 @@ export type FsWriteTextRequest = Message<"liveagent.gateway.v2.FsWriteTextReques * Use `create(FsWriteTextRequestSchema)` to create a new message. */ export const FsWriteTextRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 124); + messageDesc(file_proto_v2_gateway, 126); /** * @generated from message liveagent.gateway.v2.FsWriteTextResponse @@ -4952,7 +5068,7 @@ export type FsWriteTextResponse = Message<"liveagent.gateway.v2.FsWriteTextRespo * Use `create(FsWriteTextResponseSchema)` to create a new message. */ export const FsWriteTextResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 125); + messageDesc(file_proto_v2_gateway, 127); /** * @generated from message liveagent.gateway.v2.FsCreateDirRequest @@ -4974,7 +5090,7 @@ export type FsCreateDirRequest = Message<"liveagent.gateway.v2.FsCreateDirReques * Use `create(FsCreateDirRequestSchema)` to create a new message. */ export const FsCreateDirRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 126); + messageDesc(file_proto_v2_gateway, 128); /** * @generated from message liveagent.gateway.v2.FsCreateDirResponse @@ -4996,7 +5112,7 @@ export type FsCreateDirResponse = Message<"liveagent.gateway.v2.FsCreateDirRespo * Use `create(FsCreateDirResponseSchema)` to create a new message. */ export const FsCreateDirResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 127); + messageDesc(file_proto_v2_gateway, 129); /** * @generated from message liveagent.gateway.v2.FsRenameRequest @@ -5023,7 +5139,7 @@ export type FsRenameRequest = Message<"liveagent.gateway.v2.FsRenameRequest"> & * Use `create(FsRenameRequestSchema)` to create a new message. */ export const FsRenameRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 128); + messageDesc(file_proto_v2_gateway, 130); /** * @generated from message liveagent.gateway.v2.FsRenameResponse @@ -5050,7 +5166,7 @@ export type FsRenameResponse = Message<"liveagent.gateway.v2.FsRenameResponse"> * Use `create(FsRenameResponseSchema)` to create a new message. */ export const FsRenameResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 129); + messageDesc(file_proto_v2_gateway, 131); /** * @generated from message liveagent.gateway.v2.FsDeleteRequest @@ -5072,7 +5188,7 @@ export type FsDeleteRequest = Message<"liveagent.gateway.v2.FsDeleteRequest"> & * Use `create(FsDeleteRequestSchema)` to create a new message. */ export const FsDeleteRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 130); + messageDesc(file_proto_v2_gateway, 132); /** * @generated from message liveagent.gateway.v2.FsDeleteResponse @@ -5094,7 +5210,7 @@ export type FsDeleteResponse = Message<"liveagent.gateway.v2.FsDeleteResponse"> * Use `create(FsDeleteResponseSchema)` to create a new message. */ export const FsDeleteResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 131); + messageDesc(file_proto_v2_gateway, 133); /** * @generated from message liveagent.gateway.v2.PingRequest @@ -5111,7 +5227,7 @@ export type PingRequest = Message<"liveagent.gateway.v2.PingRequest"> & { * Use `create(PingRequestSchema)` to create a new message. */ export const PingRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 132); + messageDesc(file_proto_v2_gateway, 134); /** * @generated from message liveagent.gateway.v2.PongResponse @@ -5128,7 +5244,7 @@ export type PongResponse = Message<"liveagent.gateway.v2.PongResponse"> & { * Use `create(PongResponseSchema)` to create a new message. */ export const PongResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 133); + messageDesc(file_proto_v2_gateway, 135); /** * @generated from message liveagent.gateway.v2.ErrorResponse @@ -5150,7 +5266,7 @@ export type ErrorResponse = Message<"liveagent.gateway.v2.ErrorResponse"> & { * Use `create(ErrorResponseSchema)` to create a new message. */ export const ErrorResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 134); + messageDesc(file_proto_v2_gateway, 136); /** * @generated from message liveagent.gateway.v2.ProviderModelsRequest @@ -5182,7 +5298,7 @@ export type ProviderModelsRequest = Message<"liveagent.gateway.v2.ProviderModels * Use `create(ProviderModelsRequestSchema)` to create a new message. */ export const ProviderModelsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 135); + messageDesc(file_proto_v2_gateway, 137); /** * @generated from message liveagent.gateway.v2.ProviderModelsResponse @@ -5199,7 +5315,7 @@ export type ProviderModelsResponse = Message<"liveagent.gateway.v2.ProviderModel * Use `create(ProviderModelsResponseSchema)` to create a new message. */ export const ProviderModelsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 136); + messageDesc(file_proto_v2_gateway, 138); /** * @generated from message liveagent.gateway.v2.ProviderUsageRequest @@ -5229,7 +5345,7 @@ export type ProviderUsageRequest = Message<"liveagent.gateway.v2.ProviderUsageRe * Use `create(ProviderUsageRequestSchema)` to create a new message. */ export const ProviderUsageRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 137); + messageDesc(file_proto_v2_gateway, 139); /** * @generated from message liveagent.gateway.v2.ProviderUsageResponse @@ -5246,7 +5362,7 @@ export type ProviderUsageResponse = Message<"liveagent.gateway.v2.ProviderUsageR * Use `create(ProviderUsageResponseSchema)` to create a new message. */ export const ProviderUsageResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 138); + messageDesc(file_proto_v2_gateway, 140); /** * ChatIngressBatch carries contiguous logical records for one run. Each record @@ -5281,7 +5397,7 @@ export type ChatIngressBatch = Message<"liveagent.gateway.v2.ChatIngressBatch"> * Use `create(ChatIngressBatchSchema)` to create a new message. */ export const ChatIngressBatchSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 139); + messageDesc(file_proto_v2_gateway, 141); /** * @generated from message liveagent.gateway.v2.ChatIngressRecord @@ -5322,7 +5438,7 @@ export type ChatIngressRecord = Message<"liveagent.gateway.v2.ChatIngressRecord" * Use `create(ChatIngressRecordSchema)` to create a new message. */ export const ChatIngressRecordSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 140); + messageDesc(file_proto_v2_gateway, 142); /** * @generated from message liveagent.gateway.v2.ChatIngressDelta @@ -5344,7 +5460,7 @@ export type ChatIngressDelta = Message<"liveagent.gateway.v2.ChatIngressDelta"> * Use `create(ChatIngressDeltaSchema)` to create a new message. */ export const ChatIngressDeltaSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 141); + messageDesc(file_proto_v2_gateway, 143); /** * @generated from message liveagent.gateway.v2.ChatIngressHeartbeat @@ -5363,7 +5479,7 @@ export type ChatIngressHeartbeat = Message<"liveagent.gateway.v2.ChatIngressHear * Use `create(ChatIngressHeartbeatSchema)` to create a new message. */ export const ChatIngressHeartbeatSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 142); + messageDesc(file_proto_v2_gateway, 144); /** * @generated from message liveagent.gateway.v2.ChatIngressCheckpoint @@ -5410,7 +5526,7 @@ export type ChatIngressCheckpoint = Message<"liveagent.gateway.v2.ChatIngressChe * Use `create(ChatIngressCheckpointSchema)` to create a new message. */ export const ChatIngressCheckpointSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 143); + messageDesc(file_proto_v2_gateway, 145); /** * @generated from message liveagent.gateway.v2.ChatIngressTerminal @@ -5472,7 +5588,7 @@ export type ChatIngressTerminal = Message<"liveagent.gateway.v2.ChatIngressTermi * Use `create(ChatIngressTerminalSchema)` to create a new message. */ export const ChatIngressTerminalSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 144); + messageDesc(file_proto_v2_gateway, 146); /** * ChatIngressResume declares the replay window retained by the desktop after a @@ -5492,7 +5608,7 @@ export type ChatIngressResume = Message<"liveagent.gateway.v2.ChatIngressResume" * Use `create(ChatIngressResumeSchema)` to create a new message. */ export const ChatIngressResumeSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 145); + messageDesc(file_proto_v2_gateway, 147); /** * @generated from message liveagent.gateway.v2.ChatIngressRunResume @@ -5544,7 +5660,7 @@ export type ChatIngressRunResume = Message<"liveagent.gateway.v2.ChatIngressRunR * Use `create(ChatIngressRunResumeSchema)` to create a new message. */ export const ChatIngressRunResumeSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 146); + messageDesc(file_proto_v2_gateway, 148); /** * ChatIngressFragment transports one encoded ChatIngressRecord that exceeds a @@ -5600,7 +5716,7 @@ export type ChatIngressFragment = Message<"liveagent.gateway.v2.ChatIngressFragm * Use `create(ChatIngressFragmentSchema)` to create a new message. */ export const ChatIngressFragmentSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 147); + messageDesc(file_proto_v2_gateway, 149); /** * @generated from message liveagent.gateway.v2.ChatIngressAck @@ -5652,7 +5768,7 @@ export type ChatIngressAck = Message<"liveagent.gateway.v2.ChatIngressAck"> & { * Use `create(ChatIngressAckSchema)` to create a new message. */ export const ChatIngressAckSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 148); + messageDesc(file_proto_v2_gateway, 150); /** * @generated from enum liveagent.gateway.v2.ChatIngressAck.Action @@ -5688,7 +5804,7 @@ export enum ChatIngressAck_Action { * Describes the enum liveagent.gateway.v2.ChatIngressAck.Action. */ export const ChatIngressAck_ActionSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_proto_v2_gateway, 148, 0); + enumDesc(file_proto_v2_gateway, 150, 0); /** * @generated from enum liveagent.gateway.v2.TunnelFrameKind diff --git a/crates/agent-gateway/web/src/pages/chat/AssistantBubble.tsx b/crates/agent-gateway/web/src/pages/chat/AssistantBubble.tsx index cf595dce3..5758c463f 100644 --- a/crates/agent-gateway/web/src/pages/chat/AssistantBubble.tsx +++ b/crates/agent-gateway/web/src/pages/chat/AssistantBubble.tsx @@ -1,6 +1,7 @@ import { memo, useMemo } from "react"; import { ChangedFilesCard } from "../../components/chat/ChangedFilesCard"; import { collectChangedFiles } from "../../lib/chat/changedFiles"; +import type { ChatFileLink } from "../../lib/chat/chatFileLinks"; import type { UiRound } from "../../lib/chat/uiMessages"; import { AssistantAvatar } from "./assistant-bubble/AssistantAvatar"; import { RoundContent } from "./assistant-bubble/RoundContent"; @@ -34,6 +35,8 @@ export const AssistantBubble = memo(function AssistantBubble(props: { toolStatusVariant?: "default" | "compaction"; readOnly?: boolean; redactToolContent?: boolean; + workdir?: string; + onOpenFileLink?: (link: ChatFileLink) => void; }) { const { rounds, @@ -46,6 +49,8 @@ export const AssistantBubble = memo(function AssistantBubble(props: { toolStatusVariant, readOnly = false, redactToolContent = false, + workdir, + onOpenFileLink, } = props; const latestTodoItem = useMemo(() => { for (let roundIndex = rounds.length - 1; roundIndex >= 0; roundIndex -= 1) { @@ -92,6 +97,8 @@ export const AssistantBubble = memo(function AssistantBubble(props: { redactToolContent={redactToolContent} latestTodoItem={latestTodoItem} isAborted={isAborted} + workdir={workdir} + onOpenFileLink={onOpenFileLink} /> ))} {changedFiles ? : null} diff --git a/crates/agent-gateway/web/src/pages/chat/assistant-bubble/RoundContent.tsx b/crates/agent-gateway/web/src/pages/chat/assistant-bubble/RoundContent.tsx index 06ed3997c..9afdf1ad9 100644 --- a/crates/agent-gateway/web/src/pages/chat/assistant-bubble/RoundContent.tsx +++ b/crates/agent-gateway/web/src/pages/chat/assistant-bubble/RoundContent.tsx @@ -2,6 +2,7 @@ import { memo, useEffect, useMemo, useRef, useState } from "react"; import { ChevronRight, Lightbulb, RefreshCw } from "../../../components/icons"; import { Markdown } from "../../../components/Markdown"; import { useLocale } from "../../../i18n"; +import type { ChatFileLink } from "../../../lib/chat/chatFileLinks"; import { normalizeLiveToolStatus, VIBING_STATUS } from "../../../lib/chat/chatPageHelpers"; import type { RetryAttemptRecord } from "../../../lib/chat/transcript/types"; import type { ToolTraceItem, UiRound } from "../../../lib/chat/uiMessages"; @@ -21,11 +22,15 @@ const ThinkingBlock = memo(function ThinkingBlock({ open, isRunning, renderMode, + workdir, + onOpenFileLink, }: { text: string; open?: boolean; isRunning?: boolean; renderMode: "streaming" | "static"; + workdir?: string; + onOpenFileLink?: (link: ChatFileLink) => void; }) { const hasText = /\S/.test(text || ""); const { t } = useLocale(); @@ -70,6 +75,8 @@ const ThinkingBlock = memo(function ThinkingBlock({ className="thinking-markdown space-y-1.5" renderMode={renderMode} showCaret={false} + workdir={workdir} + onOpenFileLink={onOpenFileLink} />
)} @@ -146,6 +153,8 @@ export const RoundContent = memo(function RoundContent(props: { redactToolContent?: boolean; latestTodoItem?: ToolTraceItem | null; isAborted?: boolean; + workdir?: string; + onOpenFileLink?: (link: ChatFileLink) => void; }) { const { round, @@ -163,6 +172,8 @@ export const RoundContent = memo(function RoundContent(props: { redactToolContent = false, latestTodoItem, isAborted = false, + workdir, + onOpenFileLink, } = props; const groupedBlocks = useMemo(() => groupRoundBlocks(round.blocks), [round.blocks]); const visibleGroupedBlocks = useMemo( @@ -256,6 +267,8 @@ export const RoundContent = memo(function RoundContent(props: { open={autoOpenThinking && block.key === latestThinkingKey} isRunning={autoOpenThinking && block.key === latestThinkingKey} renderMode={renderMode ?? (isStreaming ? "streaming" : "static")} + workdir={workdir} + onOpenFileLink={onOpenFileLink} /> ); } @@ -335,6 +348,8 @@ export const RoundContent = memo(function RoundContent(props: { renderMode={renderMode} showCaret={Boolean(isLive && isActive && isStreaming)} readOnly={readOnly} + workdir={workdir} + onOpenFileLink={onOpenFileLink} /> ); })} diff --git a/crates/agent-gateway/web/src/shims/tauriCore.ts b/crates/agent-gateway/web/src/shims/tauriCore.ts index adc5ecb62..18db64699 100644 --- a/crates/agent-gateway/web/src/shims/tauriCore.ts +++ b/crates/agent-gateway/web/src/shims/tauriCore.ts @@ -281,6 +281,18 @@ export async function invoke(command: string, args?: Record) String(args?.workdir ?? ""), String(args?.path ?? ""), )) as T; + case "open_chat_file_link": + return (await getGatewayWebSocketClient(loadToken().trim()).openChatFile({ + conversationId: String(args?.conversation_id ?? ""), + workdir: String(args?.workdir ?? ""), + path: String(args?.path ?? ""), + source: String(args?.source ?? ""), + line: typeof args?.line === "number" ? args.line : undefined, + endLine: typeof args?.end_line === "number" ? args.end_line : undefined, + column: typeof args?.column === "number" ? args.column : undefined, + openInFileManager: + typeof args?.open_in_file_manager === "boolean" ? args.open_in_file_manager : undefined, + })) as T; case "fs_create_dir": return (await getGatewayWebSocketClient(loadToken().trim()).createDir( String(args?.workdir ?? ""), diff --git a/crates/agent-gateway/web/test/chat-file-links.test.mjs b/crates/agent-gateway/web/test/chat-file-links.test.mjs new file mode 100644 index 000000000..dd12466dd --- /dev/null +++ b/crates/agent-gateway/web/test/chat-file-links.test.mjs @@ -0,0 +1,196 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; + +import { createWebModuleLoader } from "../../test/helpers/load-web-module.mjs"; + +const loader = createWebModuleLoader(); +const markdownLoader = createWebModuleLoader({ + mocks: { + "@streamdown/cjk": { cjk: {} }, + "@streamdown/code": { code: {} }, + "@streamdown/math": { math: {} }, + "@streamdown/mermaid": { mermaid: {} }, + streamdown: { + Streamdown(props) { + return { type: "Streamdown", props }; + }, + defaultRemarkPlugins: {}, + defaultRehypePlugins: { raw: () => {}, sanitize: [() => {}, {}], harden: () => {} }, + }, + "react-dom": { createPortal: (children, container) => ({ children, container }) }, + "../i18n": { useLocale: () => ({ t: (key) => key }) }, + "../lib/shared/utils": { cn: (...parts) => parts.filter(Boolean).join(" ") }, + "./icons": new Proxy( + {}, + { get: (_target, name) => (props) => ({ type: String(name), props }) }, + ), + "./ui/button": { Button: (props) => ({ type: "Button", props }) }, + }, +}); +const markdownModule = markdownLoader.loadModule("src/components/Markdown.tsx"); +const { + decodeChatFileLinkPayload, + encodeChatFileLink, + parseChatFileLink, +} = loader.loadModule("src/lib/chat/chatFileLinks.ts"); + +const validCases = [ + ["C:/work/src/a.ts", { path: "C:/work/src/a.ts", source: "absolute" }], + [String.raw`C:\work\src\a.ts`, { path: "C:/work/src/a.ts", source: "absolute" }], + [String.raw`C:\\project\\file.ts`, { path: "C:/project/file.ts", source: "absolute" }], + ["D:/other/a.ts", { path: "D:/other/a.ts", source: "absolute" }], + ["C:/work/src/a.ts:12", { path: "C:/work/src/a.ts", line: 12, source: "absolute" }], + [ + "C:/work/src/a.ts:12:4", + { path: "C:/work/src/a.ts", line: 12, column: 4, source: "absolute" }, + ], + ["C:/work/src/a.ts#L12", { path: "C:/work/src/a.ts", line: 12, source: "absolute" }], + [ + "C:/work/src/a.ts#L12-L20", + { path: "C:/work/src/a.ts", line: 12, endLine: 20, source: "absolute" }, + ], + ["C:/path with spaces/a.ts", { path: "C:/path with spaces/a.ts", source: "absolute" }], + ["C:/path%20with%20spaces/a.ts", { path: "C:/path with spaces/a.ts", source: "absolute" }], + ["C:/路径/文件.ts", { path: "C:/路径/文件.ts", source: "absolute" }], + ["file:///C:/work/a.ts", { path: "C:/work/a.ts", source: "file-url" }], + [ + "file:///C:/path%20with%20spaces/%E6%96%87%E4%BB%B6.ts#L7-L9", + { + path: "C:/path with spaces/文件.ts", + line: 7, + endLine: 9, + source: "file-url", + }, + ], + ["/home/user/work/a.ts", { path: "/home/user/work/a.ts", source: "absolute" }], + ["./src/a.ts", { path: "./src/a.ts", source: "relative" }], + ["../src/a.ts:3:2", { path: "../src/a.ts", line: 3, column: 2, source: "relative" }], + ["src/a.ts", { path: "src/a.ts", source: "relative" }], + ["README.md", { path: "README.md", source: "relative" }], + [ + String.raw`\\server\share\folder\a.ts:8`, + { path: "//server/share/folder/a.ts", line: 8, source: "absolute" }, + ], + [ + "file://server/share/folder/a.ts#L4", + { path: "//server/share/folder/a.ts", line: 4, source: "file-url" }, + ], +]; + +test("Gateway historical and streaming rows keep the explicit file-open prop chain", () => { + const files = [ + "../src/app/GatewayApp.tsx", + "../src/components/GatewayTranscript.tsx", + "../src/pages/chat/AssistantBubble.tsx", + "../src/pages/chat/assistant-bubble/RoundContent.tsx", + ]; + for (const relativePath of files) { + const source = fs.readFileSync(fileURLToPath(new URL(relativePath, import.meta.url)), "utf8"); + assert.match(source, /onOpenFileLink/, relativePath); + } + + const roundContent = fs.readFileSync( + fileURLToPath(new URL("../src/pages/chat/assistant-bubble/RoundContent.tsx", import.meta.url)), + "utf8", + ); + assert.match(roundContent, /isStreaming \? "streaming" : "static"/); + assert.ok((roundContent.match(/onOpenFileLink=\{onOpenFileLink\}/g) ?? []).length >= 3); + assert.ok((roundContent.match(/workdir=\{workdir\}/g) ?? []).length >= 3); + + const gatewayApp = fs.readFileSync( + fileURLToPath(new URL("../src/app/GatewayApp.tsx", import.meta.url)), + "utf8", + ); + assert.match(gatewayApp, /openInFileManager: true/); + assert.match(gatewayApp, /!result\.outsideWorkspace/); +}); + +test("escaped Markdown file links stay literal in Gateway Web", () => { + const source = String.raw`\[foo\*](README.md) and [foo*](README.md)`; + const tree = { + type: "root", + children: [ + { + type: "paragraph", + children: [ + { + type: "text", + value: "[foo*](README.md) and [foo*](README.md)", + position: { start: { offset: 0 }, end: { offset: source.length } }, + }, + ], + }, + ], + }; + + markdownModule.remarkChatFileLinks()(tree, { value: source }); + + assert.deepEqual(tree.children[0].children, [ + { type: "text", value: "[foo*](README.md) and " }, + { + type: "link", + url: "README.md", + children: [{ type: "text", value: "foo*" }], + }, + ]); +}); + +test("parseChatFileLink supports the cross-platform chat path matrix", () => { + for (const [input, expected] of validCases) { + assert.deepEqual(parseChatFileLink(input), expected, input); + } +}); + +test("parseChatFileLink rejects external, dangerous, internal, and malformed targets", () => { + for (const input of [ + "javascript:alert(1)", + "data:text/html,", + "vbscript:msgbox(1)", + "https://example.com/a.ts", + "http://example.com/a.ts", + "mailto:user@example.com", + "liveagent-file:path=C%3A%2Fa.ts&source=absolute", + "C:drive-relative.txt", + "C:/work/a.ts:4294967296", + "file:///C:/work/a.ts#L999999999999999999999999", + "#L12", + "", + ]) { + assert.equal(parseChatFileLink(input), null, input); + } + + assert.doesNotThrow(() => parseChatFileLink("file:///%E0%A4%A")); +}); + +test("the internal payload codec preserves locations and rejects malformed payloads", () => { + const original = { + path: "C:/路径/a file.ts", + line: 12, + endLine: 20, + column: 4, + source: "absolute", + }; + const encoded = encodeChatFileLink(original); + assert.match(encoded, /^liveagent-file:/); + assert.deepEqual( + decodeChatFileLinkPayload(encoded.slice("liveagent-file:".length)), + original, + ); + + for (const payload of [ + "path=&source=absolute", + "v=1&path=&source=absolute", + "v=1&path=C%3A%2Fa.ts&source=unknown", + "v=1&path=C%3A%2Fa.ts&source=absolute&line=0", + "v=1&path=C%3A%2Fa.ts&source=absolute&line=-1", + "v=1&path=C%3A%2Fa.ts&source=absolute&line=abc", + "v=1&path=C%3A%2Fa.ts&source=absolute&line=4294967296", + "v=1&path=C%3A%2Fa.ts&source=absolute&line=20&endLine=12", + "v=1&path=C%3A%2Fa.ts&path=D%3A%2Fb.ts&source=absolute", + "v=1&path=C%3A%2Fa.ts&source=absolute&extra=true", + ]) { + assert.equal(decodeChatFileLinkPayload(payload), null, payload); + } +}); diff --git a/crates/agent-gateway/web/test/gateway-v2-adapters.test.mjs b/crates/agent-gateway/web/test/gateway-v2-adapters.test.mjs index 71548ea7e..ab2eb253f 100644 --- a/crates/agent-gateway/web/test/gateway-v2-adapters.test.mjs +++ b/crates/agent-gateway/web/test/gateway-v2-adapters.test.mjs @@ -26,6 +26,81 @@ function decodeClientFrame(bytes) { return pb.fromBinary(v2.WebClientFrameSchema, bytes); } +test("chat file open requests remain agent-scoped and preserve source locations", () => { + const encoded = encodeRequestFrame( + "file-open-1", + "chat.file_open", + { + conversation_id: "conversation-1", + workdir: "C:/work", + path: "src/a.ts", + source: "relative", + line: 12, + end_line: 20, + column: 4, + open_in_file_manager: false, + }, + "agent-1", + ); + const frame = decodeClientFrame(encoded); + assert.equal(frame.agentId, "agent-1"); + assert.equal(frame.payload.case, "agentRequest"); + assert.equal(frame.payload.value.payload.case, "chatFileOpen"); + assert.deepEqual( + { + conversationId: frame.payload.value.payload.value.conversationId, + workdir: frame.payload.value.payload.value.workdir, + path: frame.payload.value.payload.value.path, + source: frame.payload.value.payload.value.source, + line: frame.payload.value.payload.value.line, + endLine: frame.payload.value.payload.value.endLine, + column: frame.payload.value.payload.value.column, + }, + { + conversationId: "conversation-1", + workdir: "C:/work", + path: "src/a.ts", + source: "relative", + line: 12, + endLine: 20, + column: 4, + }, + ); + + const decoded = decodeServerFrame( + roundtrip( + serverFrame({ + request_id: "file-open-1", + agent_id: "agent-1", + agent_response: { + request_id: "file-open-1", + chat_file_open_resp: { + action: "editor", + kind: "file", + workdir: "C:/work", + path: "src/a.ts", + line: 12, + end_line: 20, + column: 4, + }, + }, + }), + ), + { agentOnline: true }, + ); + assert.equal(decoded.kind, "response"); + assert.deepEqual(decoded.payload, { + action: "editor", + kind: "file", + workdir: "C:/work", + path: "src/a.ts", + line: 12, + endLine: 20, + column: 4, + outsideWorkspace: false, + }); +}); + test("adapters convert int64/uint64 fields to Number at realistic maxima", () => { // 毫秒时间戳(2100 年)与 MAX_SAFE_INTEGER 边界都必须无损转换。 const year2100Ms = 4102444800000; diff --git a/crates/agent-gui/src-tauri/src/commands/mod.rs b/crates/agent-gui/src-tauri/src/commands/mod.rs index 74577d21a..78b270fb5 100644 --- a/crates/agent-gui/src-tauri/src/commands/mod.rs +++ b/crates/agent-gui/src-tauri/src/commands/mod.rs @@ -37,6 +37,7 @@ pub use runtime_commands::sftp; pub use runtime_commands::shell; pub use runtime_commands::terminal; +pub use workspace_commands::chat_file_links; pub use workspace_commands::fs; pub use workspace_commands::git; pub use workspace_commands::subagent_worktree; diff --git a/crates/agent-gui/src-tauri/src/commands/workspace/chat_file_links.rs b/crates/agent-gui/src-tauri/src/commands/workspace/chat_file_links.rs new file mode 100644 index 000000000..fbea8edca --- /dev/null +++ b/crates/agent-gui/src-tauri/src/commands/workspace/chat_file_links.rs @@ -0,0 +1,878 @@ +use serde::Serialize; +use std::fs; +use std::io::Read; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, LazyLock}; +use std::time::Duration; +use tokio::sync::Semaphore; + +use super::fs::spawn_workspace_open_command; +use crate::commands::chat_history; +use crate::runtime::platform::expand_tilde_path; + +const TEXT_EXTENSIONS: &[&str] = &[ + "adoc", + "astro", + "bash", + "bat", + "c", + "cc", + "cfg", + "clj", + "cljc", + "cljs", + "cmake", + "cmd", + "conf", + "cpp", + "cs", + "css", + "cts", + "cxx", + "dart", + "dockerfile", + "editorconfig", + "env", + "erl", + "ex", + "exs", + "fish", + "fs", + "fsx", + "gitattributes", + "gitignore", + "go", + "gradle", + "graphql", + "gql", + "h", + "hh", + "hpp", + "hrl", + "hs", + "htm", + "html", + "hxx", + "ini", + "java", + "jl", + "js", + "json", + "json5", + "jsonc", + "jsx", + "kt", + "kts", + "less", + "lhs", + "log", + "lua", + "m", + "md", + "mdx", + "mjs", + "mm", + "mts", + "nim", + "php", + "pl", + "pm", + "properties", + "proto", + "ps1", + "psd1", + "psm1", + "py", + "pyi", + "pyw", + "r", + "rb", + "rs", + "rst", + "sass", + "scala", + "scss", + "sh", + "sol", + "sql", + "svelte", + "swift", + "tex", + "toml", + "ts", + "tsx", + "txt", + "vb", + "vbs", + "vue", + "wsf", + "xhtml", + "xml", + "yaml", + "yml", + "zsh", + "zig", +]; +const SCRIPT_EXTENSIONS: &[&str] = &[ + "bash", "bat", "cmd", "command", "fish", "hta", "js", "jse", "mjs", "cjs", "msh", "msh1", + "msh1xml", "msh2", "msh2xml", "mshxml", "ps1", "psd1", "psm1", "pssc", "py", "pyw", "rb", + "sct", "sh", "vbe", "vbs", "ws", "wsc", "wsf", "wsh", "zsh", +]; +const PREVIEW_EXTENSIONS: &[&str] = &[ + "avif", "bmp", "csv", "doc", "docx", "flac", "gif", "ico", "jpeg", "jpg", "m4a", "m4v", "mov", + "mp3", "mp4", "ods", "oga", "ogg", "ogv", "pdf", "png", "rtf", "svg", "tsv", "wav", "webm", + "webp", "xls", "xlsm", "xlsx", "xltm", "xltx", +]; +const CHAT_FILE_OPEN_TIMEOUT: Duration = Duration::from_secs(25); +const CHAT_FILE_OPEN_CONCURRENCY: usize = 4; +static CHAT_FILE_OPEN_SEMAPHORE: LazyLock> = + LazyLock::new(|| Arc::new(Semaphore::new(CHAT_FILE_OPEN_CONCURRENCY))); + +const EXECUTABLE_EXTENSIONS: &[&str] = &[ + "app", + "appimage", + "appref-ms", + "application", + "apk", + "bin", + "chm", + "com", + "cpl", + "crx", + "deb", + "desktop", + "dmg", + "exe", + "gadget", + "inf", + "ins", + "isp", + "jar", + "lnk", + "msc", + "msi", + "msp", + "pif", + "pkg", + "reg", + "rpm", + "scf", + "scr", + "shb", + "shs", + "url", + "webloc", + "workflow", + "xbap", + "xpi", +]; + +#[derive(Debug, Clone, Copy, PartialEq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum ChatFileLinkErrorCode { + InvalidRequest, + InvalidWorkdir, + NotFound, + UnsupportedTarget, + OpenFailed, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ChatFileLinkError { + pub code: ChatFileLinkErrorCode, + pub message: String, +} + +impl ChatFileLinkError { + fn new(code: ChatFileLinkErrorCode, message: impl Into) -> Self { + Self { + code, + message: message.into(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ChatFileLinkOpenResponse { + pub action: String, + pub kind: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub workdir: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub path: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub line: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub end_line: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub column: Option, + pub outside_workspace: bool, +} + +#[derive(Debug)] +struct ChatFileLinkPlan { + response: ChatFileLinkOpenResponse, + target: PathBuf, + system_mode: Option<&'static str>, +} + +fn display_path(path: &Path) -> String { + let normalized = path.to_string_lossy().replace('\\', "/"); + if let Some(rest) = normalized.strip_prefix("//?/UNC/") { + return format!("//{rest}"); + } + normalized + .strip_prefix("//?/") + .unwrap_or(&normalized) + .to_string() +} + +fn extension(path: &Path) -> String { + path.extension() + .and_then(|value| value.to_str()) + .unwrap_or_default() + .trim() + .to_ascii_lowercase() +} + +fn has_extension(path: &Path, extensions: &[&str]) -> bool { + let ext = extension(path); + extensions.iter().any(|candidate| *candidate == ext) +} + +#[cfg(target_os = "macos")] +fn is_active_directory(path: &Path) -> bool { + has_extension(path, EXECUTABLE_EXTENSIONS) +} + +#[cfg(not(target_os = "macos"))] +fn is_active_directory(_path: &Path) -> bool { + false +} + +fn has_shebang(path: &Path) -> bool { + let Ok(mut file) = fs::File::open(path) else { + return false; + }; + let mut prefix = [0_u8; 2]; + file.read_exact(&mut prefix).is_ok() && prefix == *b"#!" +} + +fn is_probably_text(path: &Path) -> bool { + if has_extension(path, TEXT_EXTENSIONS) || has_shebang(path) { + return true; + } + let Ok(mut file) = fs::File::open(path) else { + return false; + }; + let mut sample = vec![0_u8; 8192]; + let Ok(read) = file.read(&mut sample) else { + return false; + }; + sample.truncate(read); + !sample.contains(&0) && std::str::from_utf8(&sample).is_ok() +} + +#[cfg(unix)] +fn has_executable_permission(path: &Path, metadata: &fs::Metadata) -> bool { + use std::os::unix::fs::PermissionsExt; + let _ = path; + metadata.permissions().mode() & 0o111 != 0 +} + +#[cfg(not(unix))] +fn has_executable_permission(_path: &Path, _metadata: &fs::Metadata) -> bool { + false +} + +fn normalized_location( + line: Option, + end_line: Option, + column: Option, +) -> Result<(Option, Option, Option), ChatFileLinkError> { + if line == Some(0) + || end_line == Some(0) + || column == Some(0) + || (line.is_none() && (end_line.is_some() || column.is_some())) + || matches!((line, end_line), (Some(start), Some(end)) if end < start) + { + return Err(ChatFileLinkError::new( + ChatFileLinkErrorCode::InvalidRequest, + "The linked file location is invalid.", + )); + } + Ok((line, end_line, column)) +} + +fn preview_target( + conversation_workdir: &Path, + target: &Path, +) -> Result<(String, String, bool), ChatFileLinkError> { + if let Ok(relative) = target.strip_prefix(conversation_workdir) { + if !relative.as_os_str().is_empty() { + return Ok(( + display_path(conversation_workdir), + display_path(relative), + false, + )); + } + } + let parent = target.parent().ok_or_else(|| { + ChatFileLinkError::new( + ChatFileLinkErrorCode::UnsupportedTarget, + "The linked file cannot be opened safely.", + ) + })?; + let name = target.file_name().ok_or_else(|| { + ChatFileLinkError::new( + ChatFileLinkErrorCode::UnsupportedTarget, + "The linked file cannot be opened safely.", + ) + })?; + Ok(( + display_path(parent), + name.to_string_lossy().into_owned(), + true, + )) +} + +fn build_chat_file_link_plan( + conversation_id: &str, + workdir: &str, + path: &str, + source: &str, + line: Option, + end_line: Option, + column: Option, + open_in_file_manager: bool, +) -> Result { + if conversation_id.trim().is_empty() || conversation_id.len() > 256 { + return Err(ChatFileLinkError::new( + ChatFileLinkErrorCode::InvalidRequest, + "The conversation is unavailable.", + )); + } + if path.trim().is_empty() || path.contains('\0') { + return Err(ChatFileLinkError::new( + ChatFileLinkErrorCode::InvalidRequest, + "The linked file path is invalid.", + )); + } + let (line, end_line, column) = normalized_location(line, end_line, column)?; + + let raw_workdir = expand_tilde_path(workdir.trim()); + if !raw_workdir.is_absolute() { + return Err(ChatFileLinkError::new( + ChatFileLinkErrorCode::InvalidWorkdir, + "The conversation working directory is unavailable.", + )); + } + let conversation_workdir = fs::canonicalize(raw_workdir).map_err(|_| { + ChatFileLinkError::new( + ChatFileLinkErrorCode::InvalidWorkdir, + "The conversation working directory is unavailable.", + ) + })?; + if !conversation_workdir.is_dir() { + return Err(ChatFileLinkError::new( + ChatFileLinkErrorCode::InvalidWorkdir, + "The conversation working directory is unavailable.", + )); + } + + let raw_target = PathBuf::from(path.trim()); + let candidate = match source { + "relative" if !raw_target.is_absolute() => conversation_workdir.join(raw_target), + "absolute" | "file-url" if raw_target.is_absolute() => raw_target, + "relative" | "absolute" | "file-url" => { + return Err(ChatFileLinkError::new( + ChatFileLinkErrorCode::InvalidRequest, + "The linked path is not valid on this device.", + )); + } + _ => { + return Err(ChatFileLinkError::new( + ChatFileLinkErrorCode::InvalidRequest, + "The linked file source is invalid.", + )); + } + }; + let target = fs::canonicalize(candidate).map_err(|error| { + let code = if error.kind() == std::io::ErrorKind::NotFound { + ChatFileLinkErrorCode::NotFound + } else { + ChatFileLinkErrorCode::UnsupportedTarget + }; + let message = if code == ChatFileLinkErrorCode::NotFound { + "The linked file does not exist." + } else { + "The linked file cannot be accessed." + }; + ChatFileLinkError::new(code, message) + })?; + let metadata = fs::metadata(&target).map_err(|_| { + ChatFileLinkError::new( + ChatFileLinkErrorCode::UnsupportedTarget, + "The linked file cannot be accessed.", + ) + })?; + + if metadata.is_dir() { + if is_active_directory(&target) { + return Err(ChatFileLinkError::new( + ChatFileLinkErrorCode::UnsupportedTarget, + "The linked directory cannot be opened safely.", + )); + } + let inside_workspace = + target.starts_with(&conversation_workdir) && target != conversation_workdir; + if inside_workspace && !open_in_file_manager { + let (response_workdir, response_path, outside_workspace) = + preview_target(&conversation_workdir, &target)?; + return Ok(ChatFileLinkPlan { + response: ChatFileLinkOpenResponse { + action: "directory".to_string(), + kind: "directory".to_string(), + workdir: Some(response_workdir), + path: Some(response_path), + line: None, + end_line: None, + column: None, + outside_workspace, + }, + target, + system_mode: None, + }); + } + return Ok(ChatFileLinkPlan { + response: ChatFileLinkOpenResponse { + action: "opened".to_string(), + kind: "directory".to_string(), + workdir: None, + path: None, + line: None, + end_line: None, + column: None, + outside_workspace: true, + }, + target, + system_mode: Some("reveal"), + }); + } + if !metadata.is_file() { + return Err(ChatFileLinkError::new( + ChatFileLinkErrorCode::UnsupportedTarget, + "The linked path is not a regular file or directory.", + )); + } + + let script = has_extension(&target, SCRIPT_EXTENSIONS) || has_shebang(&target); + let executable = has_extension(&target, EXECUTABLE_EXTENSIONS) + || (has_executable_permission(&target, &metadata) && !script); + let action = if script || has_extension(&target, TEXT_EXTENSIONS) { + "editor" + } else if has_extension(&target, PREVIEW_EXTENSIONS) { + "preview" + } else if executable { + "revealed" + } else if is_probably_text(&target) { + "editor" + } else { + return Err(ChatFileLinkError::new( + ChatFileLinkErrorCode::UnsupportedTarget, + "The linked file type cannot be opened safely.", + )); + }; + + if action == "editor" || action == "preview" { + let (response_workdir, response_path, outside_workspace) = + preview_target(&conversation_workdir, &target)?; + return Ok(ChatFileLinkPlan { + response: ChatFileLinkOpenResponse { + action: action.to_string(), + kind: "file".to_string(), + workdir: Some(response_workdir), + path: Some(response_path), + line, + end_line, + column, + outside_workspace, + }, + target, + system_mode: None, + }); + } + + Ok(ChatFileLinkPlan { + response: ChatFileLinkOpenResponse { + action: action.to_string(), + kind: "file".to_string(), + workdir: None, + path: None, + line: None, + end_line: None, + column: None, + outside_workspace: !target.starts_with(&conversation_workdir), + }, + target, + system_mode: Some("reveal"), + }) +} + +pub(crate) fn open_chat_file_link_sync( + conversation_id: String, + workdir: String, + path: String, + source: String, + line: Option, + end_line: Option, + column: Option, + open_in_file_manager: bool, +) -> Result { + let plan = build_chat_file_link_plan( + &conversation_id, + &workdir, + &path, + &source, + line, + end_line, + column, + open_in_file_manager, + )?; + if let Some(mode) = plan.system_mode { + spawn_workspace_open_command(&plan.target, mode).map_err(|_| { + ChatFileLinkError::new( + ChatFileLinkErrorCode::OpenFailed, + "The linked file could not be opened on the host device.", + ) + })?; + } + Ok(plan.response) +} + +pub(crate) async fn open_chat_file_link_for_conversation( + conversation_id: String, + requested_workdir: String, + path: String, + source: String, + line: Option, + end_line: Option, + column: Option, + open_in_file_manager: bool, +) -> Result { + tokio::time::timeout(CHAT_FILE_OPEN_TIMEOUT, async move { + // Finding the id in the target agent's own history database is the + // conversation/worker ownership check. The request-supplied workdir is + // never used as the base for a relative path. + let summary = chat_history::chat_history_get_summary_inner(conversation_id.clone()) + .await + .map_err(|_| { + ChatFileLinkError::new( + ChatFileLinkErrorCode::InvalidRequest, + "The conversation is unavailable on this device.", + ) + })?; + let workdir = summary + .cwd + .filter(|value| !value.trim().is_empty()) + .ok_or_else(|| { + ChatFileLinkError::new( + ChatFileLinkErrorCode::InvalidWorkdir, + "The conversation working directory is unavailable.", + ) + })?; + let _ = requested_workdir; + let permit = Arc::clone(&CHAT_FILE_OPEN_SEMAPHORE) + .acquire_owned() + .await + .map_err(|_| { + ChatFileLinkError::new( + ChatFileLinkErrorCode::OpenFailed, + "The linked file request did not complete.", + ) + })?; + + tauri::async_runtime::spawn_blocking(move || { + let _permit = permit; + open_chat_file_link_sync( + conversation_id, + workdir, + path, + source, + line, + end_line, + column, + open_in_file_manager, + ) + }) + .await + .map_err(|_| { + ChatFileLinkError::new( + ChatFileLinkErrorCode::OpenFailed, + "The linked file request did not complete.", + ) + })? + }) + .await + .map_err(|_| { + ChatFileLinkError::new( + ChatFileLinkErrorCode::OpenFailed, + "The linked file request timed out.", + ) + })? +} + +#[tauri::command(rename_all = "snake_case")] +pub async fn open_chat_file_link( + conversation_id: String, + workdir: String, + path: String, + source: String, + line: Option, + end_line: Option, + column: Option, + open_in_file_manager: Option, +) -> Result { + open_chat_file_link_for_conversation( + conversation_id, + workdir, + path, + source, + line, + end_line, + column, + open_in_file_manager.unwrap_or(false), + ) + .await +} + +#[cfg(test)] +mod tests { + use super::*; + use std::time::{SystemTime, UNIX_EPOCH}; + + fn temp_workspace() -> PathBuf { + let suffix = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("clock") + .as_nanos(); + let root = std::env::temp_dir().join(format!("liveagent-chat-file-links-{suffix}")); + fs::create_dir_all(root.join("src")).expect("create temp workspace"); + root + } + + fn plan(root: &Path, path: &str, source: &str) -> ChatFileLinkPlan { + build_chat_file_link_plan( + "conversation-test", + &root.to_string_lossy(), + path, + source, + None, + None, + None, + false, + ) + .expect("build plan") + } + + #[test] + fn classifies_text_and_preview_files_but_rejects_unknown_binary_types() { + let root = temp_workspace(); + fs::write(root.join("src/a.ts"), "const value = 1;\n").expect("write source"); + fs::write(root.join("document.pdf"), b"%PDF-test").expect("write pdf"); + fs::write(root.join("opaque.custom"), [0_u8, 1, 2, 3]).expect("write opaque"); + fs::write(root.join("macro.docm"), [0_u8, 1, 2, 3]).expect("write macro document"); + + assert_eq!( + plan(&root, "src/a.ts", "relative").response.action, + "editor" + ); + assert_eq!( + plan(&root, "document.pdf", "relative").response.action, + "preview" + ); + for path in ["opaque.custom", "macro.docm"] { + let error = build_chat_file_link_plan( + "conversation-test", + &root.to_string_lossy(), + path, + "relative", + None, + None, + None, + false, + ) + .expect_err("unknown binary file must fail closed"); + assert_eq!(error.code, ChatFileLinkErrorCode::UnsupportedTarget); + } + + fs::remove_dir_all(root).expect("remove temp workspace"); + } + + #[test] + fn scripts_edit_and_executables_reveal_instead_of_running() { + let root = temp_workspace(); + fs::write(root.join("request.ps1"), "Write-Output safe\n").expect("write ps1"); + fs::write(root.join("page.hta"), "\n").expect("write hta"); + fs::write(root.join("setup.exe"), [0_u8, 1, 2, 3]).expect("write exe"); + fs::write( + root.join("settings.reg"), + "Windows Registry Editor Version 5.00\n", + ) + .expect("write reg"); + + let script = plan(&root, "request.ps1", "relative"); + assert_eq!(script.response.action, "editor"); + assert_eq!(script.system_mode, None); + assert_eq!( + plan(&root, "page.hta", "relative").response.action, + "editor" + ); + let executable = plan(&root, "setup.exe", "relative"); + assert_eq!(executable.response.action, "revealed"); + assert_eq!(executable.system_mode, Some("reveal")); + assert_eq!( + plan(&root, "settings.reg", "relative").response.action, + "revealed" + ); + + fs::remove_dir_all(root).expect("remove temp workspace"); + } + + #[test] + fn workspace_directories_use_file_tree_first_and_support_a_safe_manager_fallback() { + let root = temp_workspace(); + fs::create_dir(root.join("scripts.ps1")).expect("create script-suffixed directory"); + let directory = plan(&root, "src", "relative"); + assert_eq!(directory.response.action, "directory"); + assert_eq!(directory.system_mode, None); + let suffixed_directory = plan(&root, "scripts.ps1", "relative"); + assert_eq!(suffixed_directory.response.action, "directory"); + assert_eq!(suffixed_directory.response.kind, "directory"); + assert_eq!(suffixed_directory.system_mode, None); + + let fallback = build_chat_file_link_plan( + "conversation-test", + &root.to_string_lossy(), + "src", + "relative", + None, + None, + None, + true, + ) + .expect("build manager fallback"); + assert_eq!(fallback.response.action, "opened"); + assert_eq!(fallback.response.kind, "directory"); + assert_eq!(fallback.system_mode, Some("reveal")); + + fs::remove_dir_all(root).expect("remove temp workspace"); + } + + #[cfg(target_os = "macos")] + #[test] + fn macos_active_directory_packages_are_rejected_instead_of_launched() { + let root = temp_workspace(); + for path in ["Dangerous.app", "Dangerous.workflow", "Dangerous.pkg"] { + fs::create_dir(root.join(path)).expect("create active directory package"); + let error = build_chat_file_link_plan( + "conversation-test", + &root.to_string_lossy(), + path, + "relative", + None, + None, + None, + false, + ) + .expect_err("active directory package must fail closed"); + assert_eq!(error.code, ChatFileLinkErrorCode::UnsupportedTarget); + } + fs::remove_dir_all(root).expect("remove temp workspace"); + } + + #[cfg(target_os = "macos")] + #[test] + fn macos_other_directory_packages_are_revealed_instead_of_launched() { + let root = temp_workspace(); + for path in [ + "Dangerous.prefPane", + "Dangerous.saver", + "Dangerous.bundle", + "Dangerous.plugin", + ] { + fs::create_dir(root.join(path)).expect("create directory package"); + let planned = build_chat_file_link_plan( + "conversation-test", + &root.to_string_lossy(), + path, + "relative", + None, + None, + None, + true, + ) + .expect("directory package must use safe reveal"); + assert_eq!(planned.response.action, "opened"); + assert_eq!(planned.response.kind, "directory"); + assert_eq!(planned.system_mode, Some("reveal")); + } + fs::remove_dir_all(root).expect("remove temp workspace"); + } + + #[test] + fn resolves_outside_files_to_a_safe_preview_root_and_preserves_location() { + let root = temp_workspace(); + let outside_dir = root.parent().expect("parent").join(format!( + "{}-outside", + root.file_name().expect("name").to_string_lossy() + )); + fs::create_dir_all(&outside_dir).expect("create outside dir"); + let outside = outside_dir.join("outside.md"); + fs::write(&outside, "# outside\n").expect("write outside file"); + + let planned = build_chat_file_link_plan( + "conversation-test", + &root.to_string_lossy(), + &outside.to_string_lossy(), + "absolute", + Some(12), + Some(20), + Some(4), + false, + ) + .expect("build outside plan"); + assert_eq!(planned.response.action, "editor"); + assert!(planned.response.outside_workspace); + assert_eq!(planned.response.path.as_deref(), Some("outside.md")); + assert_eq!(planned.response.line, Some(12)); + assert_eq!(planned.response.end_line, Some(20)); + assert_eq!(planned.response.column, Some(4)); + + fs::remove_dir_all(root).expect("remove temp workspace"); + fs::remove_dir_all(outside_dir).expect("remove outside dir"); + } + + #[test] + fn nonexistent_files_fail_before_any_system_open() { + let root = temp_workspace(); + let error = build_chat_file_link_plan( + "conversation-test", + &root.to_string_lossy(), + "missing.txt", + "relative", + None, + None, + None, + false, + ) + .expect_err("missing file must fail"); + assert_eq!(error.code, ChatFileLinkErrorCode::NotFound); + assert_eq!(error.message, "The linked file does not exist."); + assert!(!error.message.contains(&root.to_string_lossy().to_string())); + + fs::remove_dir_all(root).expect("remove temp workspace"); + } +} diff --git a/crates/agent-gui/src-tauri/src/commands/workspace/fs.rs b/crates/agent-gui/src-tauri/src/commands/workspace/fs.rs index 9cc95ca4d..3f4bcc996 100644 --- a/crates/agent-gui/src-tauri/src/commands/workspace/fs.rs +++ b/crates/agent-gui/src-tauri/src/commands/workspace/fs.rs @@ -3249,41 +3249,57 @@ pub struct OpenWorkspacePathResponse { } #[cfg(target_os = "macos")] -fn spawn_workspace_open_command(target: &Path, kind: &str, mode: &str) -> Result<(), String> { +fn workspace_open_command(target: &Path, mode: &str) -> Command { let mut command = Command::new("open"); - if mode == "reveal" && kind == "file" { + if mode == "reveal" { command.arg("-R"); } command.arg(target); command +} + +#[cfg(target_os = "macos")] +pub(crate) fn spawn_workspace_open_command(target: &Path, mode: &str) -> Result<(), String> { + workspace_open_command(target, mode) .spawn() .map(|_| ()) .map_err(|e| format!("Failed to open path with macOS open: {e}")) } #[cfg(target_os = "windows")] -fn spawn_workspace_open_command(target: &Path, kind: &str, mode: &str) -> Result<(), String> { +fn workspace_open_command(target: &Path, mode: &str) -> Command { let mut command = Command::new("explorer.exe"); - if mode == "reveal" && kind == "file" { + if mode == "reveal" { command.arg(format!("/select,{}", target.display())); } else { command.arg(target); } command +} + +#[cfg(target_os = "windows")] +pub(crate) fn spawn_workspace_open_command(target: &Path, mode: &str) -> Result<(), String> { + workspace_open_command(target, mode) .spawn() .map(|_| ()) .map_err(|e| format!("Failed to open path with Windows Explorer: {e}")) } #[cfg(all(not(target_os = "macos"), not(target_os = "windows")))] -fn spawn_workspace_open_command(target: &Path, kind: &str, mode: &str) -> Result<(), String> { - let open_target = if mode == "reveal" && kind == "file" { +fn workspace_open_command(target: &Path, mode: &str) -> Command { + let open_target = if mode == "reveal" { target.parent().unwrap_or(target) } else { target }; - Command::new("xdg-open") - .arg(open_target) + let mut command = Command::new("xdg-open"); + command.arg(open_target); + command +} + +#[cfg(all(not(target_os = "macos"), not(target_os = "windows")))] +pub(crate) fn spawn_workspace_open_command(target: &Path, mode: &str) -> Result<(), String> { + workspace_open_command(target, mode) .spawn() .map(|_| ()) .map_err(|e| format!("Failed to open path with xdg-open: {e}")) @@ -3332,7 +3348,7 @@ fn fs_open_workspace_path_impl( } }; - spawn_workspace_open_command(&target, kind, normalized_mode).map_err(FsError::Other)?; + spawn_workspace_open_command(&target, normalized_mode).map_err(FsError::Other)?; Ok(OpenWorkspacePathResponse { path: logical_path, @@ -4609,6 +4625,48 @@ mod tests { assert!(!is_windows_reserved_path_component("console.txt")); } + #[cfg(target_os = "macos")] + #[test] + fn macos_reveal_selects_directory_targets_in_finder() { + let target = Path::new("/tmp/Dangerous.prefPane"); + let command = workspace_open_command(target, "reveal"); + let args = command + .get_args() + .map(|value| value.to_string_lossy().into_owned()) + .collect::>(); + + assert_eq!(command.get_program(), std::ffi::OsStr::new("open")); + assert_eq!(args, vec!["-R", "/tmp/Dangerous.prefPane"]); + } + + #[cfg(target_os = "windows")] + #[test] + fn windows_reveal_selects_directory_targets_in_explorer() { + let target = Path::new(r"C:\work\Dangerous.bundle"); + let command = workspace_open_command(target, "reveal"); + let args = command + .get_args() + .map(|value| value.to_string_lossy().into_owned()) + .collect::>(); + + assert_eq!(command.get_program(), std::ffi::OsStr::new("explorer.exe")); + assert_eq!(args, vec![r"/select,C:\work\Dangerous.bundle"]); + } + + #[cfg(all(not(target_os = "macos"), not(target_os = "windows")))] + #[test] + fn linux_reveal_opens_the_directory_targets_parent() { + let target = Path::new("/tmp/work/Dangerous.bundle"); + let command = workspace_open_command(target, "reveal"); + let args = command + .get_args() + .map(|value| value.to_string_lossy().into_owned()) + .collect::>(); + + assert_eq!(command.get_program(), std::ffi::OsStr::new("xdg-open")); + assert_eq!(args, vec!["/tmp/work"]); + } + #[cfg(windows)] #[test] fn rel_path_rejects_windows_reserved_components_on_windows() { diff --git a/crates/agent-gui/src-tauri/src/commands/workspace/mod.rs b/crates/agent-gui/src-tauri/src/commands/workspace/mod.rs index 7f26d6540..9302f1ce0 100644 --- a/crates/agent-gui/src-tauri/src/commands/workspace/mod.rs +++ b/crates/agent-gui/src-tauri/src/commands/workspace/mod.rs @@ -1,3 +1,4 @@ +pub mod chat_file_links; pub(crate) mod edit_match; pub mod fs; pub mod git; diff --git a/crates/agent-gui/src-tauri/src/lib.rs b/crates/agent-gui/src-tauri/src/lib.rs index 91672e999..17ee4ded0 100644 --- a/crates/agent-gui/src-tauri/src/lib.rs +++ b/crates/agent-gui/src-tauri/src/lib.rs @@ -84,6 +84,7 @@ macro_rules! app_invoke_handler { commands::fs::fs_glob, commands::fs::fs_grep, commands::fs::fs_mention_list, + commands::chat_file_links::open_chat_file_link, // Subagent worktrees commands::subagent_worktree::subagent_worktree_create, commands::subagent_worktree::subagent_worktree_status, diff --git a/crates/agent-gui/src-tauri/src/services/gateway/envelope_handler.rs b/crates/agent-gui/src-tauri/src/services/gateway/envelope_handler.rs index 15c1ff1f8..e37b6afff 100644 --- a/crates/agent-gui/src-tauri/src/services/gateway/envelope_handler.rs +++ b/crates/agent-gui/src-tauri/src/services/gateway/envelope_handler.rs @@ -801,6 +801,25 @@ impl GatewayController { Err(error) => self.send_error_response(request_id, 500, error).await, } } + Some(proto::gateway_envelope::Payload::ChatFileOpen(request)) => { + let sender = self.current_outbound_sender()?; + tauri::async_runtime::spawn(async move { + let envelope = match gateway_bridge::handle_chat_file_open(request).await { + Ok(response) => proto::AgentEnvelope { + request_id, + timestamp: now_unix_seconds(), + payload: Some(proto::agent_envelope::Payload::ChatFileOpenResp( + response, + )), + }, + Err(error) => build_error_response_envelope(request_id, 500, error), + }; + if let Err(error) = send_agent_envelope_to(sender, envelope).await { + eprintln!("send gateway chat file open response failed: {error}"); + } + }); + Ok(()) + } Some(proto::gateway_envelope::Payload::FsWriteText(request)) => { match gateway_bridge::handle_fs_write_text(request).await { Ok(response) => { diff --git a/crates/agent-gui/src-tauri/src/services/gateway_bridge.rs b/crates/agent-gui/src-tauri/src/services/gateway_bridge.rs index eb75b7c2e..46aaf4656 100644 --- a/crates/agent-gui/src-tauri/src/services/gateway_bridge.rs +++ b/crates/agent-gui/src-tauri/src/services/gateway_bridge.rs @@ -4,6 +4,7 @@ use serde::Deserialize; use serde_json::{json, Value}; use crate::commands::{ + chat_file_links::open_chat_file_link_for_conversation, chat_history, chat_history::ChatHistoryMessageRef, fs::{ @@ -576,6 +577,33 @@ pub async fn handle_fs_read_workspace_image( }) } +pub async fn handle_chat_file_open( + request: proto::ChatFileOpenRequest, +) -> Result { + open_chat_file_link_for_conversation( + request.conversation_id, + request.workdir, + request.path, + request.source, + request.line, + request.end_line, + request.column, + request.open_in_file_manager, + ) + .await + .map_err(|error| error.message) + .map(|response| proto::ChatFileOpenResponse { + action: response.action, + kind: response.kind, + workdir: response.workdir.unwrap_or_default(), + path: response.path.unwrap_or_default(), + line: response.line, + end_line: response.end_line, + column: response.column, + outside_workspace: response.outside_workspace, + }) +} + pub async fn handle_fs_write_text( request: proto::FsWriteTextRequest, ) -> Result { diff --git a/crates/agent-gui/src/components/Markdown.tsx b/crates/agent-gui/src/components/Markdown.tsx index dc5f5d0d3..30aad7a19 100644 --- a/crates/agent-gui/src/components/Markdown.tsx +++ b/crates/agent-gui/src/components/Markdown.tsx @@ -25,6 +25,12 @@ import { type StreamdownTranslations, } from "streamdown"; import { useLocale } from "../i18n"; +import { + type ChatFileLink, + decodeChatFileLinkPayload, + encodeChatFileLink, + parseChatFileLink, +} from "../lib/chat/chatFileLinks"; import { getCollapsedCodeBlockPreview, resolveCodeBlockRenderPolicy, @@ -34,7 +40,132 @@ import { cn } from "../lib/shared/utils"; import { Check, ChevronDown, ChevronUp, Copy, ExternalLink, X } from "./icons"; import { Button } from "./ui/button"; -type MarkdownProps = { +const CHAT_FILE_NODE_DATA_KEY = "liveagentChatFileLink"; +const LIVEAGENT_FILE_PROTOCOL = "liveagent-file:"; + +type ChatFileHastNode = { + children?: ChatFileHastNode[]; + data?: Record; + properties?: Record; + position?: { start?: { offset?: number } }; + type?: string; + tagName?: string; +}; + +type ChatFileVFile = { value?: unknown }; + +function visitChatFileElements(node: ChatFileHastNode, visitor: (node: ChatFileHastNode) => void) { + if (node.type === "element") visitor(node); + for (const child of node.children ?? []) visitChatFileElements(child, visitor); +} + +export function rewriteChatFileLinks() { + return (tree: unknown, file?: ChatFileVFile) => { + visitChatFileElements(tree as ChatFileHastNode, (node) => { + if (node.tagName !== "a") return; + const source = typeof file?.value === "string" ? file.value : ""; + const sourceOffset = node.position?.start?.offset; + // Raw HTML anchors keep their source position after rehype-raw. They + // must not acquire the trusted marker that only Markdown links receive. + if (sourceOffset !== undefined && source[sourceOffset] === "<") return; + const href = typeof node.properties?.href === "string" ? node.properties.href : ""; + const parsed = parseChatFileLink(href); + if (!parsed) return; + const internalHref = encodeChatFileLink(parsed); + node.properties = { ...node.properties, href: internalHref }; + node.data = { ...node.data, [CHAT_FILE_NODE_DATA_KEY]: internalHref }; + }); + }; +} + +type ChatFileMdastNode = { + children?: ChatFileMdastNode[]; + position?: { start?: { offset?: number }; end?: { offset?: number } }; + type?: string; + url?: string; + value?: string; +}; + +const CHAT_FILE_MARKDOWN_LINK_PATTERN = /\[([^\]\n]+)\]\(([^)\n]+)\)/g; +const SKIPPED_CHAT_FILE_MDAST_NODES = new Set(["code", "html", "image", "inlineCode", "link"]); +const COMMONMARK_ESCAPABLE_CHARACTER = /[!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]/; + +function mapDecodedSourceOffsets(sourceValue: string, value: string) { + let decoded = ""; + const rawOffsets: number[] = []; + for (let index = 0; index < sourceValue.length; index += 1) { + const next = sourceValue[index + 1]; + if (sourceValue[index] === "\\" && next && COMMONMARK_ESCAPABLE_CHARACTER.test(next)) { + decoded += next; + rawOffsets.push(index + 1); + index += 1; + } else { + decoded += sourceValue[index]; + rawOffsets.push(index); + } + } + return decoded === value ? rawOffsets : null; +} + +function rewriteChatFileTextNode(node: ChatFileMdastNode, source: string) { + const value = node.value ?? ""; + const sourceStart = node.position?.start?.offset; + const sourceEnd = node.position?.end?.offset; + const sourceValue = + sourceStart !== undefined && sourceEnd !== undefined + ? source.slice(sourceStart, sourceEnd) + : ""; + const rawOffsets = mapDecodedSourceOffsets(sourceValue, value); + if (!rawOffsets) return null; + const nodes: ChatFileMdastNode[] = []; + let cursor = 0; + for (const match of value.matchAll(CHAT_FILE_MARKDOWN_LINK_PATTERN)) { + const index = match.index ?? 0; + const sourceIndex = rawOffsets[index]; + let backslashes = 0; + for (let offset = sourceIndex - 1; offset >= 0 && sourceValue[offset] === "\\"; offset -= 1) { + backslashes += 1; + } + if (backslashes % 2 === 1) continue; + const destination = match[2].trim(); + if (!parseChatFileLink(destination)) continue; + if (index > cursor) nodes.push({ type: "text", value: value.slice(cursor, index) }); + nodes.push({ + type: "link", + url: destination, + children: [{ type: "text", value: match[1] }], + }); + cursor = index + match[0].length; + } + if (cursor === 0) return null; + if (cursor < value.length) nodes.push({ type: "text", value: value.slice(cursor) }); + return nodes; +} + +function rewriteChatFileMarkdownChildren(node: ChatFileMdastNode, source: string) { + if (!node.children || SKIPPED_CHAT_FILE_MDAST_NODES.has(node.type ?? "")) return; + for (let index = 0; index < node.children.length; index += 1) { + const child = node.children[index]; + if (child.type === "text" && typeof child.value === "string") { + const replacement = rewriteChatFileTextNode(child, source); + if (!replacement) continue; + node.children.splice(index, 1, ...replacement); + index += replacement.length - 1; + continue; + } + rewriteChatFileMarkdownChildren(child, source); + } +} + +export function remarkChatFileLinks() { + return (tree: unknown, file?: ChatFileVFile) => + rewriteChatFileMarkdownChildren( + tree as ChatFileMdastNode, + typeof file?.value === "string" ? file.value : "", + ); +} + +export type MarkdownProps = { content: string; className?: string; // Fixed render mode: content born from a live stream renders in Streamdown @@ -56,33 +187,77 @@ type MarkdownProps = { // against the page origin before they reach custom components. Sanitize // still runs, so scriptable protocols (javascript: etc.) never get through. preserveRelativeUrls?: boolean; + // Chat file links are only rewritten when this explicit user-action callback + // is present. Other Markdown surfaces keep Streamdown's normal link policy. + workdir?: string; + onOpenFileLink?: (link: ChatFileLink) => void; }; const streamdownPlugins = { code, math, mermaid, cjk }; const remarkPlugins = [...Object.values(defaultRemarkPlugins), remarkBreaks]; +const chatRemarkPlugins = [...remarkPlugins, remarkChatFileLinks]; type StreamdownRehypePlugins = NonNullable["rehypePlugins"]>; -// raw + sanitize from the default chain (raw → sanitize → harden), with data: -// image sources additionally allowed so embedded data-URI images render. -const relativeUrlRehypePlugins = (() => { +function createSanitizedRehypePlugins(options: { + allowDataImages: boolean; + preserveRelativeUrls: boolean; + rewriteFileLinks: boolean; +}) { const sanitize = defaultRehypePlugins.sanitize; if (!Array.isArray(sanitize)) { - return [defaultRehypePlugins.raw, sanitize] as StreamdownRehypePlugins; + return [ + defaultRehypePlugins.raw, + ...(options.rewriteFileLinks ? [rewriteChatFileLinks] : []), + sanitize, + ...(options.preserveRelativeUrls ? [] : [defaultRehypePlugins.harden]), + ] as StreamdownRehypePlugins; } const schema = (sanitize[1] ?? {}) as { protocols?: Record }; const srcProtocols = schema.protocols?.src; + const hrefProtocols = schema.protocols?.href; const protocols = { ...schema.protocols, - src: Array.isArray(srcProtocols) - ? [...new Set([...srcProtocols, "data"])] - : ["http", "https", "data"], + ...(options.rewriteFileLinks + ? { + href: Array.isArray(hrefProtocols) + ? [...new Set([...hrefProtocols, "liveagent-file"])] + : ["http", "https", "mailto", "liveagent-file"], + } + : {}), + ...(options.allowDataImages + ? { + src: Array.isArray(srcProtocols) + ? [...new Set([...srcProtocols, "data"])] + : ["http", "https", "data"], + } + : {}), }; return [ defaultRehypePlugins.raw, + ...(options.rewriteFileLinks ? [rewriteChatFileLinks] : []), [sanitize[0], { ...schema, protocols }], + ...(options.preserveRelativeUrls ? [] : [defaultRehypePlugins.harden]), ] as StreamdownRehypePlugins; -})(); +} + +// Workspace previews intentionally skip harden so relative assets reach their +// custom renderer. Chat surfaces always use raw → rewrite → sanitize → harden. +export const relativeUrlRehypePlugins = createSanitizedRehypePlugins({ + allowDataImages: true, + preserveRelativeUrls: true, + rewriteFileLinks: false, +}); +export const chatFileRehypePlugins = createSanitizedRehypePlugins({ + allowDataImages: false, + preserveRelativeUrls: false, + rewriteFileLinks: true, +}); +const relativeChatFileRehypePlugins = createSanitizedRehypePlugins({ + allowDataImages: true, + preserveRelativeUrls: true, + rewriteFileLinks: true, +}); type MarkdownImageFallbackProps = ComponentProps<"img"> & ExtraProps; type MarkdownAnchorFallbackProps = ComponentProps<"a"> & ExtraProps; @@ -93,8 +268,46 @@ type StreamdownCodeChildProps = { "data-block"?: string; }; +type MarkdownFileLinkProps = ComponentProps<"a"> & + ExtraProps & { + onOpenFileLink: (link: ChatFileLink) => void; + workdir?: string; + }; + const DEFAULT_CODE_BLOCK_LANGUAGE = "markdown"; +function readRewrittenChatFileLink(props: MarkdownAnchorFallbackProps) { + const href = typeof props.href === "string" ? props.href : ""; + const marker = (props.node as ChatFileHastNode | undefined)?.data?.[CHAT_FILE_NODE_DATA_KEY]; + if (!href.startsWith(LIVEAGENT_FILE_PROTOCOL) || marker !== href) return null; + const parsed = decodeChatFileLinkPayload(href.slice(LIVEAGENT_FILE_PROTOCOL.length)); + if (!parsed || encodeChatFileLink(parsed) !== href) return null; + return parsed; +} + +export function MarkdownFileLink(props: MarkdownFileLinkProps) { + const { children, title, onOpenFileLink, workdir } = props; + const parsed = readRewrittenChatFileLink(props); + if (!parsed) return ; + const label = + typeof title === "string" && title.trim() + ? title.trim() + : parsed.source === "relative" && workdir + ? `${workdir.replace(/[\\/]+$/, "")}/${parsed.path.replace(/^[\\/]+/, "")}` + : parsed.path; + return ( + + ); +} + function MarkdownImageFallback(props: MarkdownImageFallbackProps) { const { alt, title } = props; const label = @@ -140,6 +353,46 @@ export const markdownReadOnlyComponents: Components = { a: MarkdownReadOnlyLink, }; +function MarkdownExternalLink(props: MarkdownAnchorFallbackProps) { + const { children, className, href, title } = props; + const [modalOpen, setModalOpen] = useState(false); + if (!href) return ; + const incomplete = href === "streamdown:incomplete-link"; + return ( + <> + + setModalOpen(false)} + onConfirm={() => window.open(href, "_blank", "noreferrer")} + /> + + ); +} + +export function MarkdownLink(props: MarkdownFileLinkProps) { + if (readRewrittenChatFileLink(props)) return ; + if (typeof props.href === "string" && props.href.startsWith(LIVEAGENT_FILE_PROTOCOL)) { + return ; + } + return ; +} + async function copyCodeBlockText(text: string) { try { await navigator.clipboard.writeText(text); @@ -435,17 +688,33 @@ export const Markdown = memo(function Markdown(props: MarkdownProps) { readOnly = false, componentOverrides, preserveRelativeUrls = false, + workdir, + onOpenFileLink, } = props; const streaming = renderMode === "streaming"; const normalizedContent = useMemo( () => normalizeLatexDelimiters(content, streaming && showCaret), [content, showCaret, streaming], ); - const baseComponents = readOnly ? markdownReadOnlyComponents : markdownComponents; - const components = useMemo( - () => (componentOverrides ? { ...baseComponents, ...componentOverrides } : baseComponents), - [baseComponents, componentOverrides], - ); + const components = useMemo(() => { + const baseComponents = readOnly ? markdownReadOnlyComponents : markdownComponents; + const fileLinkComponents: Components = + !readOnly && onOpenFileLink + ? { + a: (linkProps) => ( + + ), + } + : {}; + return { ...baseComponents, ...fileLinkComponents, ...componentOverrides }; + }, [componentOverrides, onOpenFileLink, readOnly, workdir]); + const rehypePlugins = onOpenFileLink + ? preserveRelativeUrls + ? relativeChatFileRehypePlugins + : chatFileRehypePlugins + : preserveRelativeUrls + ? relativeUrlRehypePlugins + : undefined; return (
@@ -454,16 +723,12 @@ export const Markdown = memo(function Markdown(props: MarkdownProps) { "chat-markdown max-w-none break-words", MARKDOWN_EMBED_CLASSNAME, streaming ? "chat-markdown--streaming" : "chat-markdown--static", - // Streamdown's memo equality does not include `caret` in its check, - // so toggling the caret prop alone does not invalidate the render. - // Mirror the visibility into a className modifier to force a re-render - // that recomputes the inline `--streamdown-caret` style. showCaret ? "chat-markdown--caret-on" : "chat-markdown--caret-off", className, )} plugins={streamdownPlugins} - remarkPlugins={remarkPlugins} - {...(preserveRelativeUrls ? { rehypePlugins: relativeUrlRehypePlugins } : {})} + remarkPlugins={onOpenFileLink ? chatRemarkPlugins : remarkPlugins} + {...(rehypePlugins ? { rehypePlugins } : {})} components={components} mode={streaming ? "streaming" : "static"} dir="auto" diff --git a/crates/agent-gui/src/components/workspace-editor/WorkspaceCodeEditorOverlay.tsx b/crates/agent-gui/src/components/workspace-editor/WorkspaceCodeEditorOverlay.tsx index 5f592b046..cd860ded6 100644 --- a/crates/agent-gui/src/components/workspace-editor/WorkspaceCodeEditorOverlay.tsx +++ b/crates/agent-gui/src/components/workspace-editor/WorkspaceCodeEditorOverlay.tsx @@ -70,6 +70,9 @@ export type WorkspaceCodeEditorOpenRequest = { projectPathKey: string; workdir: string; path: string; + line?: number; + endLine?: number; + column?: number; }; type ReadEditableTextResponse = { @@ -302,6 +305,7 @@ export function WorkspaceCodeEditorOverlay(props: WorkspaceCodeEditorOverlayProp const modelsRef = useRef(new Map()); const viewStatesRef = useRef(new Map()); const editorModelKeyRef = useRef(""); + const linkedLocationKeyRef = useRef(""); const activeKeyRef = useRef(""); const openRequestIdRef = useRef(null); const closeRequestIdRef = useRef(null); @@ -822,6 +826,34 @@ export function WorkspaceCodeEditorOverlay(props: WorkspaceCodeEditorOverlayProp editorModelKeyRef.current = activeTab.key; }, [activeTab]); + const activeTabKey = activeTab?.key; + useEffect(() => { + const editor = editorRef.current; + if (!editor || !activeTabKey || !openRequest?.line) return; + if (activeTabKey !== editorTabKey(openRequest.projectPathKey, openRequest.path)) return; + const locationKey = `${openRequest.id}\u0000${activeTabKey}`; + if (linkedLocationKeyRef.current === locationKey) return; + const model = editor.getModel(); + if (!model) return; + const line = Math.min(Math.max(1, openRequest.line), model.getLineCount()); + const endLine = Math.min(Math.max(line, openRequest.endLine ?? line), model.getLineCount()); + const column = Math.min(Math.max(1, openRequest.column ?? 1), model.getLineMaxColumn(line)); + const endColumn = openRequest.endLine ? model.getLineMaxColumn(endLine) : column; + const range = new monaco.Range(line, column, endLine, endColumn); + editor.setSelection(range); + editor.revealRangeInCenter(range); + editor.focus(); + linkedLocationKeyRef.current = locationKey; + }, [ + activeTabKey, + openRequest?.column, + openRequest?.endLine, + openRequest?.id, + openRequest?.line, + openRequest?.path, + openRequest?.projectPathKey, + ]); + useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape") { diff --git a/crates/agent-gui/src/lib/chat/chatFileLinks.ts b/crates/agent-gui/src/lib/chat/chatFileLinks.ts new file mode 100644 index 000000000..fd3608ebe --- /dev/null +++ b/crates/agent-gui/src/lib/chat/chatFileLinks.ts @@ -0,0 +1 @@ +export * from "./messages/chatFileLinks"; diff --git a/crates/agent-gui/src/lib/chat/messages/chatFileLinks.ts b/crates/agent-gui/src/lib/chat/messages/chatFileLinks.ts new file mode 100644 index 000000000..7cf63d3b9 --- /dev/null +++ b/crates/agent-gui/src/lib/chat/messages/chatFileLinks.ts @@ -0,0 +1,201 @@ +export type ChatFileLink = { + path: string; + line?: number; + endLine?: number; + column?: number; + source: "absolute" | "relative" | "file-url"; +}; + +const WINDOWS_DRIVE_PATH_PATTERN = /^[a-zA-Z]:[\\/]/; +const WINDOWS_UNC_PATH_PATTERN = /^(?:\\\\|\/\/)/; +const FILE_URL_PATTERN = /^file:\/\//i; +const ABSOLUTE_POSIX_PATH_PATTERN = /^\//; +const URI_SCHEME_PATTERN = /^[a-zA-Z][a-zA-Z\d+.-]*:/; +const LOCATION_FRAGMENT_PATTERN = /^#L([1-9]\d*)(?:-L?([1-9]\d*))?$/i; +const LOCATION_SUFFIX_PATTERN = /:([1-9]\d*)(?::([1-9]\d*))?$/; +const INTERNAL_PAYLOAD_VERSION = "1"; +const MAX_LOCATION_VALUE = 0xffff_ffff; + +function parseLocationNumber(value: string) { + const parsed = Number(value); + return Number.isSafeInteger(parsed) && parsed > 0 && parsed <= MAX_LOCATION_VALUE ? parsed : null; +} + +function safeDecode(value: string) { + try { + return decodeURIComponent(value); + } catch { + return value; + } +} + +function normalizePath(path: string) { + const decoded = safeDecode(path); + const isUnc = /^(?:\\\\|\/\/[^/])/.test(decoded); + const collapsed = decoded.replace(/\\/g, "/").replace(/\/{2,}/g, "/"); + return isUnc ? `//${collapsed.replace(/^\/+/, "")}` : collapsed; +} + +function isAbsolutePath(path: string) { + return ( + WINDOWS_DRIVE_PATH_PATTERN.test(path) || + WINDOWS_UNC_PATH_PATTERN.test(path) || + ABSOLUTE_POSIX_PATH_PATTERN.test(path) + ); +} + +function parseTrailingLocation(value: string) { + let path = value.trim(); + let line: number | undefined; + let endLine: number | undefined; + let column: number | undefined; + + const hashIndex = path.lastIndexOf("#"); + const hashMatch = hashIndex >= 0 ? path.slice(hashIndex).match(LOCATION_FRAGMENT_PATTERN) : null; + if (hashMatch) { + line = parseLocationNumber(hashMatch[1]) ?? undefined; + if (line === undefined) return { path: "", line, endLine, column }; + if (hashMatch[2]) { + endLine = parseLocationNumber(hashMatch[2]) ?? undefined; + if (endLine === undefined) return { path: "", line, endLine, column }; + } + path = path.slice(0, hashIndex); + } + + const lineMatch = line === undefined ? path.match(LOCATION_SUFFIX_PATTERN) : null; + if (lineMatch?.index !== undefined) { + line = parseLocationNumber(lineMatch[1]) ?? undefined; + if (line === undefined) return { path: "", line, endLine, column }; + if (lineMatch[2]) { + column = parseLocationNumber(lineMatch[2]) ?? undefined; + if (column === undefined) return { path: "", line, endLine, column }; + } + path = path.slice(0, lineMatch.index); + } + + return { path, line, endLine, column }; +} + +function createChatFileLink( + path: string, + source: ChatFileLink["source"], + location: Omit, +): ChatFileLink { + return { + path, + ...(location.line === undefined ? {} : { line: location.line }), + ...(location.endLine === undefined ? {} : { endLine: location.endLine }), + ...(location.column === undefined ? {} : { column: location.column }), + source, + }; +} + +function isSafeRelativePath(path: string) { + return ( + Boolean(path) && + path !== "." && + path !== ".." && + !path.startsWith("#") && + !path.includes("\0") && + !path.includes("\n") && + !path.includes("\r") && + !URI_SCHEME_PATTERN.test(path) + ); +} + +export function parseChatFileLink(raw: string): ChatFileLink | null { + const input = raw.trim(); + if (!input) return null; + + if (FILE_URL_PATTERN.test(input)) { + try { + const url = new URL(input); + if (url.protocol !== "file:") return null; + const hashLocation = url.hash.match(LOCATION_FRAGMENT_PATTERN); + const hashLine = hashLocation ? parseLocationNumber(hashLocation[1]) : null; + const hashEndLine = hashLocation?.[2] ? parseLocationNumber(hashLocation[2]) : null; + if (hashLocation && (hashLine === null || (hashLocation[2] && hashEndLine === null))) { + return null; + } + const locationFromHash = hashLocation + ? { + line: hashLine ?? undefined, + ...(hashEndLine === null ? {} : { endLine: hashEndLine }), + } + : {}; + const pathWithHost = url.host ? `//${safeDecode(url.host)}${url.pathname}` : url.pathname; + const trailing = parseTrailingLocation(pathWithHost); + const normalized = normalizePath(trailing.path).replace(/^\/([a-zA-Z]:)/, "$1"); + if (!normalized || !isAbsolutePath(normalized)) return null; + return createChatFileLink(normalized, "file-url", { + ...trailing, + ...locationFromHash, + }); + } catch { + return null; + } + } + + const { path, line, endLine, column } = parseTrailingLocation(input); + const normalized = normalizePath(path); + + if (isAbsolutePath(normalized)) { + return createChatFileLink(normalized, "absolute", { line, endLine, column }); + } + + if (isSafeRelativePath(normalized)) { + return createChatFileLink(normalized, "relative", { line, endLine, column }); + } + + return null; +} + +export function isChatFileLinkTarget(raw: string) { + return Boolean(parseChatFileLink(raw)); +} + +export function encodeChatFileLink(link: ChatFileLink) { + const params = new URLSearchParams(); + params.set("v", INTERNAL_PAYLOAD_VERSION); + params.set("path", link.path); + if (link.line !== undefined) params.set("line", String(link.line)); + if (link.endLine !== undefined) params.set("endLine", String(link.endLine)); + if (link.column !== undefined) params.set("column", String(link.column)); + params.set("source", link.source); + return `liveagent-file:${params.toString()}`; +} + +export function decodeChatFileLinkPayload(payload: string): ChatFileLink | null { + const params = new URLSearchParams(payload); + const allowedKeys = new Set(["v", "path", "line", "endLine", "column", "source"]); + for (const key of params.keys()) { + if (!allowedKeys.has(key) || params.getAll(key).length !== 1) return null; + } + if (params.get("v") !== INTERNAL_PAYLOAD_VERSION) return null; + const path = params.get("path") ?? ""; + const source = params.get("source"); + if (!path || (source !== "absolute" && source !== "relative" && source !== "file-url")) + return null; + + const normalized = normalizePath(path); + const sourceMatches = + source === "relative" + ? isSafeRelativePath(normalized) && !isAbsolutePath(normalized) + : isAbsolutePath(normalized); + if (!sourceMatches) return null; + + const parseLocationValue = (key: "line" | "endLine" | "column") => { + const value = params.get(key); + if (value === null) return undefined; + if (!/^[1-9]\d*$/.test(value)) return null; + return parseLocationNumber(value); + }; + const line = parseLocationValue("line"); + const endLine = parseLocationValue("endLine"); + const column = parseLocationValue("column"); + if (line === null || endLine === null || column === null) return null; + if ((endLine !== undefined || column !== undefined) && line === undefined) return null; + if (line !== undefined && endLine !== undefined && endLine < line) return null; + + return createChatFileLink(normalized, source, { line, endLine, column }); +} diff --git a/crates/agent-gui/src/lib/chat/openChatFileLink.ts b/crates/agent-gui/src/lib/chat/openChatFileLink.ts new file mode 100644 index 000000000..1071b913a --- /dev/null +++ b/crates/agent-gui/src/lib/chat/openChatFileLink.ts @@ -0,0 +1,33 @@ +import { invoke } from "@tauri-apps/api/core"; + +import type { ChatFileLink } from "./chatFileLinks"; + +export type OpenChatFileLinkParams = ChatFileLink & { + conversationId: string; + workdir: string; + openInFileManager?: boolean; +}; + +export type OpenChatFileLinkResult = { + action: "directory" | "editor" | "opened" | "preview" | "revealed"; + kind: "directory" | "file"; + workdir?: string; + path?: string; + line?: number; + endLine?: number; + column?: number; + outsideWorkspace: boolean; +}; + +export function openChatFileLink(params: OpenChatFileLinkParams) { + return invoke("open_chat_file_link", { + conversation_id: params.conversationId, + workdir: params.workdir, + path: params.path, + source: params.source, + line: params.line, + end_line: params.endLine, + column: params.column, + open_in_file_manager: params.openInFileManager, + }); +} diff --git a/crates/agent-gui/src/pages/ChatPage.tsx b/crates/agent-gui/src/pages/ChatPage.tsx index 39f76edb0..8da88b5b4 100644 --- a/crates/agent-gui/src/pages/ChatPage.tsx +++ b/crates/agent-gui/src/pages/ChatPage.tsx @@ -31,6 +31,7 @@ import { useConfirmDialog } from "../components/ui/confirm-dialog"; import { useLocale } from "../i18n"; import type { AppUpdateController } from "../lib/appUpdates"; import { getAutomationState, useAutomation } from "../lib/automation"; +import type { ChatFileLink } from "../lib/chat/chatFileLinks"; import type { CompactionStatus } from "../lib/chat/compaction/types"; import { buildRequestContext, @@ -41,6 +42,7 @@ import { import type { ChatHistorySummary } from "../lib/chat/history/chatHistory"; import { memoryExtraction } from "../lib/chat/memory/extractionController"; import type { CodeMentionReference } from "../lib/chat/messages/mentionReferences"; +import { openChatFileLink } from "../lib/chat/openChatFileLink"; import { buildFallbackConversationTitle, createConversationIdentity, @@ -667,7 +669,12 @@ export function ChatPage(props: ChatPageProps) { terminalProjectPathKey, rightDockFileTreeOpen, }); - const { handleOpenWorkspaceFile, handleOpenSshTerminal } = workspaceOverlays; + const { + handleOpenWorkspaceFile, + handleOpenSshTerminal, + openWorkspaceEditorFile, + openWorkspaceFilePreview, + } = workspaceOverlays; // ── 回复末尾「已编辑文件」卡的三个动作 ──────────────────────────────── const gitReviewFocusNonceRef = useRef(0); const [gitReviewFocusRequest, setGitReviewFocusRequest] = useState( @@ -750,6 +757,84 @@ export function ChatPage(props: ChatPageProps) { compactionStatus, }); + const handleOpenChatFileLink = useCallback( + (link: ChatFileLink) => { + const conversationId = currentConversationId; + const conversationWorkdir = displayedConversationWorkdir.trim(); + if (!conversationWorkdir) { + addNotify("error", "The conversation working directory is unavailable."); + return; + } + const request = { ...link, conversationId, workdir: conversationWorkdir }; + void openChatFileLink(request) + .then(async (result) => { + if (result.action === "opened" || result.action === "revealed") return; + const resultWorkdir = result.workdir?.trim() ?? ""; + const resultPath = result.path?.trim() ?? ""; + if (!resultWorkdir || !resultPath) { + addNotify("error", "The linked file could not be opened."); + return; + } + if (result.action === "directory") { + if (workspaceProjectPathKey(resultWorkdir) === terminalProjectPathKey) { + handleChangedFileReveal(resultPath); + return; + } + const fallback = await openChatFileLink({ ...request, openInFileManager: true }); + if (fallback.action !== "opened") { + addNotify("error", "The linked directory could not be opened."); + } + return; + } + const workspaceRequest = { + projectPathKey: workspaceProjectPathKey(resultWorkdir), + workdir: resultWorkdir, + path: resultPath, + }; + if ( + !result.outsideWorkspace && + workspaceRequest.projectPathKey === terminalProjectPathKey + ) { + handleChangedFileReveal(resultPath); + } + if (result.action === "preview") { + openWorkspaceFilePreview(workspaceRequest); + return; + } + openWorkspaceEditorFile({ + ...workspaceRequest, + line: result.line, + endLine: result.endLine, + column: result.column, + }); + }) + .catch((error: unknown) => { + const message = + error && typeof error === "object" && "message" in error + ? String((error as { message?: unknown }).message ?? "") + : String(error ?? ""); + const normalized = message.toLowerCase(); + addNotify( + "error", + normalized.includes("timed out") || + normalized.includes("offline") || + normalized.includes("not connected") + ? "The device that owns this conversation is offline or did not respond." + : message || "The linked file could not be opened.", + ); + }); + }, + [ + addNotify, + currentConversationId, + displayedConversationWorkdir, + handleChangedFileReveal, + openWorkspaceEditorFile, + openWorkspaceFilePreview, + terminalProjectPathKey, + ], + ); + const { isUploadingFiles, pendingUploadedFiles, @@ -1888,6 +1973,7 @@ export function ChatPage(props: ChatPageProps) { bottomReservePx={composerOverlayHeight} contentWidth={settings.customSettings.chatTranscript.width} onContentWidthChange={handleChatTranscriptWidthChange} + onOpenFileLink={handleOpenChatFileLink} onResendFromEdit={handleResendFromEdit} onBranchConversation={ // 会话加载中或加载失败时直接不传操作,展示明确的禁用态。 diff --git a/crates/agent-gui/src/pages/chat/components/AssistantBubble.tsx b/crates/agent-gui/src/pages/chat/components/AssistantBubble.tsx index ae9220295..85236e056 100644 --- a/crates/agent-gui/src/pages/chat/components/AssistantBubble.tsx +++ b/crates/agent-gui/src/pages/chat/components/AssistantBubble.tsx @@ -1,5 +1,6 @@ import { memo, type ReactNode } from "react"; +import type { ChatFileLink } from "../../../lib/chat/chatFileLinks"; import type { RetryAttemptRecord } from "../../../lib/chat/conversation/liveTranscriptStore"; import { VIBING_STATUS } from "../../../lib/chat/page/chatPageHelpers"; import type { AssistantUnitRow } from "../transcript/rowModel"; @@ -18,6 +19,8 @@ export const AssistantBubbleUnit = memo(function AssistantBubbleUnit(props: { isCompactionRunning: boolean; toolStatus: string | null; retryAttempts?: RetryAttemptRecord[]; + workdir?: string; + onOpenFileLink?: (link: ChatFileLink) => void; }) { const { row, @@ -27,6 +30,8 @@ export const AssistantBubbleUnit = memo(function AssistantBubbleUnit(props: { isCompactionRunning, toolStatus, retryAttempts, + workdir, + onOpenFileLink, } = props; const { unit } = row; if (unit.kind === "footer") return null; @@ -98,6 +103,8 @@ export const AssistantBubbleUnit = memo(function AssistantBubbleUnit(props: { thinkingOpen={unit.thinkingOpen} isLatestThinking={unit.isLatestThinking} isAborted={row.isAborted} + workdir={workdir} + onOpenFileLink={onOpenFileLink} /> ) : null} diff --git a/crates/agent-gui/src/pages/chat/components/assistant-bubble/RoundContent.tsx b/crates/agent-gui/src/pages/chat/components/assistant-bubble/RoundContent.tsx index 8734139dd..4e1d84ef9 100644 --- a/crates/agent-gui/src/pages/chat/components/assistant-bubble/RoundContent.tsx +++ b/crates/agent-gui/src/pages/chat/components/assistant-bubble/RoundContent.tsx @@ -3,6 +3,7 @@ import { memo, type ReactNode, useEffect, useRef, useState } from "react"; import { ChevronRight, Lightbulb, RefreshCw } from "../../../../components/icons"; import { Markdown } from "../../../../components/Markdown"; import { useLocale } from "../../../../i18n"; +import type { ChatFileLink } from "../../../../lib/chat/chatFileLinks"; import type { RetryAttemptRecord } from "../../../../lib/chat/conversation/liveTranscriptStore"; import type { GroupedRoundBlock } from "./assistantBubbleUtils"; import { HostedSearchGroupView } from "./HostedSearchGroupView"; @@ -17,11 +18,15 @@ const ThinkingBlock = memo(function ThinkingBlock({ open, isRunning, renderMode, + workdir, + onOpenFileLink, }: { text: string; open?: boolean; isRunning?: boolean; renderMode: "streaming" | "static"; + workdir?: string; + onOpenFileLink?: (link: ChatFileLink) => void; }) { const hasText = /\S/.test(text || ""); const { t } = useLocale(); @@ -66,6 +71,8 @@ const ThinkingBlock = memo(function ThinkingBlock({ className="thinking-markdown space-y-1.5" renderMode={renderMode} showCaret={false} + workdir={workdir} + onOpenFileLink={onOpenFileLink} />
)} @@ -131,6 +138,8 @@ export const RoundBlockContent = memo(function RoundBlockContent(props: { thinkingOpen: boolean; isLatestThinking: boolean; isAborted: boolean; + workdir?: string; + onOpenFileLink?: (link: ChatFileLink) => void; }) { const { block, @@ -141,6 +150,8 @@ export const RoundBlockContent = memo(function RoundBlockContent(props: { thinkingOpen, isLatestThinking, isAborted, + workdir, + onOpenFileLink, } = props; let content: ReactNode; @@ -152,6 +163,8 @@ export const RoundBlockContent = memo(function RoundBlockContent(props: { open={isRunning} isRunning={isRunning} renderMode={renderMode} + workdir={workdir} + onOpenFileLink={onOpenFileLink} /> ); } else if (block.kind === "tool") { @@ -193,6 +206,8 @@ export const RoundBlockContent = memo(function RoundBlockContent(props: { className="font-chat" renderMode={renderMode} showCaret={Boolean(isLive && isMutable)} + workdir={workdir} + onOpenFileLink={onOpenFileLink} /> ); } else { diff --git a/crates/agent-gui/src/pages/chat/transcript/AssistantRenderUnit.tsx b/crates/agent-gui/src/pages/chat/transcript/AssistantRenderUnit.tsx index 16f6924b8..3b775eb27 100644 --- a/crates/agent-gui/src/pages/chat/transcript/AssistantRenderUnit.tsx +++ b/crates/agent-gui/src/pages/chat/transcript/AssistantRenderUnit.tsx @@ -1,6 +1,7 @@ import { memo, useMemo } from "react"; import { ChangedFilesCard } from "../../../components/chat/ChangedFilesCard"; +import type { ChatFileLink } from "../../../lib/chat/chatFileLinks"; import type { HistoryMessageRef } from "../../../lib/chat/conversation/conversationState"; import type { RetryAttemptRecord } from "../../../lib/chat/conversation/liveTranscriptStore"; import { collectChangedFiles } from "../../../lib/chat/messages/changedFiles"; @@ -17,6 +18,8 @@ export type AssistantRenderUnitProps = { isCompactionRunning: boolean; toolStatus: string | null; retryAttempts?: RetryAttemptRecord[]; + workdir?: string; + onOpenFileLink?: (link: ChatFileLink) => void; onResendFromEdit: ( messageRef: HistoryMessageRef, text: string, @@ -79,6 +82,8 @@ export const AssistantRenderUnit = memo(function AssistantRenderUnit( isCompactionRunning, toolStatus, retryAttempts, + workdir, + onOpenFileLink, onResendFromEdit, onBranchConversation, } = props; @@ -106,6 +111,8 @@ export const AssistantRenderUnit = memo(function AssistantRenderUnit( isCompactionRunning={isCompactionRunning} toolStatus={toolStatus} retryAttempts={retryAttempts} + workdir={workdir} + onOpenFileLink={onOpenFileLink} /> ); diff --git a/crates/agent-gui/src/pages/chat/transcript/ChatTranscript.tsx b/crates/agent-gui/src/pages/chat/transcript/ChatTranscript.tsx index 2aa6dd2ba..bf3fb1335 100644 --- a/crates/agent-gui/src/pages/chat/transcript/ChatTranscript.tsx +++ b/crates/agent-gui/src/pages/chat/transcript/ChatTranscript.tsx @@ -54,6 +54,7 @@ export const ChatTranscript = memo(function ChatTranscript(props: ChatTranscript bottomReservePx = 0, contentWidth, onContentWidthChange, + onOpenFileLink, onResendFromEdit, onBranchConversation, branchPendingMessageId, @@ -322,6 +323,7 @@ export const ChatTranscript = memo(function ChatTranscript(props: ChatTranscript showUsage={showUsage} usageContextWindow={usageContextWindow} workspaceRoot={workspaceRoot} + onOpenFileLink={onOpenFileLink} gitClient={gitClient} navRef={transcriptNavRef} onAnchorUserRowChange={setActiveFloorKey} diff --git a/crates/agent-gui/src/pages/chat/transcript/TranscriptList.tsx b/crates/agent-gui/src/pages/chat/transcript/TranscriptList.tsx index 5d1d07541..6f775dc14 100644 --- a/crates/agent-gui/src/pages/chat/transcript/TranscriptList.tsx +++ b/crates/agent-gui/src/pages/chat/transcript/TranscriptList.tsx @@ -15,6 +15,7 @@ import { import { CheckCircle2, ChevronDown } from "../../../components/icons"; import { Markdown } from "../../../components/Markdown"; import { useLocale } from "../../../i18n"; +import type { ChatFileLink } from "../../../lib/chat/chatFileLinks"; import type { HistoryMessageRef, RenderSummaryCard, @@ -117,6 +118,7 @@ export type TranscriptListProps = { usageContextWindow?: number; workspaceRoot?: string; gitClient?: GitClient | null; + onOpenFileLink?: (link: ChatFileLink) => void; // 楼层导航:跳转句柄挂载点(与 followRef 同一模式),以及「视口顶部 // 当前处于哪条用户消息行」变化时的上报回调。 navRef?: MutableRefObject; @@ -152,6 +154,7 @@ export const TranscriptList = memo(function TranscriptList(props: TranscriptList usageContextWindow, workspaceRoot, gitClient, + onOpenFileLink, navRef, onAnchorUserRowChange, onResendFromEdit, @@ -522,6 +525,8 @@ export const TranscriptList = memo(function TranscriptList(props: TranscriptList isCompactionRunning={row.mutable ? isCompactionRunning : false} toolStatus={row.mutable ? displayedToolStatus : null} retryAttempts={row.mutable ? liveState.retryAttempts : undefined} + workdir={workspaceRoot} + onOpenFileLink={onOpenFileLink} onResendFromEdit={onResendFromEdit} onBranchConversation={onBranchConversation} /> diff --git a/crates/agent-gui/src/pages/chat/transcript/transcriptTypes.ts b/crates/agent-gui/src/pages/chat/transcript/transcriptTypes.ts index ef640313b..5640481cd 100644 --- a/crates/agent-gui/src/pages/chat/transcript/transcriptTypes.ts +++ b/crates/agent-gui/src/pages/chat/transcript/transcriptTypes.ts @@ -1,5 +1,5 @@ import type { MutableRefObject } from "react"; - +import type { ChatFileLink } from "../../../lib/chat/chatFileLinks"; import type { HistoryMessageRef, RenderTimelineItem, @@ -31,6 +31,7 @@ export type ChatTranscriptProps = { bottomReservePx?: number; contentWidth: number; onContentWidthChange: (width: number) => void; + onOpenFileLink?: (link: ChatFileLink) => void; onResendFromEdit: ( messageRef: HistoryMessageRef, text: string, diff --git a/crates/agent-gui/test/chat/chat-file-links.test.mjs b/crates/agent-gui/test/chat/chat-file-links.test.mjs new file mode 100644 index 000000000..2ba3a2a57 --- /dev/null +++ b/crates/agent-gui/test/chat/chat-file-links.test.mjs @@ -0,0 +1,153 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; + +import { createTsModuleLoader } from "../helpers/load-ts-module.mjs"; + +const loader = createTsModuleLoader(); +const { + decodeChatFileLinkPayload, + encodeChatFileLink, + parseChatFileLink, +} = loader.loadModule("src/lib/chat/messages/chatFileLinks.ts"); + +const validCases = [ + ["C:/work/src/a.ts", { path: "C:/work/src/a.ts", source: "absolute" }], + [String.raw`C:\work\src\a.ts`, { path: "C:/work/src/a.ts", source: "absolute" }], + [String.raw`C:\\project\\file.ts`, { path: "C:/project/file.ts", source: "absolute" }], + ["D:/other/a.ts", { path: "D:/other/a.ts", source: "absolute" }], + ["C:/work/src/a.ts:12", { path: "C:/work/src/a.ts", line: 12, source: "absolute" }], + [ + "C:/work/src/a.ts:12:4", + { path: "C:/work/src/a.ts", line: 12, column: 4, source: "absolute" }, + ], + ["C:/work/src/a.ts#L12", { path: "C:/work/src/a.ts", line: 12, source: "absolute" }], + [ + "C:/work/src/a.ts#L12-L20", + { path: "C:/work/src/a.ts", line: 12, endLine: 20, source: "absolute" }, + ], + ["C:/path with spaces/a.ts", { path: "C:/path with spaces/a.ts", source: "absolute" }], + ["C:/path%20with%20spaces/a.ts", { path: "C:/path with spaces/a.ts", source: "absolute" }], + ["C:/路径/文件.ts", { path: "C:/路径/文件.ts", source: "absolute" }], + [ + "file:///C:/work/a.ts", + { path: "C:/work/a.ts", source: "file-url" }, + ], + [ + "file:///C:/path%20with%20spaces/%E6%96%87%E4%BB%B6.ts#L7-L9", + { + path: "C:/path with spaces/文件.ts", + line: 7, + endLine: 9, + source: "file-url", + }, + ], + ["/home/user/work/a.ts", { path: "/home/user/work/a.ts", source: "absolute" }], + ["./src/a.ts", { path: "./src/a.ts", source: "relative" }], + ["../src/a.ts:3:2", { path: "../src/a.ts", line: 3, column: 2, source: "relative" }], + ["src/a.ts", { path: "src/a.ts", source: "relative" }], + ["README.md", { path: "README.md", source: "relative" }], + [ + String.raw`\\server\share\folder\a.ts:8`, + { path: "//server/share/folder/a.ts", line: 8, source: "absolute" }, + ], + [ + "file://server/share/folder/a.ts#L4", + { path: "//server/share/folder/a.ts", line: 4, source: "file-url" }, + ], +]; + +test("parseChatFileLink supports the cross-platform chat path matrix", () => { + for (const [input, expected] of validCases) { + assert.deepEqual(parseChatFileLink(input), expected, input); + } +}); + +test("parseChatFileLink rejects external, dangerous, internal, and malformed targets", () => { + for (const input of [ + "javascript:alert(1)", + "data:text/html,", + "vbscript:msgbox(1)", + "https://example.com/a.ts", + "http://example.com/a.ts", + "mailto:user@example.com", + "liveagent-file:path=C%3A%2Fa.ts&source=absolute", + "C:drive-relative.txt", + "C:/work/a.ts:4294967296", + "file:///C:/work/a.ts#L999999999999999999999999", + "#L12", + "", + ]) { + assert.equal(parseChatFileLink(input), null, input); + } + + assert.doesNotThrow(() => parseChatFileLink("file:///%E0%A4%A")); +}); + +test("Gateway chat file opens run off-loop with bounded host concurrency", () => { + const envelopeHandler = fs.readFileSync( + fileURLToPath( + new URL( + "../../src-tauri/src/services/gateway/envelope_handler.rs", + import.meta.url, + ), + ), + "utf8", + ); + const chatFileLinks = fs.readFileSync( + fileURLToPath( + new URL( + "../../src-tauri/src/commands/workspace/chat_file_links.rs", + import.meta.url, + ), + ), + "utf8", + ); + + const branch = envelopeHandler.slice( + envelopeHandler.indexOf("Payload::ChatFileOpen"), + envelopeHandler.indexOf("Payload::FsWriteText"), + ); + assert.match(branch, /tauri::async_runtime::spawn/); + assert.match(branch, /let sender = self\.current_outbound_sender\(\)\?/); + assert.match(branch, /send_agent_envelope_to\(sender, envelope\)/); + assert.ok( + branch.indexOf("tauri::async_runtime::spawn") < + branch.indexOf("handle_chat_file_open(request).await"), + ); + assert.match(chatFileLinks, /tokio::time::timeout\(CHAT_FILE_OPEN_TIMEOUT/); + assert.match(chatFileLinks, /CHAT_FILE_OPEN_SEMAPHORE/); + assert.match(chatFileLinks, /let _permit = permit/); +}); + +test("the internal payload codec preserves locations and rejects malformed payloads", () => { + const original = { + path: "C:/路径/a file.ts", + line: 12, + endLine: 20, + column: 4, + source: "absolute", + }; + const encoded = encodeChatFileLink(original); + assert.match(encoded, /^liveagent-file:/); + assert.deepEqual( + decodeChatFileLinkPayload(encoded.slice("liveagent-file:".length)), + original, + ); + + for (const payload of [ + "path=&source=absolute", + "v=1&path=&source=absolute", + "v=1&path=C%3A%2Fa.ts&source=unknown", + "v=1&path=C%3A%2Fa.ts&source=absolute&line=0", + "v=1&path=C%3A%2Fa.ts&source=absolute&line=-1", + "v=1&path=C%3A%2Fa.ts&source=absolute&line=abc", + "v=1&path=C%3A%2Fa.ts&source=absolute&line=4294967296", + "v=1&path=C%3A%2Fa.ts&source=absolute&line=20&endLine=12", + "v=1&path=C%3A%2Fa.ts&path=D%3A%2Fb.ts&source=absolute", + "v=1&path=C%3A%2Fa.ts&source=absolute&extra=true", + ]) { + assert.equal(decodeChatFileLinkPayload(payload), null, payload); + } +}); diff --git a/crates/agent-gui/test/chat/markdown-image-policy.test.mjs b/crates/agent-gui/test/chat/markdown-image-policy.test.mjs index 937cc73f2..606ee1375 100644 --- a/crates/agent-gui/test/chat/markdown-image-policy.test.mjs +++ b/crates/agent-gui/test/chat/markdown-image-policy.test.mjs @@ -5,6 +5,10 @@ import { fileURLToPath } from "node:url"; import { createTsModuleLoader } from "../helpers/load-ts-module.mjs"; +const rawPlugin = () => {}; +const sanitizePlugin = () => {}; +const hardenPlugin = () => {}; + const loader = createTsModuleLoader({ mocks: { "@streamdown/cjk": { @@ -24,7 +28,19 @@ const loader = createTsModuleLoader({ return { type: "Streamdown", props }; }, defaultRemarkPlugins: {}, - defaultRehypePlugins: {}, + defaultRehypePlugins: { + raw: rawPlugin, + sanitize: [ + sanitizePlugin, + { + protocols: { + href: ["http", "https", "mailto"], + src: ["http", "https"], + }, + }, + ], + harden: hardenPlugin, + }, }, "@tauri-apps/plugin-opener": { openUrl() { @@ -129,6 +145,258 @@ test("markdown image syntax falls back to alt text instead of rendering a real i assert.equal(empty, null); }); +test("chat file links are rewritten before sanitize and harden without widening unsafe schemes", () => { + const fileNode = { + type: "element", + tagName: "a", + properties: { href: "C:/Users/AlphaCat/claude-code-request.curl.ps1" }, + children: [{ type: "text", value: "claude-code-request.curl.ps1" }], + }; + const httpsNode = { + type: "element", + tagName: "a", + properties: { href: "https://example.com" }, + children: [], + }; + const dangerousNode = { + type: "element", + tagName: "a", + properties: { href: "javascript:alert(1)" }, + children: [], + }; + const rawHtmlNode = { + type: "element", + tagName: "a", + properties: { href: "C:/Users/AlphaCat/disguised.ps1" }, + position: { start: { offset: 0 } }, + children: [{ type: "text", value: "https://example.com" }], + }; + markdownModule.rewriteChatFileLinks()({ + type: "root", + children: [fileNode, httpsNode, dangerousNode], + }); + markdownModule.rewriteChatFileLinks()( + { type: "root", children: [rawHtmlNode] }, + { value: 'https://example.com' }, + ); + + assert.match(fileNode.properties.href, /^liveagent-file:/); + assert.equal(fileNode.data.liveagentChatFileLink, fileNode.properties.href); + assert.equal(httpsNode.properties.href, "https://example.com"); + assert.equal(dangerousNode.properties.href, "javascript:alert(1)"); + assert.equal(rawHtmlNode.properties.href, "C:/Users/AlphaCat/disguised.ps1"); + assert.equal(rawHtmlNode.data, undefined); + + const plugins = markdownModule.chatFileRehypePlugins; + assert.equal(plugins[0], rawPlugin); + assert.equal(plugins[1], markdownModule.rewriteChatFileLinks); + assert.equal(plugins[2][0], sanitizePlugin); + assert.equal(plugins.at(-1), hardenPlugin); + const hrefProtocols = plugins[2][1].protocols.href; + assert.ok(hrefProtocols.includes("liveagent-file")); + for (const scheme of ["file", "c", "d", "javascript", "data", "vbscript"]) { + assert.equal(hrefProtocols.includes(scheme), false, scheme); + } +}); + +test("raw spaces and backslashes become Markdown file links without touching code", () => { + const source = "Open [space](C:/path with spaces/a.ts) and [windows](C:\\work\\src\\a.ts)."; + const tree = { + type: "root", + children: [ + { + type: "paragraph", + children: [ + { + type: "text", + value: source, + position: { start: { offset: 0 }, end: { offset: source.length } }, + }, + ], + }, + { + type: "code", + value: "[literal](C:/path with spaces/a.ts)", + }, + { + type: "image", + url: "C:/path with spaces/image.png", + value: "image", + }, + { + type: "math", + value: String.raw`[x](C:\math\formula.ts)`, + }, + { + type: "table", + children: [{ type: "tableRow", children: [{ type: "text", value: "plain cell" }] }], + }, + ], + }; + markdownModule.remarkChatFileLinks()(tree, { value: source }); + + const paragraph = tree.children[0].children; + assert.equal(paragraph.filter((node) => node.type === "link").length, 2); + assert.equal(paragraph[1].url, "C:/path with spaces/a.ts"); + assert.equal(paragraph[3].url, "C:\\work\\src\\a.ts"); + assert.equal(tree.children[1].value, "[literal](C:/path with spaces/a.ts)"); + assert.equal(tree.children[2].url, "C:/path with spaces/image.png"); + assert.equal(tree.children[3].value, String.raw`[x](C:\math\formula.ts)`); + assert.equal(tree.children[4].type, "table"); + assert.equal(tree.children[4].children[0].children[0].value, "plain cell"); +}); + +test("escaped Markdown file links stay literal while a following link remains clickable", () => { + const source = String.raw`\[foo\*](README.md) and [foo*](README.md)`; + const tree = { + type: "root", + children: [ + { + type: "paragraph", + children: [ + { + type: "text", + value: "[foo*](README.md) and [foo*](README.md)", + position: { start: { offset: 0 }, end: { offset: source.length } }, + }, + ], + }, + ], + }; + + markdownModule.remarkChatFileLinks()(tree, { value: source }); + + assert.deepEqual(tree.children[0].children, [ + { type: "text", value: "[foo*](README.md) and " }, + { + type: "link", + url: "README.md", + children: [{ type: "text", value: "foo*" }], + }, + ]); +}); + +test("linked editor locations are applied once per request and tab in both frontends", () => { + const files = [ + "../../src/components/workspace-editor/WorkspaceCodeEditorOverlay.tsx", + "../../../agent-gateway/web/src/components/workspace-editor/WorkspaceCodeEditorOverlay.tsx", + ]; + for (const relativePath of files) { + const source = fs.readFileSync(fileURLToPath(new URL(relativePath, import.meta.url)), "utf8"); + assert.match(source, /linkedLocationKeyRef/); + assert.match(source, /const locationKey = `\$\{openRequest\.id\}\\u0000\$\{activeTabKey\}`/); + assert.match(source, /if \(linkedLocationKeyRef\.current === locationKey\) return/); + assert.doesNotMatch(source, /\}, \[activeTab, openRequest\]\);/); + } +}); + +test("the reported ps1 link renders as one accessible click target and never executes itself", () => { + const fileNode = { + type: "element", + tagName: "a", + properties: { href: "C:/Users/AlphaCat/claude-code-request.curl.ps1" }, + children: [], + }; + markdownModule.rewriteChatFileLinks()({ type: "root", children: [fileNode] }); + const opened = []; + const button = markdownModule.MarkdownFileLink({ + children: "claude-code-request.curl.ps1", + href: fileNode.properties.href, + node: fileNode, + workdir: "C:/Users/AlphaCat", + onOpenFileLink(link) { + opened.push(link); + }, + }); + + assert.equal(button.type, "button"); + assert.equal(button.props.type, "button"); + assert.equal(button.props["data-liveagent-file-link"], "true"); + assert.equal(button.props.children, "claude-code-request.curl.ps1"); + assert.doesNotMatch(String(button.props.children), /\[blocked\]/); + assert.match(button.props.className, /overflow-wrap:anywhere/); + assert.match(button.props.className, /max-w-full/); + assert.match(button.props.className, /whitespace-normal/); + button.props.onClick(); + assert.deepEqual(opened, [ + { + path: "C:/Users/AlphaCat/claude-code-request.curl.ps1", + source: "absolute", + }, + ]); +}); + +test("historical and streaming assistant rows share the explicit file-open prop chain", () => { + const files = [ + "../../src/pages/chat/transcript/ChatTranscript.tsx", + "../../src/pages/chat/transcript/TranscriptList.tsx", + "../../src/pages/chat/transcript/AssistantRenderUnit.tsx", + "../../src/pages/chat/components/AssistantBubble.tsx", + "../../src/pages/chat/components/assistant-bubble/RoundContent.tsx", + ]; + for (const relativePath of files) { + const source = fs.readFileSync(fileURLToPath(new URL(relativePath, import.meta.url)), "utf8"); + assert.match(source, /onOpenFileLink/, relativePath); + } + + const roundContent = fs.readFileSync( + fileURLToPath( + new URL( + "../../src/pages/chat/components/assistant-bubble/RoundContent.tsx", + import.meta.url, + ), + ), + "utf8", + ); + assert.match(roundContent, /export const RoundBlockContent/); + assert.match(roundContent, /renderMode=\{renderMode\}/); + assert.ok((roundContent.match(/onOpenFileLink=\{onOpenFileLink\}/g) ?? []).length >= 2); + assert.ok((roundContent.match(/workdir=\{workdir\}/g) ?? []).length >= 2); + + const transcriptList = fs.readFileSync( + fileURLToPath(new URL("../../src/pages/chat/transcript/TranscriptList.tsx", import.meta.url)), + "utf8", + ); + assert.match(transcriptList, /isCompactionRunning=\{row\.mutable/); + assert.match(transcriptList, /workdir=\{workspaceRoot\}/); + assert.match(transcriptList, /onOpenFileLink=\{onOpenFileLink\}/); + + const chatPage = fs.readFileSync( + fileURLToPath(new URL("../../src/pages/ChatPage.tsx", import.meta.url)), + "utf8", + ); + assert.match(chatPage, /openInFileManager: true/); + assert.match(chatPage, /!result\.outsideWorkspace/); +}); + +test("forged internal payloads cannot become clickable file links", () => { + let opened = 0; + const forged = markdownModule.MarkdownFileLink({ + children: "not-a-real-marker.ps1", + href: "liveagent-file:v=1&path=C%3A%2Fnot-a-real-marker.ps1&source=absolute", + node: { type: "element", tagName: "a", properties: {} }, + onOpenFileLink() { + opened += 1; + }, + }); + const rendered = forged.type(forged.props); + assert.equal(rendered.type, "span"); + assert.equal(rendered.props["data-liveagent-file-link"], undefined); + assert.equal(opened, 0); +}); + +test("ordinary https links stay on the external-link path", () => { + const routed = markdownModule.MarkdownLink({ + children: "example", + href: "https://example.com", + node: { type: "element", tagName: "a", properties: { href: "https://example.com" } }, + onOpenFileLink() { + throw new Error("https link must not open as a chat file"); + }, + }); + assert.equal(routed.type.name, "MarkdownExternalLink"); +}); + test("external link safety modal renders through document body portal", () => { const previousDocument = globalThis.document; const body = { nodeType: 1 }; diff --git a/docs/worklog/chat-file-links.md b/docs/worklog/chat-file-links.md new file mode 100644 index 000000000..4dc30d57b --- /dev/null +++ b/docs/worklog/chat-file-links.md @@ -0,0 +1,90 @@ +# Chat file links + +## Goal + +Provide one safe, cross-platform chat file-link path for desktop and Gateway Web. Historical and streaming assistant Markdown must recognize local paths without weakening URL sanitization, and user clicks must resolve/open the path on the conversation's owning host. + +## Baseline + +- Branch: `fix/chat-file-links` +- Base: `upstream/main` at `410eef1d`; fetched and verified on 2026-07-29. +- Existing unrelated worktree changes are present and must be preserved, notably `Cargo.lock` and local/untracked project files. +- Pre-existing draft files for `chatFileLinks.ts` and GUI `Markdown.tsx` were found. Audit identified duplicate plugin declarations, sanitize/harden bypass, external-link regression, unsafe scheme classification, and missing click plumbing. Treat the draft as unverified input, not completed work. +- `.trellis/` is absent, so `trellis-before-dev` could not load package specs. Repository CodeGraph was synced and used for discovery. + +## Execution plan + +1. Add failing parser and Markdown rewrite tests for the reported Windows path and the complete path/security matrix. +2. Implement byte-identical GUI/Web parser and safe pre-sanitize rewrite using only `liveagent-file:`. +3. Add accessible file-link rendering while preserving Streamdown external-link confirmation and dangerous-protocol blocking. +4. Carry conversation id/workdir and the click callback through Transcript → AssistantRow → AssistantBubble → RoundContent → Markdown. +5. Add a Tauri command that resolves relative paths against the conversation workdir, canonicalizes, classifies, and returns a safe action. Never execute scripts/executables. +6. Extend the Gateway protobuf relay and Web shim so requests are sent only to the selected/owning agent, with offline/mismatch/timeout errors. +7. Reuse/extend workspace editor and preview requests for in-app text/preview handling and line/column location. +8. Run targeted tests, Rust/Go checks, GUI/Web build+lint+tests, mirror check, diff check, and responsive UI verification. + +## Recovery checkpoint + +Implementation is complete across the parser, Markdown pipeline, explicit GUI/Web prop chains, +workspace editor location handling, controlled Rust open policy, and Gateway protobuf relay. + +Key invariants now enforced: + +- `raw → rewriteChatFileLinks → sanitize → harden`; only `liveagent-file:` is added. +- Internal payloads require an AST marker created before sanitize plus a canonical codec round trip. +- The target agent reloads `conversation_id` from its own history database and uses the stored + `cwd`; the request-supplied workdir is never the relative-path base. +- Paths are canonicalized after click. Missing targets fail before any system process is spawned. +- Scripts open in the editor; executable/active-content files and bundles are revealed, never run. +- Workspace directories use the file tree first, then a validated directory-only file-manager + fallback. Workspace-external previewable files reuse existing editor/preview readers. + +Completed checks: + +- GUI parser/Markdown/security tests: 20 passed. +- Gateway parser/adapter/prop-chain tests: 11 passed; complete Web suite: 483 passed. +- Go `internal/protocol/pbws`: passed. Full `go test ./...` passed every other package but is + blocked on Windows by the pre-existing `agenttoken` permission assertion (0666 vs 0600). +- Rust `chat_file_links` tests: 5 passed using temporary `libclang` tooling; + `cargo check --tests` passed with five unrelated existing warnings. +- GUI and Gateway Web production builds and `tsc` passed. +- Full GUI frontend suite ran 1,353 tests: 1,348 passed and five unrelated static-source/preset + sync tests failed (`mention-composer-selection`, `mention-refetch`, provider usage preset sync). +- Full GUI/Web Biome checks were attempted and are blocked by hundreds of existing repository + diagnostics/CRLF formatting differences. Task-core targeted Biome lint passed; large touched + host files reported only existing warnings and no diagnostics on the new handlers. +- Mirror check passed for all 116 mirrored files; `git diff --check` passed. +- Playwright narrow-width check: 390px and 412px had no horizontal overflow; mouse, Enter, + and Space each generated one click; tooltip and focus outline were present. + +## Hardening follow-up — 2026-07-29 + +The follow-up closes the remaining review findings without changing the public file-link contract: + +- Escaped Markdown links remain literal, including candidates whose label contains escaped punctuation +- Linked editor locations apply once per request and tab, so typing no longer resets the caret +- Directories are classified before file extensions; macOS active directory packages fail closed +- Unknown binary and active targets never reach the host opener +- Gateway file-open work runs outside the serial envelope loop, with a 25-second timeout and a four-request concurrency limit +- Detached Gateway responses stay bound to the outbound connection that received the request + +Verification for the follow-up snapshot: + +- GUI targeted chat/Markdown tests: 23 passed +- Gateway targeted chat-file-link tests: 5 passed +- Rust `chat_file_links` tests: 5 passed; `cargo check --tests` passed with five unrelated existing warnings +- GUI and Gateway Web `pnpm build`: passed +- Focused Biome checks for the changed Markdown/editor logic: passed with only existing editor accessibility warnings +- Full package Biome checks remain unusable on this Windows checkout because untouched files produce hundreds of existing CRLF-format and lint diagnostics; no broad formatting rewrite was applied +- `cargo fmt --check`, mirror check for 116 files, and `git diff --check`: passed +- `Cargo.lock`, `Cargo.toml`, generated build output, and unrelated local files are excluded from the follow-up + +The hardening implementation and local verification are complete. Commit, push, PR creation, and remote CI follow this checkpoint. + +## Maintainer safety follow-up — 2026-07-29 + +Host file-manager fallbacks now reveal or select directory targets instead of opening the targets +through platform file associations. This keeps unrecognized macOS bundles such as `.prefPane`, +`.saver`, `.bundle`, and `.plugin` directories from being launched after a chat-file-link click. +Platform command construction and the directory-package plan are covered by focused Rust regression +tests. diff --git a/scripts/mirror-manifest.json b/scripts/mirror-manifest.json index 37a3df542..8f063e537 100644 --- a/scripts/mirror-manifest.json +++ b/scripts/mirror-manifest.json @@ -36,6 +36,7 @@ "components/chat/AskUserQuestionCard.tsx", "components/CliIdentityUpdateHost.tsx", "lib/chat/askUserQuestion.ts", + "lib/chat/openChatFileLink.ts", "components/Markdown.tsx", "lib/markdownCodeBlockPolicy.ts", "lib/normalizeLatexDelimiters.ts",