From 9e5122ff2a49bb0e163959aa13868a944b8d3c0c Mon Sep 17 00:00:00 2001 From: su-fen <715041@qq.com> Date: Tue, 2 Jun 2026 17:47:10 +0800 Subject: [PATCH] feat(project-tools): preview workspace images from file tree --- .../internal/proto/v1/gateway.pb.go | 443 +++++++++++++----- .../internal/server/websocket_fs_handlers.go | 62 +++ .../internal/server/websocket_payload_test.go | 18 + .../internal/server/websocket_routes.go | 1 + .../internal/server/websocket_routes_test.go | 1 + crates/agent-gateway/proto/v1/gateway.proto | 16 + crates/agent-gateway/web/src/App.tsx | 83 +++- .../project-tools/ProjectFileTreePanel.tsx | 26 +- .../project-tools/ProjectToolsPanel.tsx | 6 +- .../WorkspaceImagePreviewOverlay.tsx | 194 ++++++++ .../workspace-editor/workspaceImagePreview.ts | 18 + crates/agent-gateway/web/src/i18n/config.ts | 14 + .../web/src/lib/gatewaySocket.ts | 30 ++ .../web/src/lib/gatewaySocket.worker.ts | 2 + .../agent-gateway/web/src/shims/tauriCore.ts | 5 + crates/agent-gateway/web/src/styles.css | 3 +- crates/agent-gui/src-tauri/src/commands/fs.rs | 55 +++ crates/agent-gui/src-tauri/src/lib.rs | 1 + .../src-tauri/src/services/gateway.rs | 15 + .../src-tauri/src/services/gateway_bridge.rs | 27 +- .../project-tools/ProjectFileTreePanel.tsx | 26 +- .../project-tools/ProjectToolsPanel.tsx | 6 +- .../WorkspaceImagePreviewOverlay.tsx | 196 ++++++++ .../workspace-editor/workspaceImagePreview.ts | 18 + crates/agent-gui/src/i18n/config.ts | 14 + crates/agent-gui/src/pages/ChatPage.tsx | 83 +++- 26 files changed, 1186 insertions(+), 177 deletions(-) create mode 100644 crates/agent-gateway/web/src/components/workspace-editor/WorkspaceImagePreviewOverlay.tsx create mode 100644 crates/agent-gateway/web/src/components/workspace-editor/workspaceImagePreview.ts create mode 100644 crates/agent-gui/src/components/workspace-editor/WorkspaceImagePreviewOverlay.tsx create mode 100644 crates/agent-gui/src/components/workspace-editor/workspaceImagePreview.ts diff --git a/crates/agent-gateway/internal/proto/v1/gateway.pb.go b/crates/agent-gateway/internal/proto/v1/gateway.pb.go index eff8d9a6..0d65fb22 100644 --- a/crates/agent-gateway/internal/proto/v1/gateway.pb.go +++ b/crates/agent-gateway/internal/proto/v1/gateway.pb.go @@ -247,6 +247,7 @@ type GatewayEnvelope struct { // *GatewayEnvelope_FsDelete // *GatewayEnvelope_GitRequest // *GatewayEnvelope_FsReadEditableText + // *GatewayEnvelope_FsReadWorkspaceImage Payload isGatewayEnvelope_Payload `protobuf_oneof:"payload"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -627,6 +628,15 @@ func (x *GatewayEnvelope) GetFsReadEditableText() *FsReadEditableTextRequest { return nil } +func (x *GatewayEnvelope) GetFsReadWorkspaceImage() *FsReadWorkspaceImageRequest { + if x != nil { + if x, ok := x.Payload.(*GatewayEnvelope_FsReadWorkspaceImage); ok { + return x.FsReadWorkspaceImage + } + } + return nil +} + type isGatewayEnvelope_Payload interface { isGatewayEnvelope_Payload() } @@ -775,6 +785,10 @@ type GatewayEnvelope_FsReadEditableText struct { FsReadEditableText *FsReadEditableTextRequest `protobuf:"bytes,62,opt,name=fs_read_editable_text,json=fsReadEditableText,proto3,oneof"` } +type GatewayEnvelope_FsReadWorkspaceImage struct { + FsReadWorkspaceImage *FsReadWorkspaceImageRequest `protobuf:"bytes,63,opt,name=fs_read_workspace_image,json=fsReadWorkspaceImage,proto3,oneof"` +} + func (*GatewayEnvelope_ChatRequest) isGatewayEnvelope_Payload() {} func (*GatewayEnvelope_CancelChat) isGatewayEnvelope_Payload() {} @@ -847,6 +861,8 @@ func (*GatewayEnvelope_GitRequest) isGatewayEnvelope_Payload() {} func (*GatewayEnvelope_FsReadEditableText) isGatewayEnvelope_Payload() {} +func (*GatewayEnvelope_FsReadWorkspaceImage) 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"` @@ -891,6 +907,7 @@ type AgentEnvelope struct { // *AgentEnvelope_FsDeleteResp // *AgentEnvelope_GitResponse // *AgentEnvelope_FsReadEditableTextResp + // *AgentEnvelope_FsReadWorkspaceImageResp // *AgentEnvelope_Error Payload isAgentEnvelope_Payload `protobuf_oneof:"payload"` unknownFields protoimpl.UnknownFields @@ -1290,6 +1307,15 @@ func (x *AgentEnvelope) GetFsReadEditableTextResp() *FsReadEditableTextResponse return nil } +func (x *AgentEnvelope) GetFsReadWorkspaceImageResp() *FsReadWorkspaceImageResponse { + if x != nil { + if x, ok := x.Payload.(*AgentEnvelope_FsReadWorkspaceImageResp); ok { + return x.FsReadWorkspaceImageResp + } + } + return nil +} + func (x *AgentEnvelope) GetError() *ErrorResponse { if x != nil { if x, ok := x.Payload.(*AgentEnvelope_Error); ok { @@ -1455,6 +1481,10 @@ type AgentEnvelope_FsReadEditableTextResp struct { FsReadEditableTextResp *FsReadEditableTextResponse `protobuf:"bytes,65,opt,name=fs_read_editable_text_resp,json=fsReadEditableTextResp,proto3,oneof"` } +type AgentEnvelope_FsReadWorkspaceImageResp struct { + FsReadWorkspaceImageResp *FsReadWorkspaceImageResponse `protobuf:"bytes,66,opt,name=fs_read_workspace_image_resp,json=fsReadWorkspaceImageResp,proto3,oneof"` +} + type AgentEnvelope_Error struct { Error *ErrorResponse `protobuf:"bytes,99,opt,name=error,proto3,oneof"` } @@ -1535,6 +1565,8 @@ func (*AgentEnvelope_GitResponse) isAgentEnvelope_Payload() {} func (*AgentEnvelope_FsReadEditableTextResp) isAgentEnvelope_Payload() {} +func (*AgentEnvelope_FsReadWorkspaceImageResp) isAgentEnvelope_Payload() {} + func (*AgentEnvelope_Error) isAgentEnvelope_Payload() {} type ChatSelectedModel struct { @@ -6061,6 +6093,142 @@ func (x *FsReadEditableTextResponse) GetTotalLines() uint64 { return 0 } +type FsReadWorkspaceImageRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Workdir string `protobuf:"bytes,1,opt,name=workdir,proto3" json:"workdir,omitempty"` + Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FsReadWorkspaceImageRequest) Reset() { + *x = FsReadWorkspaceImageRequest{} + mi := &file_proto_v1_gateway_proto_msgTypes[81] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FsReadWorkspaceImageRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FsReadWorkspaceImageRequest) ProtoMessage() {} + +func (x *FsReadWorkspaceImageRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_v1_gateway_proto_msgTypes[81] + 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 FsReadWorkspaceImageRequest.ProtoReflect.Descriptor instead. +func (*FsReadWorkspaceImageRequest) Descriptor() ([]byte, []int) { + return file_proto_v1_gateway_proto_rawDescGZIP(), []int{81} +} + +func (x *FsReadWorkspaceImageRequest) GetWorkdir() string { + if x != nil { + return x.Workdir + } + return "" +} + +func (x *FsReadWorkspaceImageRequest) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +type FsReadWorkspaceImageResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` + MimeType string `protobuf:"bytes,2,opt,name=mime_type,json=mimeType,proto3" json:"mime_type,omitempty"` + Data string `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"` + SizeBytes uint64 `protobuf:"varint,4,opt,name=size_bytes,json=sizeBytes,proto3" json:"size_bytes,omitempty"` + MtimeMs uint64 `protobuf:"varint,5,opt,name=mtime_ms,json=mtimeMs,proto3" json:"mtime_ms,omitempty"` + ContentHash string `protobuf:"bytes,6,opt,name=content_hash,json=contentHash,proto3" json:"content_hash,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FsReadWorkspaceImageResponse) Reset() { + *x = FsReadWorkspaceImageResponse{} + mi := &file_proto_v1_gateway_proto_msgTypes[82] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FsReadWorkspaceImageResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FsReadWorkspaceImageResponse) ProtoMessage() {} + +func (x *FsReadWorkspaceImageResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_v1_gateway_proto_msgTypes[82] + 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 FsReadWorkspaceImageResponse.ProtoReflect.Descriptor instead. +func (*FsReadWorkspaceImageResponse) Descriptor() ([]byte, []int) { + return file_proto_v1_gateway_proto_rawDescGZIP(), []int{82} +} + +func (x *FsReadWorkspaceImageResponse) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *FsReadWorkspaceImageResponse) GetMimeType() string { + if x != nil { + return x.MimeType + } + return "" +} + +func (x *FsReadWorkspaceImageResponse) GetData() string { + if x != nil { + return x.Data + } + return "" +} + +func (x *FsReadWorkspaceImageResponse) GetSizeBytes() uint64 { + if x != nil { + return x.SizeBytes + } + return 0 +} + +func (x *FsReadWorkspaceImageResponse) GetMtimeMs() uint64 { + if x != nil { + return x.MtimeMs + } + return 0 +} + +func (x *FsReadWorkspaceImageResponse) GetContentHash() string { + if x != nil { + return x.ContentHash + } + return "" +} + type FsWriteTextRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Workdir string `protobuf:"bytes,1,opt,name=workdir,proto3" json:"workdir,omitempty"` @@ -6077,7 +6245,7 @@ type FsWriteTextRequest struct { func (x *FsWriteTextRequest) Reset() { *x = FsWriteTextRequest{} - mi := &file_proto_v1_gateway_proto_msgTypes[81] + mi := &file_proto_v1_gateway_proto_msgTypes[83] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6089,7 +6257,7 @@ func (x *FsWriteTextRequest) String() string { func (*FsWriteTextRequest) ProtoMessage() {} func (x *FsWriteTextRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v1_gateway_proto_msgTypes[81] + mi := &file_proto_v1_gateway_proto_msgTypes[83] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6102,7 +6270,7 @@ func (x *FsWriteTextRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FsWriteTextRequest.ProtoReflect.Descriptor instead. func (*FsWriteTextRequest) Descriptor() ([]byte, []int) { - return file_proto_v1_gateway_proto_rawDescGZIP(), []int{81} + return file_proto_v1_gateway_proto_rawDescGZIP(), []int{83} } func (x *FsWriteTextRequest) GetWorkdir() string { @@ -6176,7 +6344,7 @@ type FsWriteTextResponse struct { func (x *FsWriteTextResponse) Reset() { *x = FsWriteTextResponse{} - mi := &file_proto_v1_gateway_proto_msgTypes[82] + mi := &file_proto_v1_gateway_proto_msgTypes[84] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6188,7 +6356,7 @@ func (x *FsWriteTextResponse) String() string { func (*FsWriteTextResponse) ProtoMessage() {} func (x *FsWriteTextResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v1_gateway_proto_msgTypes[82] + mi := &file_proto_v1_gateway_proto_msgTypes[84] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6201,7 +6369,7 @@ func (x *FsWriteTextResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use FsWriteTextResponse.ProtoReflect.Descriptor instead. func (*FsWriteTextResponse) Descriptor() ([]byte, []int) { - return file_proto_v1_gateway_proto_rawDescGZIP(), []int{82} + return file_proto_v1_gateway_proto_rawDescGZIP(), []int{84} } func (x *FsWriteTextResponse) GetPath() string { @@ -6263,7 +6431,7 @@ type FsCreateDirRequest struct { func (x *FsCreateDirRequest) Reset() { *x = FsCreateDirRequest{} - mi := &file_proto_v1_gateway_proto_msgTypes[83] + mi := &file_proto_v1_gateway_proto_msgTypes[85] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6275,7 +6443,7 @@ func (x *FsCreateDirRequest) String() string { func (*FsCreateDirRequest) ProtoMessage() {} func (x *FsCreateDirRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v1_gateway_proto_msgTypes[83] + mi := &file_proto_v1_gateway_proto_msgTypes[85] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6288,7 +6456,7 @@ func (x *FsCreateDirRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FsCreateDirRequest.ProtoReflect.Descriptor instead. func (*FsCreateDirRequest) Descriptor() ([]byte, []int) { - return file_proto_v1_gateway_proto_rawDescGZIP(), []int{83} + return file_proto_v1_gateway_proto_rawDescGZIP(), []int{85} } func (x *FsCreateDirRequest) GetWorkdir() string { @@ -6315,7 +6483,7 @@ type FsCreateDirResponse struct { func (x *FsCreateDirResponse) Reset() { *x = FsCreateDirResponse{} - mi := &file_proto_v1_gateway_proto_msgTypes[84] + mi := &file_proto_v1_gateway_proto_msgTypes[86] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6327,7 +6495,7 @@ func (x *FsCreateDirResponse) String() string { func (*FsCreateDirResponse) ProtoMessage() {} func (x *FsCreateDirResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v1_gateway_proto_msgTypes[84] + mi := &file_proto_v1_gateway_proto_msgTypes[86] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6340,7 +6508,7 @@ func (x *FsCreateDirResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use FsCreateDirResponse.ProtoReflect.Descriptor instead. func (*FsCreateDirResponse) Descriptor() ([]byte, []int) { - return file_proto_v1_gateway_proto_rawDescGZIP(), []int{84} + return file_proto_v1_gateway_proto_rawDescGZIP(), []int{86} } func (x *FsCreateDirResponse) GetPath() string { @@ -6368,7 +6536,7 @@ type FsRenameRequest struct { func (x *FsRenameRequest) Reset() { *x = FsRenameRequest{} - mi := &file_proto_v1_gateway_proto_msgTypes[85] + mi := &file_proto_v1_gateway_proto_msgTypes[87] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6380,7 +6548,7 @@ func (x *FsRenameRequest) String() string { func (*FsRenameRequest) ProtoMessage() {} func (x *FsRenameRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v1_gateway_proto_msgTypes[85] + mi := &file_proto_v1_gateway_proto_msgTypes[87] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6393,7 +6561,7 @@ func (x *FsRenameRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FsRenameRequest.ProtoReflect.Descriptor instead. func (*FsRenameRequest) Descriptor() ([]byte, []int) { - return file_proto_v1_gateway_proto_rawDescGZIP(), []int{85} + return file_proto_v1_gateway_proto_rawDescGZIP(), []int{87} } func (x *FsRenameRequest) GetWorkdir() string { @@ -6428,7 +6596,7 @@ type FsRenameResponse struct { func (x *FsRenameResponse) Reset() { *x = FsRenameResponse{} - mi := &file_proto_v1_gateway_proto_msgTypes[86] + mi := &file_proto_v1_gateway_proto_msgTypes[88] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6440,7 +6608,7 @@ func (x *FsRenameResponse) String() string { func (*FsRenameResponse) ProtoMessage() {} func (x *FsRenameResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v1_gateway_proto_msgTypes[86] + mi := &file_proto_v1_gateway_proto_msgTypes[88] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6453,7 +6621,7 @@ func (x *FsRenameResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use FsRenameResponse.ProtoReflect.Descriptor instead. func (*FsRenameResponse) Descriptor() ([]byte, []int) { - return file_proto_v1_gateway_proto_rawDescGZIP(), []int{86} + return file_proto_v1_gateway_proto_rawDescGZIP(), []int{88} } func (x *FsRenameResponse) GetFromPath() string { @@ -6487,7 +6655,7 @@ type FsDeleteRequest struct { func (x *FsDeleteRequest) Reset() { *x = FsDeleteRequest{} - mi := &file_proto_v1_gateway_proto_msgTypes[87] + mi := &file_proto_v1_gateway_proto_msgTypes[89] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6499,7 +6667,7 @@ func (x *FsDeleteRequest) String() string { func (*FsDeleteRequest) ProtoMessage() {} func (x *FsDeleteRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v1_gateway_proto_msgTypes[87] + mi := &file_proto_v1_gateway_proto_msgTypes[89] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6512,7 +6680,7 @@ func (x *FsDeleteRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FsDeleteRequest.ProtoReflect.Descriptor instead. func (*FsDeleteRequest) Descriptor() ([]byte, []int) { - return file_proto_v1_gateway_proto_rawDescGZIP(), []int{87} + return file_proto_v1_gateway_proto_rawDescGZIP(), []int{89} } func (x *FsDeleteRequest) GetWorkdir() string { @@ -6539,7 +6707,7 @@ type FsDeleteResponse struct { func (x *FsDeleteResponse) Reset() { *x = FsDeleteResponse{} - mi := &file_proto_v1_gateway_proto_msgTypes[88] + mi := &file_proto_v1_gateway_proto_msgTypes[90] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6551,7 +6719,7 @@ func (x *FsDeleteResponse) String() string { func (*FsDeleteResponse) ProtoMessage() {} func (x *FsDeleteResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v1_gateway_proto_msgTypes[88] + mi := &file_proto_v1_gateway_proto_msgTypes[90] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6564,7 +6732,7 @@ func (x *FsDeleteResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use FsDeleteResponse.ProtoReflect.Descriptor instead. func (*FsDeleteResponse) Descriptor() ([]byte, []int) { - return file_proto_v1_gateway_proto_rawDescGZIP(), []int{88} + return file_proto_v1_gateway_proto_rawDescGZIP(), []int{90} } func (x *FsDeleteResponse) GetPath() string { @@ -6590,7 +6758,7 @@ type PingRequest struct { func (x *PingRequest) Reset() { *x = PingRequest{} - mi := &file_proto_v1_gateway_proto_msgTypes[89] + mi := &file_proto_v1_gateway_proto_msgTypes[91] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6602,7 +6770,7 @@ func (x *PingRequest) String() string { func (*PingRequest) ProtoMessage() {} func (x *PingRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v1_gateway_proto_msgTypes[89] + mi := &file_proto_v1_gateway_proto_msgTypes[91] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6615,7 +6783,7 @@ func (x *PingRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PingRequest.ProtoReflect.Descriptor instead. func (*PingRequest) Descriptor() ([]byte, []int) { - return file_proto_v1_gateway_proto_rawDescGZIP(), []int{89} + return file_proto_v1_gateway_proto_rawDescGZIP(), []int{91} } func (x *PingRequest) GetTimestamp() int64 { @@ -6634,7 +6802,7 @@ type PongResponse struct { func (x *PongResponse) Reset() { *x = PongResponse{} - mi := &file_proto_v1_gateway_proto_msgTypes[90] + mi := &file_proto_v1_gateway_proto_msgTypes[92] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6646,7 +6814,7 @@ func (x *PongResponse) String() string { func (*PongResponse) ProtoMessage() {} func (x *PongResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v1_gateway_proto_msgTypes[90] + mi := &file_proto_v1_gateway_proto_msgTypes[92] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6659,7 +6827,7 @@ func (x *PongResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PongResponse.ProtoReflect.Descriptor instead. func (*PongResponse) Descriptor() ([]byte, []int) { - return file_proto_v1_gateway_proto_rawDescGZIP(), []int{90} + return file_proto_v1_gateway_proto_rawDescGZIP(), []int{92} } func (x *PongResponse) GetTimestamp() int64 { @@ -6679,7 +6847,7 @@ type ErrorResponse struct { func (x *ErrorResponse) Reset() { *x = ErrorResponse{} - mi := &file_proto_v1_gateway_proto_msgTypes[91] + mi := &file_proto_v1_gateway_proto_msgTypes[93] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6691,7 +6859,7 @@ func (x *ErrorResponse) String() string { func (*ErrorResponse) ProtoMessage() {} func (x *ErrorResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v1_gateway_proto_msgTypes[91] + mi := &file_proto_v1_gateway_proto_msgTypes[93] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6704,7 +6872,7 @@ func (x *ErrorResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ErrorResponse.ProtoReflect.Descriptor instead. func (*ErrorResponse) Descriptor() ([]byte, []int) { - return file_proto_v1_gateway_proto_rawDescGZIP(), []int{91} + return file_proto_v1_gateway_proto_rawDescGZIP(), []int{93} } func (x *ErrorResponse) GetCode() int32 { @@ -6734,7 +6902,7 @@ const file_proto_v1_gateway_proto_rawDesc = "" + "\asuccess\x18\x01 \x01(\bR\asuccess\x12\x18\n" + "\amessage\x18\x02 \x01(\tR\amessage\x12\x1d\n" + "\n" + - "session_id\x18\x03 \x01(\tR\tsessionId\"\x9d\x18\n" + + "session_id\x18\x03 \x01(\tR\tsessionId\"\x89\x19\n" + "\x0fGatewayEnvelope\x12\x1d\n" + "\n" + "request_id\x18\x01 \x01(\tR\trequestId\x12\x1c\n" + @@ -6781,8 +6949,9 @@ const file_proto_v1_gateway_proto_rawDesc = "" + "\tfs_delete\x18< \x01(\v2%.liveagent.gateway.v1.FsDeleteRequestH\x00R\bfsDelete\x12C\n" + "\vgit_request\x18= \x01(\v2 .liveagent.gateway.v1.GitRequestH\x00R\n" + "gitRequest\x12d\n" + - "\x15fs_read_editable_text\x18> \x01(\v2/.liveagent.gateway.v1.FsReadEditableTextRequestH\x00R\x12fsReadEditableTextB\t\n" + - "\apayload\"\xae\x1c\n" + + "\x15fs_read_editable_text\x18> \x01(\v2/.liveagent.gateway.v1.FsReadEditableTextRequestH\x00R\x12fsReadEditableText\x12j\n" + + "\x17fs_read_workspace_image\x18? \x01(\v21.liveagent.gateway.v1.FsReadWorkspaceImageRequestH\x00R\x14fsReadWorkspaceImageB\t\n" + + "\apayload\"\xa4\x1d\n" + "\rAgentEnvelope\x12\x1d\n" + "\n" + "request_id\x18\x01 \x01(\tR\trequestId\x12\x1c\n" + @@ -6827,7 +6996,8 @@ const file_proto_v1_gateway_proto_rawDesc = "" + "\x0efs_rename_resp\x18> \x01(\v2&.liveagent.gateway.v1.FsRenameResponseH\x00R\ffsRenameResp\x12N\n" + "\x0efs_delete_resp\x18? \x01(\v2&.liveagent.gateway.v1.FsDeleteResponseH\x00R\ffsDeleteResp\x12F\n" + "\fgit_response\x18@ \x01(\v2!.liveagent.gateway.v1.GitResponseH\x00R\vgitResponse\x12n\n" + - "\x1afs_read_editable_text_resp\x18A \x01(\v20.liveagent.gateway.v1.FsReadEditableTextResponseH\x00R\x16fsReadEditableTextResp\x12;\n" + + "\x1afs_read_editable_text_resp\x18A \x01(\v20.liveagent.gateway.v1.FsReadEditableTextResponseH\x00R\x16fsReadEditableTextResp\x12t\n" + + "\x1cfs_read_workspace_image_resp\x18B \x01(\v22.liveagent.gateway.v1.FsReadWorkspaceImageResponseH\x00R\x18fsReadWorkspaceImageResp\x12;\n" + "\x05error\x18c \x01(\v2#.liveagent.gateway.v1.ErrorResponseH\x00R\x05errorB\t\n" + "\apayload\"|\n" + "\x11ChatSelectedModel\x12,\n" + @@ -7164,7 +7334,18 @@ const file_proto_v1_gateway_proto_rawDesc = "" + "\n" + "size_bytes\x18\x05 \x01(\x04R\tsizeBytes\x12\x1f\n" + "\vtotal_lines\x18\x06 \x01(\x04R\n" + - "totalLines\"\xbe\x02\n" + + "totalLines\"K\n" + + "\x1bFsReadWorkspaceImageRequest\x12\x18\n" + + "\aworkdir\x18\x01 \x01(\tR\aworkdir\x12\x12\n" + + "\x04path\x18\x02 \x01(\tR\x04path\"\xc0\x01\n" + + "\x1cFsReadWorkspaceImageResponse\x12\x12\n" + + "\x04path\x18\x01 \x01(\tR\x04path\x12\x1b\n" + + "\tmime_type\x18\x02 \x01(\tR\bmimeType\x12\x12\n" + + "\x04data\x18\x03 \x01(\tR\x04data\x12\x1d\n" + + "\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" + "\x12FsWriteTextRequest\x12\x18\n" + "\aworkdir\x18\x01 \x01(\tR\aworkdir\x12\x12\n" + "\x04path\x18\x02 \x01(\tR\x04path\x12\x18\n" + @@ -7227,7 +7408,7 @@ func file_proto_v1_gateway_proto_rawDescGZIP() []byte { } var file_proto_v1_gateway_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_proto_v1_gateway_proto_msgTypes = make([]protoimpl.MessageInfo, 92) +var file_proto_v1_gateway_proto_msgTypes = make([]protoimpl.MessageInfo, 94) var file_proto_v1_gateway_proto_goTypes = []any{ (ChatEvent_ChatEventType)(0), // 0: liveagent.gateway.v1.ChatEvent.ChatEventType (*AuthRequest)(nil), // 1: liveagent.gateway.v1.AuthRequest @@ -7311,17 +7492,19 @@ var file_proto_v1_gateway_proto_goTypes = []any{ (*FsListResponse)(nil), // 79: liveagent.gateway.v1.FsListResponse (*FsReadEditableTextRequest)(nil), // 80: liveagent.gateway.v1.FsReadEditableTextRequest (*FsReadEditableTextResponse)(nil), // 81: liveagent.gateway.v1.FsReadEditableTextResponse - (*FsWriteTextRequest)(nil), // 82: liveagent.gateway.v1.FsWriteTextRequest - (*FsWriteTextResponse)(nil), // 83: liveagent.gateway.v1.FsWriteTextResponse - (*FsCreateDirRequest)(nil), // 84: liveagent.gateway.v1.FsCreateDirRequest - (*FsCreateDirResponse)(nil), // 85: liveagent.gateway.v1.FsCreateDirResponse - (*FsRenameRequest)(nil), // 86: liveagent.gateway.v1.FsRenameRequest - (*FsRenameResponse)(nil), // 87: liveagent.gateway.v1.FsRenameResponse - (*FsDeleteRequest)(nil), // 88: liveagent.gateway.v1.FsDeleteRequest - (*FsDeleteResponse)(nil), // 89: liveagent.gateway.v1.FsDeleteResponse - (*PingRequest)(nil), // 90: liveagent.gateway.v1.PingRequest - (*PongResponse)(nil), // 91: liveagent.gateway.v1.PongResponse - (*ErrorResponse)(nil), // 92: liveagent.gateway.v1.ErrorResponse + (*FsReadWorkspaceImageRequest)(nil), // 82: liveagent.gateway.v1.FsReadWorkspaceImageRequest + (*FsReadWorkspaceImageResponse)(nil), // 83: liveagent.gateway.v1.FsReadWorkspaceImageResponse + (*FsWriteTextRequest)(nil), // 84: liveagent.gateway.v1.FsWriteTextRequest + (*FsWriteTextResponse)(nil), // 85: liveagent.gateway.v1.FsWriteTextResponse + (*FsCreateDirRequest)(nil), // 86: liveagent.gateway.v1.FsCreateDirRequest + (*FsCreateDirResponse)(nil), // 87: liveagent.gateway.v1.FsCreateDirResponse + (*FsRenameRequest)(nil), // 88: liveagent.gateway.v1.FsRenameRequest + (*FsRenameResponse)(nil), // 89: liveagent.gateway.v1.FsRenameResponse + (*FsDeleteRequest)(nil), // 90: liveagent.gateway.v1.FsDeleteRequest + (*FsDeleteResponse)(nil), // 91: liveagent.gateway.v1.FsDeleteResponse + (*PingRequest)(nil), // 92: liveagent.gateway.v1.PingRequest + (*PongResponse)(nil), // 93: liveagent.gateway.v1.PongResponse + (*ErrorResponse)(nil), // 94: liveagent.gateway.v1.ErrorResponse } var file_proto_v1_gateway_proto_depIdxs = []int32{ 22, // 0: liveagent.gateway.v1.GatewayEnvelope.chat_request:type_name -> liveagent.gateway.v1.ChatRequest @@ -7347,91 +7530,93 @@ var file_proto_v1_gateway_proto_depIdxs = []int32{ 9, // 20: liveagent.gateway.v1.GatewayEnvelope.upload_readable_files:type_name -> liveagent.gateway.v1.UploadReadableFilesRequest 70, // 21: liveagent.gateway.v1.GatewayEnvelope.fs_roots:type_name -> liveagent.gateway.v1.FsRootsRequest 72, // 22: liveagent.gateway.v1.GatewayEnvelope.fs_list_dirs:type_name -> liveagent.gateway.v1.FsListDirsRequest - 90, // 23: liveagent.gateway.v1.GatewayEnvelope.ping:type_name -> liveagent.gateway.v1.PingRequest + 92, // 23: liveagent.gateway.v1.GatewayEnvelope.ping:type_name -> liveagent.gateway.v1.PingRequest 11, // 24: liveagent.gateway.v1.GatewayEnvelope.uploaded_image_preview:type_name -> liveagent.gateway.v1.UploadedImagePreviewRequest 13, // 25: liveagent.gateway.v1.GatewayEnvelope.memory_manage:type_name -> liveagent.gateway.v1.MemoryManageRequest 64, // 26: liveagent.gateway.v1.GatewayEnvelope.skill_manage:type_name -> liveagent.gateway.v1.SkillManageRequest 75, // 27: liveagent.gateway.v1.GatewayEnvelope.fs_create_project_folder:type_name -> liveagent.gateway.v1.FsCreateProjectFolderRequest 15, // 28: liveagent.gateway.v1.GatewayEnvelope.terminal_request:type_name -> liveagent.gateway.v1.TerminalRequest 77, // 29: liveagent.gateway.v1.GatewayEnvelope.fs_list:type_name -> liveagent.gateway.v1.FsListRequest - 82, // 30: liveagent.gateway.v1.GatewayEnvelope.fs_write_text:type_name -> liveagent.gateway.v1.FsWriteTextRequest - 84, // 31: liveagent.gateway.v1.GatewayEnvelope.fs_create_dir:type_name -> liveagent.gateway.v1.FsCreateDirRequest - 86, // 32: liveagent.gateway.v1.GatewayEnvelope.fs_rename:type_name -> liveagent.gateway.v1.FsRenameRequest - 88, // 33: liveagent.gateway.v1.GatewayEnvelope.fs_delete:type_name -> liveagent.gateway.v1.FsDeleteRequest + 84, // 30: liveagent.gateway.v1.GatewayEnvelope.fs_write_text:type_name -> liveagent.gateway.v1.FsWriteTextRequest + 86, // 31: liveagent.gateway.v1.GatewayEnvelope.fs_create_dir:type_name -> liveagent.gateway.v1.FsCreateDirRequest + 88, // 32: liveagent.gateway.v1.GatewayEnvelope.fs_rename:type_name -> liveagent.gateway.v1.FsRenameRequest + 90, // 33: liveagent.gateway.v1.GatewayEnvelope.fs_delete:type_name -> liveagent.gateway.v1.FsDeleteRequest 20, // 34: liveagent.gateway.v1.GatewayEnvelope.git_request:type_name -> liveagent.gateway.v1.GitRequest 80, // 35: liveagent.gateway.v1.GatewayEnvelope.fs_read_editable_text:type_name -> liveagent.gateway.v1.FsReadEditableTextRequest - 24, // 36: liveagent.gateway.v1.AgentEnvelope.chat_event:type_name -> liveagent.gateway.v1.ChatEvent - 26, // 37: liveagent.gateway.v1.AgentEnvelope.cron_manage_resp:type_name -> liveagent.gateway.v1.CronManageResponse - 28, // 38: liveagent.gateway.v1.AgentEnvelope.history_list_resp:type_name -> liveagent.gateway.v1.HistoryListResponse - 31, // 39: liveagent.gateway.v1.AgentEnvelope.history_get_resp:type_name -> liveagent.gateway.v1.HistoryGetResponse - 33, // 40: liveagent.gateway.v1.AgentEnvelope.history_rename_resp:type_name -> liveagent.gateway.v1.HistoryRenameResponse - 47, // 41: liveagent.gateway.v1.AgentEnvelope.history_delete_resp:type_name -> liveagent.gateway.v1.HistoryDeleteResponse - 50, // 42: liveagent.gateway.v1.AgentEnvelope.history_sync:type_name -> liveagent.gateway.v1.HistorySyncEvent - 49, // 43: liveagent.gateway.v1.AgentEnvelope.history_truncate_resp:type_name -> liveagent.gateway.v1.HistoryTruncateResponse - 35, // 44: liveagent.gateway.v1.AgentEnvelope.history_pin_resp:type_name -> liveagent.gateway.v1.HistoryPinResponse - 38, // 45: liveagent.gateway.v1.AgentEnvelope.history_share_get_resp:type_name -> liveagent.gateway.v1.HistoryShareGetResponse - 40, // 46: liveagent.gateway.v1.AgentEnvelope.history_share_set_resp:type_name -> liveagent.gateway.v1.HistoryShareSetResponse - 42, // 47: liveagent.gateway.v1.AgentEnvelope.history_share_resolve_resp:type_name -> liveagent.gateway.v1.HistoryShareResolveResponse - 45, // 48: liveagent.gateway.v1.AgentEnvelope.history_workdirs_resp:type_name -> liveagent.gateway.v1.HistoryWorkdirsResponse - 52, // 49: liveagent.gateway.v1.AgentEnvelope.provider_list_resp:type_name -> liveagent.gateway.v1.ProviderListResponse - 54, // 50: liveagent.gateway.v1.AgentEnvelope.settings_get_resp:type_name -> liveagent.gateway.v1.SettingsGetResponse - 56, // 51: liveagent.gateway.v1.AgentEnvelope.settings_update_resp:type_name -> liveagent.gateway.v1.SettingsUpdateResponse - 57, // 52: liveagent.gateway.v1.AgentEnvelope.settings_sync:type_name -> liveagent.gateway.v1.SettingsSyncEvent - 59, // 53: liveagent.gateway.v1.AgentEnvelope.skill_files_list_resp:type_name -> liveagent.gateway.v1.SkillFilesListResponse - 61, // 54: liveagent.gateway.v1.AgentEnvelope.skill_metadata_read_resp:type_name -> liveagent.gateway.v1.SkillMetadataReadResponse - 63, // 55: liveagent.gateway.v1.AgentEnvelope.skill_text_read_resp:type_name -> liveagent.gateway.v1.SkillTextReadResponse - 68, // 56: liveagent.gateway.v1.AgentEnvelope.file_mention_list_resp:type_name -> liveagent.gateway.v1.FileMentionListResponse - 10, // 57: liveagent.gateway.v1.AgentEnvelope.upload_readable_files_resp:type_name -> liveagent.gateway.v1.UploadReadableFilesResponse - 71, // 58: liveagent.gateway.v1.AgentEnvelope.fs_roots_resp:type_name -> liveagent.gateway.v1.FsRootsResponse - 91, // 59: liveagent.gateway.v1.AgentEnvelope.pong:type_name -> liveagent.gateway.v1.PongResponse - 74, // 60: liveagent.gateway.v1.AgentEnvelope.fs_list_dirs_resp:type_name -> liveagent.gateway.v1.FsListDirsResponse - 12, // 61: liveagent.gateway.v1.AgentEnvelope.uploaded_image_preview_resp:type_name -> liveagent.gateway.v1.UploadedImagePreviewResponse - 14, // 62: liveagent.gateway.v1.AgentEnvelope.memory_manage_resp:type_name -> liveagent.gateway.v1.MemoryManageResponse - 65, // 63: liveagent.gateway.v1.AgentEnvelope.skill_manage_resp:type_name -> liveagent.gateway.v1.SkillManageResponse - 76, // 64: liveagent.gateway.v1.AgentEnvelope.fs_create_project_folder_resp:type_name -> liveagent.gateway.v1.FsCreateProjectFolderResponse - 18, // 65: liveagent.gateway.v1.AgentEnvelope.terminal_response:type_name -> liveagent.gateway.v1.TerminalResponse - 19, // 66: liveagent.gateway.v1.AgentEnvelope.terminal_event:type_name -> liveagent.gateway.v1.TerminalEvent - 79, // 67: liveagent.gateway.v1.AgentEnvelope.fs_list_resp:type_name -> liveagent.gateway.v1.FsListResponse - 83, // 68: liveagent.gateway.v1.AgentEnvelope.fs_write_text_resp:type_name -> liveagent.gateway.v1.FsWriteTextResponse - 85, // 69: liveagent.gateway.v1.AgentEnvelope.fs_create_dir_resp:type_name -> liveagent.gateway.v1.FsCreateDirResponse - 87, // 70: liveagent.gateway.v1.AgentEnvelope.fs_rename_resp:type_name -> liveagent.gateway.v1.FsRenameResponse - 89, // 71: liveagent.gateway.v1.AgentEnvelope.fs_delete_resp:type_name -> liveagent.gateway.v1.FsDeleteResponse - 21, // 72: liveagent.gateway.v1.AgentEnvelope.git_response:type_name -> liveagent.gateway.v1.GitResponse - 81, // 73: liveagent.gateway.v1.AgentEnvelope.fs_read_editable_text_resp:type_name -> liveagent.gateway.v1.FsReadEditableTextResponse - 92, // 74: liveagent.gateway.v1.AgentEnvelope.error:type_name -> liveagent.gateway.v1.ErrorResponse - 8, // 75: liveagent.gateway.v1.UploadReadableFilesRequest.files:type_name -> liveagent.gateway.v1.UploadReadableFile - 7, // 76: liveagent.gateway.v1.UploadReadableFilesResponse.files:type_name -> liveagent.gateway.v1.ChatUploadedFile - 16, // 77: liveagent.gateway.v1.TerminalResponse.sessions:type_name -> liveagent.gateway.v1.TerminalSession - 16, // 78: liveagent.gateway.v1.TerminalResponse.session:type_name -> liveagent.gateway.v1.TerminalSession - 17, // 79: liveagent.gateway.v1.TerminalResponse.shell_options:type_name -> liveagent.gateway.v1.TerminalShellOption - 16, // 80: liveagent.gateway.v1.TerminalEvent.session:type_name -> liveagent.gateway.v1.TerminalSession - 5, // 81: liveagent.gateway.v1.ChatRequest.selected_model:type_name -> liveagent.gateway.v1.ChatSelectedModel - 7, // 82: liveagent.gateway.v1.ChatRequest.uploaded_files:type_name -> liveagent.gateway.v1.ChatUploadedFile - 6, // 83: liveagent.gateway.v1.ChatRequest.runtime_controls:type_name -> liveagent.gateway.v1.ChatRuntimeControls - 0, // 84: liveagent.gateway.v1.ChatEvent.type:type_name -> liveagent.gateway.v1.ChatEvent.ChatEventType - 29, // 85: liveagent.gateway.v1.HistoryListResponse.conversations:type_name -> liveagent.gateway.v1.ConversationSummary - 29, // 86: liveagent.gateway.v1.HistoryGetResponse.conversation:type_name -> liveagent.gateway.v1.ConversationSummary - 29, // 87: liveagent.gateway.v1.HistoryRenameResponse.conversation:type_name -> liveagent.gateway.v1.ConversationSummary - 29, // 88: liveagent.gateway.v1.HistoryPinResponse.conversation:type_name -> liveagent.gateway.v1.ConversationSummary - 36, // 89: liveagent.gateway.v1.HistoryShareGetResponse.share:type_name -> liveagent.gateway.v1.HistoryShareStatus - 36, // 90: liveagent.gateway.v1.HistoryShareSetResponse.share:type_name -> liveagent.gateway.v1.HistoryShareStatus - 29, // 91: liveagent.gateway.v1.HistoryShareResolveResponse.conversation:type_name -> liveagent.gateway.v1.ConversationSummary - 44, // 92: liveagent.gateway.v1.HistoryWorkdirsResponse.workdirs:type_name -> liveagent.gateway.v1.HistoryWorkdirSummary - 29, // 93: liveagent.gateway.v1.HistoryTruncateResponse.conversation:type_name -> liveagent.gateway.v1.ConversationSummary - 29, // 94: liveagent.gateway.v1.HistorySyncEvent.conversation:type_name -> liveagent.gateway.v1.ConversationSummary - 67, // 95: liveagent.gateway.v1.FileMentionListResponse.entries:type_name -> liveagent.gateway.v1.FileMentionEntry - 69, // 96: liveagent.gateway.v1.FsRootsResponse.roots:type_name -> liveagent.gateway.v1.FsRoot - 73, // 97: liveagent.gateway.v1.FsListDirsResponse.entries:type_name -> liveagent.gateway.v1.FsDirEntry - 78, // 98: liveagent.gateway.v1.FsListResponse.entries:type_name -> liveagent.gateway.v1.FsListEntry - 4, // 99: liveagent.gateway.v1.AgentGateway.AgentConnect:input_type -> liveagent.gateway.v1.AgentEnvelope - 1, // 100: liveagent.gateway.v1.AgentGateway.Authenticate:input_type -> liveagent.gateway.v1.AuthRequest - 3, // 101: liveagent.gateway.v1.AgentGateway.AgentConnect:output_type -> liveagent.gateway.v1.GatewayEnvelope - 2, // 102: liveagent.gateway.v1.AgentGateway.Authenticate:output_type -> liveagent.gateway.v1.AuthResponse - 101, // [101:103] is the sub-list for method output_type - 99, // [99:101] is the sub-list for method input_type - 99, // [99:99] is the sub-list for extension type_name - 99, // [99:99] is the sub-list for extension extendee - 0, // [0:99] is the sub-list for field type_name + 82, // 36: liveagent.gateway.v1.GatewayEnvelope.fs_read_workspace_image:type_name -> liveagent.gateway.v1.FsReadWorkspaceImageRequest + 24, // 37: liveagent.gateway.v1.AgentEnvelope.chat_event:type_name -> liveagent.gateway.v1.ChatEvent + 26, // 38: liveagent.gateway.v1.AgentEnvelope.cron_manage_resp:type_name -> liveagent.gateway.v1.CronManageResponse + 28, // 39: liveagent.gateway.v1.AgentEnvelope.history_list_resp:type_name -> liveagent.gateway.v1.HistoryListResponse + 31, // 40: liveagent.gateway.v1.AgentEnvelope.history_get_resp:type_name -> liveagent.gateway.v1.HistoryGetResponse + 33, // 41: liveagent.gateway.v1.AgentEnvelope.history_rename_resp:type_name -> liveagent.gateway.v1.HistoryRenameResponse + 47, // 42: liveagent.gateway.v1.AgentEnvelope.history_delete_resp:type_name -> liveagent.gateway.v1.HistoryDeleteResponse + 50, // 43: liveagent.gateway.v1.AgentEnvelope.history_sync:type_name -> liveagent.gateway.v1.HistorySyncEvent + 49, // 44: liveagent.gateway.v1.AgentEnvelope.history_truncate_resp:type_name -> liveagent.gateway.v1.HistoryTruncateResponse + 35, // 45: liveagent.gateway.v1.AgentEnvelope.history_pin_resp:type_name -> liveagent.gateway.v1.HistoryPinResponse + 38, // 46: liveagent.gateway.v1.AgentEnvelope.history_share_get_resp:type_name -> liveagent.gateway.v1.HistoryShareGetResponse + 40, // 47: liveagent.gateway.v1.AgentEnvelope.history_share_set_resp:type_name -> liveagent.gateway.v1.HistoryShareSetResponse + 42, // 48: liveagent.gateway.v1.AgentEnvelope.history_share_resolve_resp:type_name -> liveagent.gateway.v1.HistoryShareResolveResponse + 45, // 49: liveagent.gateway.v1.AgentEnvelope.history_workdirs_resp:type_name -> liveagent.gateway.v1.HistoryWorkdirsResponse + 52, // 50: liveagent.gateway.v1.AgentEnvelope.provider_list_resp:type_name -> liveagent.gateway.v1.ProviderListResponse + 54, // 51: liveagent.gateway.v1.AgentEnvelope.settings_get_resp:type_name -> liveagent.gateway.v1.SettingsGetResponse + 56, // 52: liveagent.gateway.v1.AgentEnvelope.settings_update_resp:type_name -> liveagent.gateway.v1.SettingsUpdateResponse + 57, // 53: liveagent.gateway.v1.AgentEnvelope.settings_sync:type_name -> liveagent.gateway.v1.SettingsSyncEvent + 59, // 54: liveagent.gateway.v1.AgentEnvelope.skill_files_list_resp:type_name -> liveagent.gateway.v1.SkillFilesListResponse + 61, // 55: liveagent.gateway.v1.AgentEnvelope.skill_metadata_read_resp:type_name -> liveagent.gateway.v1.SkillMetadataReadResponse + 63, // 56: liveagent.gateway.v1.AgentEnvelope.skill_text_read_resp:type_name -> liveagent.gateway.v1.SkillTextReadResponse + 68, // 57: liveagent.gateway.v1.AgentEnvelope.file_mention_list_resp:type_name -> liveagent.gateway.v1.FileMentionListResponse + 10, // 58: liveagent.gateway.v1.AgentEnvelope.upload_readable_files_resp:type_name -> liveagent.gateway.v1.UploadReadableFilesResponse + 71, // 59: liveagent.gateway.v1.AgentEnvelope.fs_roots_resp:type_name -> liveagent.gateway.v1.FsRootsResponse + 93, // 60: liveagent.gateway.v1.AgentEnvelope.pong:type_name -> liveagent.gateway.v1.PongResponse + 74, // 61: liveagent.gateway.v1.AgentEnvelope.fs_list_dirs_resp:type_name -> liveagent.gateway.v1.FsListDirsResponse + 12, // 62: liveagent.gateway.v1.AgentEnvelope.uploaded_image_preview_resp:type_name -> liveagent.gateway.v1.UploadedImagePreviewResponse + 14, // 63: liveagent.gateway.v1.AgentEnvelope.memory_manage_resp:type_name -> liveagent.gateway.v1.MemoryManageResponse + 65, // 64: liveagent.gateway.v1.AgentEnvelope.skill_manage_resp:type_name -> liveagent.gateway.v1.SkillManageResponse + 76, // 65: liveagent.gateway.v1.AgentEnvelope.fs_create_project_folder_resp:type_name -> liveagent.gateway.v1.FsCreateProjectFolderResponse + 18, // 66: liveagent.gateway.v1.AgentEnvelope.terminal_response:type_name -> liveagent.gateway.v1.TerminalResponse + 19, // 67: liveagent.gateway.v1.AgentEnvelope.terminal_event:type_name -> liveagent.gateway.v1.TerminalEvent + 79, // 68: liveagent.gateway.v1.AgentEnvelope.fs_list_resp:type_name -> liveagent.gateway.v1.FsListResponse + 85, // 69: liveagent.gateway.v1.AgentEnvelope.fs_write_text_resp:type_name -> liveagent.gateway.v1.FsWriteTextResponse + 87, // 70: liveagent.gateway.v1.AgentEnvelope.fs_create_dir_resp:type_name -> liveagent.gateway.v1.FsCreateDirResponse + 89, // 71: liveagent.gateway.v1.AgentEnvelope.fs_rename_resp:type_name -> liveagent.gateway.v1.FsRenameResponse + 91, // 72: liveagent.gateway.v1.AgentEnvelope.fs_delete_resp:type_name -> liveagent.gateway.v1.FsDeleteResponse + 21, // 73: liveagent.gateway.v1.AgentEnvelope.git_response:type_name -> liveagent.gateway.v1.GitResponse + 81, // 74: liveagent.gateway.v1.AgentEnvelope.fs_read_editable_text_resp:type_name -> liveagent.gateway.v1.FsReadEditableTextResponse + 83, // 75: liveagent.gateway.v1.AgentEnvelope.fs_read_workspace_image_resp:type_name -> liveagent.gateway.v1.FsReadWorkspaceImageResponse + 94, // 76: liveagent.gateway.v1.AgentEnvelope.error:type_name -> liveagent.gateway.v1.ErrorResponse + 8, // 77: liveagent.gateway.v1.UploadReadableFilesRequest.files:type_name -> liveagent.gateway.v1.UploadReadableFile + 7, // 78: liveagent.gateway.v1.UploadReadableFilesResponse.files:type_name -> liveagent.gateway.v1.ChatUploadedFile + 16, // 79: liveagent.gateway.v1.TerminalResponse.sessions:type_name -> liveagent.gateway.v1.TerminalSession + 16, // 80: liveagent.gateway.v1.TerminalResponse.session:type_name -> liveagent.gateway.v1.TerminalSession + 17, // 81: liveagent.gateway.v1.TerminalResponse.shell_options:type_name -> liveagent.gateway.v1.TerminalShellOption + 16, // 82: liveagent.gateway.v1.TerminalEvent.session:type_name -> liveagent.gateway.v1.TerminalSession + 5, // 83: liveagent.gateway.v1.ChatRequest.selected_model:type_name -> liveagent.gateway.v1.ChatSelectedModel + 7, // 84: liveagent.gateway.v1.ChatRequest.uploaded_files:type_name -> liveagent.gateway.v1.ChatUploadedFile + 6, // 85: liveagent.gateway.v1.ChatRequest.runtime_controls:type_name -> liveagent.gateway.v1.ChatRuntimeControls + 0, // 86: liveagent.gateway.v1.ChatEvent.type:type_name -> liveagent.gateway.v1.ChatEvent.ChatEventType + 29, // 87: liveagent.gateway.v1.HistoryListResponse.conversations:type_name -> liveagent.gateway.v1.ConversationSummary + 29, // 88: liveagent.gateway.v1.HistoryGetResponse.conversation:type_name -> liveagent.gateway.v1.ConversationSummary + 29, // 89: liveagent.gateway.v1.HistoryRenameResponse.conversation:type_name -> liveagent.gateway.v1.ConversationSummary + 29, // 90: liveagent.gateway.v1.HistoryPinResponse.conversation:type_name -> liveagent.gateway.v1.ConversationSummary + 36, // 91: liveagent.gateway.v1.HistoryShareGetResponse.share:type_name -> liveagent.gateway.v1.HistoryShareStatus + 36, // 92: liveagent.gateway.v1.HistoryShareSetResponse.share:type_name -> liveagent.gateway.v1.HistoryShareStatus + 29, // 93: liveagent.gateway.v1.HistoryShareResolveResponse.conversation:type_name -> liveagent.gateway.v1.ConversationSummary + 44, // 94: liveagent.gateway.v1.HistoryWorkdirsResponse.workdirs:type_name -> liveagent.gateway.v1.HistoryWorkdirSummary + 29, // 95: liveagent.gateway.v1.HistoryTruncateResponse.conversation:type_name -> liveagent.gateway.v1.ConversationSummary + 29, // 96: liveagent.gateway.v1.HistorySyncEvent.conversation:type_name -> liveagent.gateway.v1.ConversationSummary + 67, // 97: liveagent.gateway.v1.FileMentionListResponse.entries:type_name -> liveagent.gateway.v1.FileMentionEntry + 69, // 98: liveagent.gateway.v1.FsRootsResponse.roots:type_name -> liveagent.gateway.v1.FsRoot + 73, // 99: liveagent.gateway.v1.FsListDirsResponse.entries:type_name -> liveagent.gateway.v1.FsDirEntry + 78, // 100: liveagent.gateway.v1.FsListResponse.entries:type_name -> liveagent.gateway.v1.FsListEntry + 4, // 101: liveagent.gateway.v1.AgentGateway.AgentConnect:input_type -> liveagent.gateway.v1.AgentEnvelope + 1, // 102: liveagent.gateway.v1.AgentGateway.Authenticate:input_type -> liveagent.gateway.v1.AuthRequest + 3, // 103: liveagent.gateway.v1.AgentGateway.AgentConnect:output_type -> liveagent.gateway.v1.GatewayEnvelope + 2, // 104: liveagent.gateway.v1.AgentGateway.Authenticate:output_type -> liveagent.gateway.v1.AuthResponse + 103, // [103:105] is the sub-list for method output_type + 101, // [101:103] is the sub-list for method input_type + 101, // [101:101] is the sub-list for extension type_name + 101, // [101:101] is the sub-list for extension extendee + 0, // [0:101] is the sub-list for field type_name } func init() { file_proto_v1_gateway_proto_init() } @@ -7476,6 +7661,7 @@ func file_proto_v1_gateway_proto_init() { (*GatewayEnvelope_FsDelete)(nil), (*GatewayEnvelope_GitRequest)(nil), (*GatewayEnvelope_FsReadEditableText)(nil), + (*GatewayEnvelope_FsReadWorkspaceImage)(nil), } file_proto_v1_gateway_proto_msgTypes[3].OneofWrappers = []any{ (*AgentEnvelope_ChatEvent)(nil), @@ -7516,6 +7702,7 @@ func file_proto_v1_gateway_proto_init() { (*AgentEnvelope_FsDeleteResp)(nil), (*AgentEnvelope_GitResponse)(nil), (*AgentEnvelope_FsReadEditableTextResp)(nil), + (*AgentEnvelope_FsReadWorkspaceImageResp)(nil), (*AgentEnvelope_Error)(nil), } file_proto_v1_gateway_proto_msgTypes[38].OneofWrappers = []any{} @@ -7525,7 +7712,7 @@ func file_proto_v1_gateway_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_v1_gateway_proto_rawDesc), len(file_proto_v1_gateway_proto_rawDesc)), NumEnums: 1, - NumMessages: 92, + NumMessages: 94, NumExtensions: 0, NumServices: 1, }, diff --git a/crates/agent-gateway/internal/server/websocket_fs_handlers.go b/crates/agent-gateway/internal/server/websocket_fs_handlers.go index a481741b..de641564 100644 --- a/crates/agent-gateway/internal/server/websocket_fs_handlers.go +++ b/crates/agent-gateway/internal/server/websocket_fs_handlers.go @@ -288,6 +288,57 @@ func (c *websocketConnection) handleFsReadEditableText(req websocketRequest) { _ = c.writeResponse(req.ID, websocketFsReadEditableTextResponsePayload(resp)) } +func (c *websocketConnection) handleFsReadWorkspaceImage(req websocketRequest) { + type payload struct { + Workdir string `json:"workdir"` + Path string `json:"path"` + } + + var body payload + if err := decodeWebSocketPayload(req.Payload, &body); err != nil { + _ = c.writeError(req.ID, "invalid fs.read_workspace_image payload") + return + } + + workdir := strings.TrimSpace(body.Workdir) + path := strings.TrimSpace(body.Path) + if workdir == "" { + _ = c.writeError(req.ID, "workdir is required") + return + } + if path == "" { + _ = c.writeError(req.ID, "path is required") + return + } + + response, err := c.awaitAgentResponse(req.ID, &gatewayv1.GatewayEnvelope{ + RequestId: req.ID, + Timestamp: time.Now().Unix(), + Payload: &gatewayv1.GatewayEnvelope_FsReadWorkspaceImage{ + FsReadWorkspaceImage: &gatewayv1.FsReadWorkspaceImageRequest{ + Workdir: workdir, + Path: path, + }, + }, + }) + if err != nil { + _ = c.writeError(req.ID, websocketErrorMessage(err)) + return + } + if errResp := response.GetError(); errResp != nil { + _ = c.writeError(req.ID, errResp.GetMessage()) + return + } + + resp := response.GetFsReadWorkspaceImageResp() + if resp == nil { + _ = c.writeError(req.ID, "unexpected agent response") + return + } + + _ = c.writeResponse(req.ID, websocketFsReadWorkspaceImageResponsePayload(resp)) +} + func (c *websocketConnection) handleFsWriteText(req websocketRequest) { type payload struct { Workdir string `json:"workdir"` @@ -561,6 +612,17 @@ func websocketFsReadEditableTextResponsePayload(resp *gatewayv1.FsReadEditableTe } } +func websocketFsReadWorkspaceImageResponsePayload(resp *gatewayv1.FsReadWorkspaceImageResponse) map[string]any { + return map[string]any{ + "path": resp.GetPath(), + "mimeType": resp.GetMimeType(), + "data": resp.GetData(), + "sizeBytes": resp.GetSizeBytes(), + "mtimeMs": resp.GetMtimeMs(), + "contentHash": resp.GetContentHash(), + } +} + func websocketFsWriteTextResponsePayload(resp *gatewayv1.FsWriteTextResponse) map[string]any { return map[string]any{ "path": resp.GetPath(), diff --git a/crates/agent-gateway/internal/server/websocket_payload_test.go b/crates/agent-gateway/internal/server/websocket_payload_test.go index 1f43a0af..32508c83 100644 --- a/crates/agent-gateway/internal/server/websocket_payload_test.go +++ b/crates/agent-gateway/internal/server/websocket_payload_test.go @@ -109,6 +109,24 @@ func TestWebsocketFsPayloadsUseFrontendFieldNames(t *testing.T) { t.Fatalf("fs.read_editable_text sizeBytes = %#v, want 11", readEditable["sizeBytes"]) } + readWorkspaceImage := websocketFsReadWorkspaceImageResponsePayload(&gatewayv1.FsReadWorkspaceImageResponse{ + Path: "assets/preview.png", + MimeType: "image/png", + Data: "base64", + SizeBytes: 6, + MtimeMs: 42, + ContentHash: "hash", + }) + if readWorkspaceImage["mimeType"] != "image/png" { + t.Fatalf("fs.read_workspace_image mimeType = %#v", readWorkspaceImage["mimeType"]) + } + if readWorkspaceImage["sizeBytes"] != uint64(6) { + t.Fatalf("fs.read_workspace_image sizeBytes = %#v, want 6", readWorkspaceImage["sizeBytes"]) + } + if readWorkspaceImage["contentHash"] != "hash" { + t.Fatalf("fs.read_workspace_image contentHash = %#v", readWorkspaceImage["contentHash"]) + } + write := websocketFsWriteTextResponsePayload(&gatewayv1.FsWriteTextResponse{ Path: "src/new.ts", Mode: "rewrite", diff --git a/crates/agent-gateway/internal/server/websocket_routes.go b/crates/agent-gateway/internal/server/websocket_routes.go index 0baf1455..82324447 100644 --- a/crates/agent-gateway/internal/server/websocket_routes.go +++ b/crates/agent-gateway/internal/server/websocket_routes.go @@ -11,6 +11,7 @@ var websocketRequestHandlers = map[string]websocketRequestHandler{ "fs.create_project_folder": (*websocketConnection).handleFsCreateProjectFolder, "fs.list": (*websocketConnection).handleFsList, "fs.read_editable_text": (*websocketConnection).handleFsReadEditableText, + "fs.read_workspace_image": (*websocketConnection).handleFsReadWorkspaceImage, "fs.write_text": (*websocketConnection).handleFsWriteText, "fs.create_dir": (*websocketConnection).handleFsCreateDir, "fs.rename": (*websocketConnection).handleFsRename, diff --git a/crates/agent-gateway/internal/server/websocket_routes_test.go b/crates/agent-gateway/internal/server/websocket_routes_test.go index ae89d487..e7e8a2d8 100644 --- a/crates/agent-gateway/internal/server/websocket_routes_test.go +++ b/crates/agent-gateway/internal/server/websocket_routes_test.go @@ -12,6 +12,7 @@ func TestWebsocketRequestHandlersCoverKnownProtocolTypes(t *testing.T) { "fs.create_project_folder", "fs.list", "fs.read_editable_text", + "fs.read_workspace_image", "fs.write_text", "fs.create_dir", "fs.rename", diff --git a/crates/agent-gateway/proto/v1/gateway.proto b/crates/agent-gateway/proto/v1/gateway.proto index 0b55cf6b..94a0df3c 100644 --- a/crates/agent-gateway/proto/v1/gateway.proto +++ b/crates/agent-gateway/proto/v1/gateway.proto @@ -62,6 +62,7 @@ message GatewayEnvelope { FsDeleteRequest fs_delete = 60; GitRequest git_request = 61; FsReadEditableTextRequest fs_read_editable_text = 62; + FsReadWorkspaceImageRequest fs_read_workspace_image = 63; } } @@ -108,6 +109,7 @@ message AgentEnvelope { FsDeleteResponse fs_delete_resp = 63; GitResponse git_response = 64; FsReadEditableTextResponse fs_read_editable_text_resp = 65; + FsReadWorkspaceImageResponse fs_read_workspace_image_resp = 66; ErrorResponse error = 99; } } @@ -565,6 +567,20 @@ message FsReadEditableTextResponse { uint64 total_lines = 6; } +message FsReadWorkspaceImageRequest { + string workdir = 1; + string path = 2; +} + +message FsReadWorkspaceImageResponse { + string path = 1; + string mime_type = 2; + string data = 3; + uint64 size_bytes = 4; + uint64 mtime_ms = 5; + string content_hash = 6; +} + message FsWriteTextRequest { string workdir = 1; string path = 2; diff --git a/crates/agent-gateway/web/src/App.tsx b/crates/agent-gateway/web/src/App.tsx index f5c179b7..d8e7734e 100644 --- a/crates/agent-gateway/web/src/App.tsx +++ b/crates/agent-gateway/web/src/App.tsx @@ -31,6 +31,8 @@ import { Button } from "@/components/ui/button"; import { ScrollArea } from "@/components/ui/scroll-area"; import { ProjectToolsPanel } from "@/components/project-tools/ProjectToolsPanel"; import type { WorkspaceCodeEditorOpenRequest } from "@/components/workspace-editor/WorkspaceCodeEditorOverlay"; +import type { WorkspaceImagePreviewOpenRequest } from "@/components/workspace-editor/WorkspaceImagePreviewOverlay"; +import { isWorkspaceImagePath } from "@/components/workspace-editor/workspaceImagePreview"; import { LocaleContext, t as translate } from "@/i18n"; import type { MentionComposerCommitMention, @@ -216,6 +218,13 @@ const WorkspaceCodeEditorOverlay = lazy(async () => { }; }); +const WorkspaceImagePreviewOverlay = lazy(async () => { + const module = await import("@/components/workspace-editor/WorkspaceImagePreviewOverlay"); + return { + default: module.WorkspaceImagePreviewOverlay, + }; +}); + const MAX_UPLOAD_FILES = 9; function dragEventHasFiles(event: DragEvent) { @@ -817,9 +826,7 @@ export default function App() { const [isFileDropActive, setIsFileDropActive] = useState(false); const [activeView, setActiveView] = useState<"chat" | "skills-hub" | "mcp-hub">("chat"); const [projectToolsPanelOpen, setProjectToolsPanelOpen] = useState(false); - const projectToolsFileTreeOpenCount = - settings.customSettings.projectToolsFileTree.openProjectPathKeys.length; - const previousProjectToolsFileTreeOpenCountRef = useRef(projectToolsFileTreeOpenCount); + const previousProjectToolsFileTreeOpenRef = useRef(false); const [workspaceEditorMounted, setWorkspaceEditorMounted] = useState(false); const [workspaceEditorOpen, setWorkspaceEditorOpen] = useState(false); const [workspaceEditorCleanupPending, setWorkspaceEditorCleanupPending] = useState(false); @@ -827,6 +834,11 @@ export default function App() { useState(null); const [workspaceEditorCloseRequestId, setWorkspaceEditorCloseRequestId] = useState(0); const workspaceEditorRequestIdRef = useRef(0); + const [workspaceImagePreviewMounted, setWorkspaceImagePreviewMounted] = useState(false); + const [workspaceImagePreviewOpen, setWorkspaceImagePreviewOpen] = useState(false); + const [workspaceImagePreviewOpenRequest, setWorkspaceImagePreviewOpenRequest] = + useState(null); + const workspaceImagePreviewRequestIdRef = useRef(0); const [terminalSessions, setTerminalSessions] = useState([]); const { confirm: requestConfirmDialog, dialog: confirmDialog } = useConfirmDialog(); const terminalSessionsVersionRef = useRef(0); @@ -5280,6 +5292,10 @@ export default function App() { const terminalProjectPathKey = terminalProjectPath ? workspaceProjectPathKey(terminalProjectPath) : ""; + const projectToolsFileTreeOpen = isProjectToolsFileTreeOpen( + settings.customSettings, + terminalProjectPathKey, + ); const projectToolsDisabledMessage = !settingsSyncReady ? "Syncing desktop settings..." : !isAgentMode @@ -5295,9 +5311,22 @@ export default function App() { const gitDisabledMessage = !settings.remote.enableWebGit ? "WebUI Git is disabled in desktop Remote settings." : undefined; - const handleOpenEditableFile = useCallback( + const handleOpenWorkspaceFile = useCallback( (path: string) => { if (!terminalProjectPath || !terminalProjectPathKey) return; + if (isWorkspaceImagePath(path)) { + workspaceImagePreviewRequestIdRef.current += 1; + setWorkspaceImagePreviewMounted(true); + setWorkspaceImagePreviewOpen(true); + setWorkspaceImagePreviewOpenRequest({ + id: workspaceImagePreviewRequestIdRef.current, + projectPathKey: terminalProjectPathKey, + workdir: terminalProjectPath, + path, + }); + return; + } + setWorkspaceImagePreviewOpen(false); workspaceEditorRequestIdRef.current += 1; setWorkspaceEditorCleanupPending(false); setWorkspaceEditorMounted(true); @@ -5314,22 +5343,35 @@ export default function App() { const requestWorkspaceEditorClose = useCallback(() => { setWorkspaceEditorCloseRequestId((current) => current + 1); }, []); + const requestWorkspaceImagePreviewClose = useCallback(() => { + setWorkspaceImagePreviewOpen(false); + }, []); + const handleWorkspaceImagePreviewClosed = useCallback(() => { + setWorkspaceImagePreviewOpen(false); + setWorkspaceImagePreviewMounted(false); + setWorkspaceImagePreviewOpenRequest(null); + }, []); useEffect(() => { - const previousOpenCount = previousProjectToolsFileTreeOpenCountRef.current; - previousProjectToolsFileTreeOpenCountRef.current = projectToolsFileTreeOpenCount; - if (projectToolsFileTreeOpenCount > 0 && workspaceEditorCleanupPending) { + const previousOpen = previousProjectToolsFileTreeOpenRef.current; + previousProjectToolsFileTreeOpenRef.current = projectToolsFileTreeOpen; + if (projectToolsFileTreeOpen && workspaceEditorCleanupPending) { setWorkspaceEditorCleanupPending(false); } - if (previousOpenCount > 0 && projectToolsFileTreeOpenCount === 0 && workspaceEditorMounted) { + if (previousOpen && !projectToolsFileTreeOpen && workspaceEditorMounted) { setWorkspaceEditorCleanupPending(true); setWorkspaceEditorOpen(true); requestWorkspaceEditorClose(); } + if (previousOpen && !projectToolsFileTreeOpen && workspaceImagePreviewMounted) { + requestWorkspaceImagePreviewClose(); + } }, [ - projectToolsFileTreeOpenCount, + projectToolsFileTreeOpen, requestWorkspaceEditorClose, + requestWorkspaceImagePreviewClose, workspaceEditorCleanupPending, workspaceEditorMounted, + workspaceImagePreviewMounted, ]); const projectTerminalSessions = useMemo( () => @@ -6341,6 +6383,22 @@ export default function App() { /> ) : null} + {workspaceImagePreviewMounted ? ( + + {translate("workspaceImagePreview.loading", settings.locale)} + + } + > + + + ) : null} {terminalClient ? ( @@ -6356,10 +6414,7 @@ export default function App() { terminalDisabledMessage={terminalDisabledMessage} activeTab={settings.customSettings.projectToolsPanel.activeTab} tabOrder={getProjectToolsPanelTabOrder(settings.customSettings, terminalProjectPathKey)} - fileTreeOpen={isProjectToolsFileTreeOpen( - settings.customSettings, - terminalProjectPathKey, - )} + fileTreeOpen={projectToolsFileTreeOpen} fileTreeState={getProjectToolsFileTreeProjectState( settings.customSettings, terminalProjectPathKey, @@ -6417,7 +6472,7 @@ export default function App() { composerRef.current?.insertFileMention(path, kind); composerRef.current?.focus(); }} - onOpenEditableFile={handleOpenEditableFile} + onOpenFile={handleOpenWorkspaceFile} onInsertCommitMention={(commit) => { composerRef.current?.insertCommitMention(commit); composerRef.current?.focus(); diff --git a/crates/agent-gateway/web/src/components/project-tools/ProjectFileTreePanel.tsx b/crates/agent-gateway/web/src/components/project-tools/ProjectFileTreePanel.tsx index 95a53336..9900e888 100644 --- a/crates/agent-gateway/web/src/components/project-tools/ProjectFileTreePanel.tsx +++ b/crates/agent-gateway/web/src/components/project-tools/ProjectFileTreePanel.tsx @@ -7,6 +7,7 @@ import type { } from "@/lib/settings"; import { cn } from "@/lib/shared/utils"; import { getFileTypeIcon } from "../chat/fileTypeIcons"; +import { isWorkspaceImagePath } from "../workspace-editor/workspaceImagePreview"; import { Check, ChevronRight, @@ -15,6 +16,7 @@ import { FilePenLine, Folder, FolderOpen, + ImageIcon, Loader2, Plus, RefreshCw, @@ -149,7 +151,7 @@ export function ProjectFileTreePanel(props: { onInitializedChange: (initialized: boolean) => void; onSyncStateChange: (patch: ProjectToolsFileTreeStatePatch) => void; onInsertFileMention?: (path: string, kind: FileTreeKind) => void; - onOpenEditableFile?: (path: string) => void; + onOpenFile?: (path: string) => void; }) { const { projectPathKey, @@ -159,7 +161,7 @@ export function ProjectFileTreePanel(props: { onInitializedChange, onSyncStateChange, onInsertFileMention, - onOpenEditableFile, + onOpenFile, } = props; const { t } = useLocale(); const [states, setStates] = useState>({}); @@ -737,7 +739,7 @@ export function ProjectFileTreePanel(props: { toggleDirectory(path, expanded); return; } - onOpenEditableFile?.(path); + onOpenFile?.(path); }} > {node.kind === "dir" ? ( @@ -763,7 +765,7 @@ export function ProjectFileTreePanel(props: { }, [ cwd, - onOpenEditableFile, + onOpenFile, openContextMenu, setProjectState, state, @@ -959,14 +961,22 @@ export function ProjectFileTreePanel(props: { type="button" role="menuitem" className="flex w-full items-center gap-2 rounded-lg px-2.5 py-1.5 text-left transition-colors hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-45" - disabled={!onOpenEditableFile} + disabled={!onOpenFile} onClick={() => { - onOpenEditableFile?.(contextPath); + onOpenFile?.(contextPath); setContextMenu(null); }} > - - {t("projectTools.fileTree.openFile")} + {isWorkspaceImagePath(contextPath) ? ( + + ) : ( + + )} + {t( + isWorkspaceImagePath(contextPath) + ? "projectTools.fileTree.previewImage" + : "projectTools.fileTree.openFile", + )}
diff --git a/crates/agent-gateway/web/src/components/project-tools/ProjectToolsPanel.tsx b/crates/agent-gateway/web/src/components/project-tools/ProjectToolsPanel.tsx index 3d5e5bf1..c150a6ec 100644 --- a/crates/agent-gateway/web/src/components/project-tools/ProjectToolsPanel.tsx +++ b/crates/agent-gateway/web/src/components/project-tools/ProjectToolsPanel.tsx @@ -93,7 +93,7 @@ type ProjectToolsPanelProps = { onGitReviewOpenChange: (open: boolean) => void; onSessionsChange?: (sessions: TerminalSession[]) => void; onInsertFileMention?: (path: string, kind: "file" | "dir") => void; - onOpenEditableFile?: (path: string) => void; + onOpenFile?: (path: string) => void; onInsertCommitMention?: (commit: GitCommitContextPayload) => void; onInsertGitFileMention?: (file: GitFileContextPayload) => void; onClose?: () => void; @@ -623,7 +623,7 @@ export function ProjectToolsPanel(props: ProjectToolsPanelProps) { onGitReviewOpenChange, onSessionsChange, onInsertFileMention, - onOpenEditableFile, + onOpenFile, onInsertCommitMention, onInsertGitFileMention, onClose, @@ -1935,7 +1935,7 @@ export function ProjectToolsPanel(props: ProjectToolsPanelProps) { onInitializedChange={setFileTreeInitialized} onSyncStateChange={onFileTreeStateChange} onInsertFileMention={onInsertFileMention} - onOpenEditableFile={onOpenEditableFile} + onOpenFile={onOpenFile} />
) : null} diff --git a/crates/agent-gateway/web/src/components/workspace-editor/WorkspaceImagePreviewOverlay.tsx b/crates/agent-gateway/web/src/components/workspace-editor/WorkspaceImagePreviewOverlay.tsx new file mode 100644 index 00000000..83f40fa5 --- /dev/null +++ b/crates/agent-gateway/web/src/components/workspace-editor/WorkspaceImagePreviewOverlay.tsx @@ -0,0 +1,194 @@ +import { invoke } from "@tauri-apps/api/core"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { useLocale } from "@/i18n"; +import { cn } from "@/lib/shared/utils"; +import { AlertTriangle, ImageIcon, ImageOff, Loader2, RefreshCw, X } from "../icons"; + +export type WorkspaceImagePreviewOpenRequest = { + id: number; + projectPathKey: string; + workdir: string; + path: string; +}; + +type ReadWorkspaceImageResponse = { + path: string; + mimeType: string; + data: string; + sizeBytes: number; +}; + +type WorkspaceImagePreviewOverlayProps = { + openRequest: WorkspaceImagePreviewOpenRequest | null; + isOpen: boolean; + onRequestClose: () => void; + onClose: () => void; +}; + +const IMAGE_PREVIEW_OVERLAY_ANIMATION_MS = 180; + +function basename(path: string) { + const normalized = path.replace(/\\/g, "/").replace(/\/+$/, ""); + const index = normalized.lastIndexOf("/"); + return index >= 0 ? normalized.slice(index + 1) : normalized; +} + +function formatBytes(bytes: number) { + if (!Number.isFinite(bytes) || bytes < 0) return ""; + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; +} + +function toMessage(error: unknown, fallback: string) { + if (error instanceof Error && error.message.trim()) return error.message; + const text = String(error ?? "").trim(); + return text || fallback; +} + +export function WorkspaceImagePreviewOverlay(props: WorkspaceImagePreviewOverlayProps) { + const { openRequest, isOpen, onRequestClose, onClose } = props; + const { t } = useLocale(); + const closeAnimationTimeoutRef = useRef(null); + const loadSequenceRef = useRef(0); + const [image, setImage] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [isVisible, setIsVisible] = useState(false); + + useEffect(() => { + if (isOpen) { + if (closeAnimationTimeoutRef.current !== null) { + window.clearTimeout(closeAnimationTimeoutRef.current); + closeAnimationTimeoutRef.current = null; + } + const animationFrame = window.requestAnimationFrame(() => setIsVisible(true)); + return () => window.cancelAnimationFrame(animationFrame); + } + + setIsVisible(false); + closeAnimationTimeoutRef.current = window.setTimeout(() => { + closeAnimationTimeoutRef.current = null; + onClose(); + }, IMAGE_PREVIEW_OVERLAY_ANIMATION_MS); + }, [isOpen, onClose]); + + useEffect( + () => () => { + if (closeAnimationTimeoutRef.current !== null) { + window.clearTimeout(closeAnimationTimeoutRef.current); + } + }, + [], + ); + + const loadImage = useCallback( + async (request: WorkspaceImagePreviewOpenRequest) => { + const sequence = loadSequenceRef.current + 1; + loadSequenceRef.current = sequence; + setLoading(true); + setError(null); + setImage(null); + try { + const response = await invoke("fs_read_workspace_image", { + workdir: request.workdir, + path: request.path, + }); + if (loadSequenceRef.current !== sequence) return; + setImage(response); + } catch (loadError) { + if (loadSequenceRef.current !== sequence) return; + setImage(null); + setError(toMessage(loadError, t("workspaceImagePreview.openFailed"))); + } finally { + if (loadSequenceRef.current === sequence) { + setLoading(false); + } + } + }, + [t], + ); + + useEffect(() => { + if (!openRequest) return; + void loadImage(openRequest); + }, [loadImage, openRequest]); + + const source = image ? `data:${image.mimeType};base64,${image.data}` : ""; + const activePath = image?.path ?? openRequest?.path ?? ""; + + return ( +
+
+ +
+
+ {t("workspaceImagePreview.title")} +
+
{activePath}
+
+
+ + +
+
+ + {error ? ( +
+ +
{error}
+
+ ) : null} + +
+ {loading ? ( + + ) : source ? ( + {basename(activePath)} + ) : ( +
+ + {t("workspaceImagePreview.empty")} +
+ )} +
+ +
+ {activePath} + {image ? ( + + {image.mimeType} · {formatBytes(image.sizeBytes)} + + ) : null} +
+
+ ); +} diff --git a/crates/agent-gateway/web/src/components/workspace-editor/workspaceImagePreview.ts b/crates/agent-gateway/web/src/components/workspace-editor/workspaceImagePreview.ts new file mode 100644 index 00000000..d407f0e6 --- /dev/null +++ b/crates/agent-gateway/web/src/components/workspace-editor/workspaceImagePreview.ts @@ -0,0 +1,18 @@ +const WORKSPACE_IMAGE_EXTENSIONS = new Set([ + "bmp", + "gif", + "ico", + "jpeg", + "jpg", + "png", + "svg", + "webp", +]); + +export function isWorkspaceImagePath(path: string) { + const normalized = path.trim().replace(/\\/g, "/"); + const name = normalized.slice(normalized.lastIndexOf("/") + 1); + const extensionIndex = name.lastIndexOf("."); + if (extensionIndex < 0) return false; + return WORKSPACE_IMAGE_EXTENSIONS.has(name.slice(extensionIndex + 1).toLowerCase()); +} diff --git a/crates/agent-gateway/web/src/i18n/config.ts b/crates/agent-gateway/web/src/i18n/config.ts index 64e4e89e..b3e4d7ff 100644 --- a/crates/agent-gateway/web/src/i18n/config.ts +++ b/crates/agent-gateway/web/src/i18n/config.ts @@ -414,6 +414,7 @@ export const translations: Record> = { "projectTools.fileTree.newFile": "新建文件", "projectTools.fileTree.newFolder": "新建文件夹", "projectTools.fileTree.openFile": "打开文件", + "projectTools.fileTree.previewImage": "预览图片", "projectTools.fileTree.rename": "重命名", "projectTools.fileTree.delete": "删除", "projectTools.fileTree.copyPath": "复制路径", @@ -455,6 +456,12 @@ export const translations: Record> = { "此文件有未保存修改。你可以保存、放弃修改,或返回继续编辑。", "workspaceEditor.reloadDirtyTitle": "重新加载前放弃当前修改?", "workspaceEditor.reloadDirtyDescription": "重新加载会用磁盘版本替换当前编辑内容。", + "workspaceImagePreview.loading": "正在加载图片...", + "workspaceImagePreview.title": "图片预览", + "workspaceImagePreview.reload": "重新加载", + "workspaceImagePreview.close": "关闭预览", + "workspaceImagePreview.openFailed": "打开图片失败", + "workspaceImagePreview.empty": "未选择图片", /* ── Settings Nav ── */ "settings.navSystem": "系统设置", @@ -1583,6 +1590,7 @@ export const translations: Record> = { "projectTools.fileTree.newFile": "New File", "projectTools.fileTree.newFolder": "New Folder", "projectTools.fileTree.openFile": "Open File", + "projectTools.fileTree.previewImage": "Preview Image", "projectTools.fileTree.rename": "Rename", "projectTools.fileTree.delete": "Delete", "projectTools.fileTree.copyPath": "Copy Path", @@ -1625,6 +1633,12 @@ export const translations: Record> = { "workspaceEditor.reloadDirtyTitle": "Discard current changes before reloading?", "workspaceEditor.reloadDirtyDescription": "Reloading replaces the current editor contents with the version on disk.", + "workspaceImagePreview.loading": "Loading image...", + "workspaceImagePreview.title": "Image Preview", + "workspaceImagePreview.reload": "Reload", + "workspaceImagePreview.close": "Close preview", + "workspaceImagePreview.openFailed": "Failed to open image", + "workspaceImagePreview.empty": "No image selected", /* ── Settings Nav ── */ "settings.navSystem": "System", diff --git a/crates/agent-gateway/web/src/lib/gatewaySocket.ts b/crates/agent-gateway/web/src/lib/gatewaySocket.ts index ec18d5c1..d81da040 100644 --- a/crates/agent-gateway/web/src/lib/gatewaySocket.ts +++ b/crates/agent-gateway/web/src/lib/gatewaySocket.ts @@ -124,6 +124,15 @@ type FsReadEditableTextResponse = { totalLines: number; }; +type FsReadWorkspaceImageResponse = { + path: string; + mimeType: string; + data: string; + sizeBytes: number; + mtimeMs: number; + contentHash: string; +}; + type FsCreateDirResponse = { path: string; kind: "dir"; @@ -1065,6 +1074,16 @@ export class GatewayWebSocketClient { }); } + async readWorkspaceImageFile( + workdir: string, + path: string, + ): Promise { + return this.request("fs.read_workspace_image", { + workdir, + path, + }); + } + async createDir(workdir: string, path: string): Promise { return this.request("fs.create_dir", { workdir, path }); } @@ -1897,6 +1916,7 @@ export type GatewayWebSocketClientLike = { expectedContentHash?: string; }): Promise; readEditableTextFile(workdir: string, path: string): Promise; + readWorkspaceImageFile(workdir: string, path: string): Promise; createDir(workdir: string, path: string): Promise; renamePath(workdir: string, fromPath: string, toPath: string): Promise; deletePath(workdir: string, path: string): Promise; @@ -2600,6 +2620,16 @@ class SharedWorkerGatewayWebSocketClient implements GatewayWebSocketClientLike { }); } + async readWorkspaceImageFile( + workdir: string, + path: string, + ): Promise { + return this.request("fs.read_workspace_image", { + workdir, + path, + }); + } + async createDir(workdir: string, path: string): Promise { return this.request("fs.create_dir", { workdir, path }); } diff --git a/crates/agent-gateway/web/src/lib/gatewaySocket.worker.ts b/crates/agent-gateway/web/src/lib/gatewaySocket.worker.ts index 7164bb08..ad95bd46 100644 --- a/crates/agent-gateway/web/src/lib/gatewaySocket.worker.ts +++ b/crates/agent-gateway/web/src/lib/gatewaySocket.worker.ts @@ -394,6 +394,8 @@ async function resolveRequest(client: GatewayWebSocketClient, method: string, pa }); case "fs.read_editable_text": return client.readEditableTextFile(String(body.workdir ?? ""), String(body.path ?? "")); + case "fs.read_workspace_image": + return client.readWorkspaceImageFile(String(body.workdir ?? ""), String(body.path ?? "")); case "fs.create_dir": return client.createDir(String(body.workdir ?? ""), String(body.path ?? "")); case "fs.rename": diff --git a/crates/agent-gateway/web/src/shims/tauriCore.ts b/crates/agent-gateway/web/src/shims/tauriCore.ts index 5f237fe3..10cc43df 100644 --- a/crates/agent-gateway/web/src/shims/tauriCore.ts +++ b/crates/agent-gateway/web/src/shims/tauriCore.ts @@ -222,6 +222,11 @@ export async function invoke(command: string, args?: Record) String(args?.workdir ?? ""), String(args?.path ?? ""), )) as T; + case "fs_read_workspace_image": + return (await getGatewayWebSocketClient(loadToken().trim()).readWorkspaceImageFile( + String(args?.workdir ?? ""), + String(args?.path ?? ""), + )) as T; case "fs_create_dir": return (await getGatewayWebSocketClient(loadToken().trim()).createDir( String(args?.workdir ?? ""), diff --git a/crates/agent-gateway/web/src/styles.css b/crates/agent-gateway/web/src/styles.css index a3733c5c..016936f7 100644 --- a/crates/agent-gateway/web/src/styles.css +++ b/crates/agent-gateway/web/src/styles.css @@ -1839,7 +1839,8 @@ html[data-liveagent-webui="gateway"] .external-link-modal-overlay[data-state="cl backdrop-filter: blur(18px); } - html[data-liveagent-webui="gateway"] .workspace-code-editor-overlay { + html[data-liveagent-webui="gateway"] .workspace-code-editor-overlay, + html[data-liveagent-webui="gateway"] .workspace-image-preview-overlay { z-index: 60; } diff --git a/crates/agent-gui/src-tauri/src/commands/fs.rs b/crates/agent-gui/src-tauri/src/commands/fs.rs index 3136152a..ea4ef8e4 100644 --- a/crates/agent-gui/src-tauri/src/commands/fs.rs +++ b/crates/agent-gui/src-tauri/src/commands/fs.rs @@ -1766,6 +1766,29 @@ pub async fn fs_read_image_source( .await } +pub(crate) fn fs_read_workspace_image_sync( + workdir: String, + path: String, +) -> Result { + let wd = canonicalize_workdir(&workdir).map_err(|e| e.to_string())?; + let rel = sanitize_rel_path(&path).map_err(|e| e.to_string())?; + let logical_path = logical_rel_path(&rel); + let target = wd.join(&rel); + let target = resolve_existing_file_target(&wd, &target, "Image.path")?; + read_local_image_file(target, logical_path) +} + +#[tauri::command(rename_all = "snake_case")] +pub async fn fs_read_workspace_image( + workdir: String, + path: String, +) -> Result { + run_blocking("fs_read_workspace_image", move || { + fs_read_workspace_image_sync(workdir, path) + }) + .await +} + fn fs_read_text_sync( workdir: String, path: String, @@ -3375,6 +3398,38 @@ mod tests { let _ = fs::remove_dir_all(workdir); } + #[test] + fn workspace_image_reads_relative_path_and_rejects_out_of_bounds_sources() { + let workdir = unique_test_workdir("workspace-image"); + fs::create_dir_all(workdir.join("assets")).expect("create workdir"); + let bytes = png_like_bytes(); + fs::write(workdir.join("assets/preview.png"), &bytes).expect("write workspace image"); + + let response = fs_read_workspace_image_sync( + workdir.display().to_string(), + "assets/preview.png".to_string(), + ) + .expect("workspace image should read"); + + assert_eq!(response.kind, "image"); + assert_eq!(response.path, "assets/preview.png"); + assert_eq!(response.mime_type.as_deref(), Some("image/png")); + assert_eq!(response.size_bytes, Some(bytes.len())); + assert_eq!( + response.data.as_deref(), + Some(BASE64_STANDARD.encode(&bytes).as_str()) + ); + + for path in ["/tmp/liveagent-outside.png", "../outside.png", ""] { + let error = + fs_read_workspace_image_sync(workdir.display().to_string(), path.to_string()) + .expect_err("out-of-bounds workspace image path should fail"); + assert!(!error.trim().is_empty(), "expected error for {path:?}"); + } + + let _ = fs::remove_dir_all(workdir); + } + #[test] fn read_docx_extracts_word_text() { let workdir = unique_test_workdir("read-docx"); diff --git a/crates/agent-gui/src-tauri/src/lib.rs b/crates/agent-gui/src-tauri/src/lib.rs index 5f2a15e2..8d1f3957 100644 --- a/crates/agent-gui/src-tauri/src/lib.rs +++ b/crates/agent-gui/src-tauri/src/lib.rs @@ -62,6 +62,7 @@ macro_rules! app_invoke_handler { commands::fs::fs_read_text, commands::fs::fs_read_editable_text, commands::fs::fs_read_image_source, + commands::fs::fs_read_workspace_image, commands::fs::fs_write_text, commands::fs::fs_edit_text, commands::fs::fs_delete, diff --git a/crates/agent-gui/src-tauri/src/services/gateway.rs b/crates/agent-gui/src-tauri/src/services/gateway.rs index 87c0d532..8cff5b6f 100644 --- a/crates/agent-gui/src-tauri/src/services/gateway.rs +++ b/crates/agent-gui/src-tauri/src/services/gateway.rs @@ -999,6 +999,21 @@ impl GatewayController { Err(error) => self.send_error_response(request_id, 500, error).await, } } + Some(proto::gateway_envelope::Payload::FsReadWorkspaceImage(request)) => { + match gateway_bridge::handle_fs_read_workspace_image(request).await { + Ok(response) => { + self.send_agent_envelope(proto::AgentEnvelope { + request_id, + timestamp: now_unix_seconds(), + payload: Some( + proto::agent_envelope::Payload::FsReadWorkspaceImageResp(response), + ), + }) + .await + } + Err(error) => self.send_error_response(request_id, 500, error).await, + } + } 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 2a214884..a627bf03 100644 --- a/crates/agent-gui/src-tauri/src/services/gateway_bridge.rs +++ b/crates/agent-gui/src-tauri/src/services/gateway_bridge.rs @@ -7,7 +7,8 @@ use crate::commands::{ chat_history, fs::{ fs_create_dir_sync, fs_delete_sync, fs_list_sync, fs_mention_list_sync, - fs_read_editable_text_sync, fs_rename_sync, fs_write_text_sync, + fs_read_editable_text_sync, fs_read_workspace_image_sync, fs_rename_sync, + fs_write_text_sync, }, git::git_gateway_action_sync, settings::{load_providers, open_db}, @@ -551,6 +552,30 @@ pub async fn handle_fs_read_editable_text( }) } +pub async fn handle_fs_read_workspace_image( + request: proto::FsReadWorkspaceImageRequest, +) -> Result { + tauri::async_runtime::spawn_blocking(move || { + fs_read_workspace_image_sync(request.workdir, request.path) + }) + .await + .map_err(|e| format!("gateway fs read workspace image join failed: {e}"))? + .and_then(|response| { + Ok(proto::FsReadWorkspaceImageResponse { + path: response.path, + mime_type: response + .mime_type + .ok_or_else(|| "workspace image response is missing mime type".to_string())?, + data: response + .data + .ok_or_else(|| "workspace image response is missing data".to_string())?, + size_bytes: u64::try_from(response.size_bytes.unwrap_or_default()).unwrap_or(u64::MAX), + mtime_ms: response.mtime_ms, + content_hash: response.content_hash, + }) + }) +} + pub async fn handle_fs_write_text( request: proto::FsWriteTextRequest, ) -> Result { diff --git a/crates/agent-gui/src/components/project-tools/ProjectFileTreePanel.tsx b/crates/agent-gui/src/components/project-tools/ProjectFileTreePanel.tsx index c3a3415a..e1e28dd8 100644 --- a/crates/agent-gui/src/components/project-tools/ProjectFileTreePanel.tsx +++ b/crates/agent-gui/src/components/project-tools/ProjectFileTreePanel.tsx @@ -7,6 +7,7 @@ import type { } from "../../lib/settings"; import { cn } from "../../lib/shared/utils"; import { getFileTypeIcon } from "../chat/fileTypeIcons"; +import { isWorkspaceImagePath } from "../workspace-editor/workspaceImagePreview"; import { Check, ChevronRight, @@ -15,6 +16,7 @@ import { FilePenLine, Folder, FolderOpen, + ImageIcon, Loader2, Plus, RefreshCw, @@ -149,7 +151,7 @@ export function ProjectFileTreePanel(props: { onInitializedChange: (initialized: boolean) => void; onSyncStateChange: (patch: ProjectToolsFileTreeStatePatch) => void; onInsertFileMention?: (path: string, kind: FileTreeKind) => void; - onOpenEditableFile?: (path: string) => void; + onOpenFile?: (path: string) => void; }) { const { projectPathKey, @@ -159,7 +161,7 @@ export function ProjectFileTreePanel(props: { onInitializedChange, onSyncStateChange, onInsertFileMention, - onOpenEditableFile, + onOpenFile, } = props; const { t } = useLocale(); const [states, setStates] = useState>({}); @@ -737,7 +739,7 @@ export function ProjectFileTreePanel(props: { toggleDirectory(path, expanded); return; } - onOpenEditableFile?.(path); + onOpenFile?.(path); }} > {node.kind === "dir" ? ( @@ -763,7 +765,7 @@ export function ProjectFileTreePanel(props: { }, [ cwd, - onOpenEditableFile, + onOpenFile, openContextMenu, setProjectState, state, @@ -959,14 +961,22 @@ export function ProjectFileTreePanel(props: { type="button" role="menuitem" className="flex w-full items-center gap-2 rounded-lg px-2.5 py-1.5 text-left transition-colors hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-45" - disabled={!onOpenEditableFile} + disabled={!onOpenFile} onClick={() => { - onOpenEditableFile?.(contextPath); + onOpenFile?.(contextPath); setContextMenu(null); }} > - - {t("projectTools.fileTree.openFile")} + {isWorkspaceImagePath(contextPath) ? ( + + ) : ( + + )} + {t( + isWorkspaceImagePath(contextPath) + ? "projectTools.fileTree.previewImage" + : "projectTools.fileTree.openFile", + )}
diff --git a/crates/agent-gui/src/components/project-tools/ProjectToolsPanel.tsx b/crates/agent-gui/src/components/project-tools/ProjectToolsPanel.tsx index 546b9105..6547995c 100644 --- a/crates/agent-gui/src/components/project-tools/ProjectToolsPanel.tsx +++ b/crates/agent-gui/src/components/project-tools/ProjectToolsPanel.tsx @@ -92,7 +92,7 @@ type ProjectToolsPanelProps = { onGitReviewOpenChange: (open: boolean) => void; onSessionsChange?: (sessions: TerminalSession[]) => void; onInsertFileMention?: (path: string, kind: "file" | "dir") => void; - onOpenEditableFile?: (path: string) => void; + onOpenFile?: (path: string) => void; onInsertCommitMention?: (commit: GitCommitContextPayload) => void; onInsertGitFileMention?: (file: GitFileContextPayload) => void; onClose?: () => void; @@ -618,7 +618,7 @@ export function ProjectToolsPanel(props: ProjectToolsPanelProps) { onGitReviewOpenChange, onSessionsChange, onInsertFileMention, - onOpenEditableFile, + onOpenFile, onInsertCommitMention, onInsertGitFileMention, onClose, @@ -1689,7 +1689,7 @@ export function ProjectToolsPanel(props: ProjectToolsPanelProps) { onInitializedChange={setFileTreeInitialized} onSyncStateChange={onFileTreeStateChange} onInsertFileMention={onInsertFileMention} - onOpenEditableFile={onOpenEditableFile} + onOpenFile={onOpenFile} />
) : null} diff --git a/crates/agent-gui/src/components/workspace-editor/WorkspaceImagePreviewOverlay.tsx b/crates/agent-gui/src/components/workspace-editor/WorkspaceImagePreviewOverlay.tsx new file mode 100644 index 00000000..4986ad42 --- /dev/null +++ b/crates/agent-gui/src/components/workspace-editor/WorkspaceImagePreviewOverlay.tsx @@ -0,0 +1,196 @@ +import { invoke } from "@tauri-apps/api/core"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { useLocale } from "../../i18n"; +import { cn } from "../../lib/shared/utils"; +import { AlertTriangle, ImageIcon, ImageOff, Loader2, RefreshCw, X } from "../icons"; +import { MacOsTitleBarSpacer } from "../MacOsTitleBarSpacer"; + +export type WorkspaceImagePreviewOpenRequest = { + id: number; + projectPathKey: string; + workdir: string; + path: string; +}; + +type ReadWorkspaceImageResponse = { + path: string; + mimeType: string; + data: string; + sizeBytes: number; +}; + +type WorkspaceImagePreviewOverlayProps = { + openRequest: WorkspaceImagePreviewOpenRequest | null; + isOpen: boolean; + onRequestClose: () => void; + onClose: () => void; +}; + +const IMAGE_PREVIEW_OVERLAY_ANIMATION_MS = 180; + +function basename(path: string) { + const normalized = path.replace(/\\/g, "/").replace(/\/+$/, ""); + const index = normalized.lastIndexOf("/"); + return index >= 0 ? normalized.slice(index + 1) : normalized; +} + +function formatBytes(bytes: number) { + if (!Number.isFinite(bytes) || bytes < 0) return ""; + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; +} + +function toMessage(error: unknown, fallback: string) { + if (error instanceof Error && error.message.trim()) return error.message; + const text = String(error ?? "").trim(); + return text || fallback; +} + +export function WorkspaceImagePreviewOverlay(props: WorkspaceImagePreviewOverlayProps) { + const { openRequest, isOpen, onRequestClose, onClose } = props; + const { t } = useLocale(); + const closeAnimationTimeoutRef = useRef(null); + const loadSequenceRef = useRef(0); + const [image, setImage] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [isVisible, setIsVisible] = useState(false); + + useEffect(() => { + if (isOpen) { + if (closeAnimationTimeoutRef.current !== null) { + window.clearTimeout(closeAnimationTimeoutRef.current); + closeAnimationTimeoutRef.current = null; + } + const animationFrame = window.requestAnimationFrame(() => setIsVisible(true)); + return () => window.cancelAnimationFrame(animationFrame); + } + + setIsVisible(false); + closeAnimationTimeoutRef.current = window.setTimeout(() => { + closeAnimationTimeoutRef.current = null; + onClose(); + }, IMAGE_PREVIEW_OVERLAY_ANIMATION_MS); + }, [isOpen, onClose]); + + useEffect( + () => () => { + if (closeAnimationTimeoutRef.current !== null) { + window.clearTimeout(closeAnimationTimeoutRef.current); + } + }, + [], + ); + + const loadImage = useCallback( + async (request: WorkspaceImagePreviewOpenRequest) => { + const sequence = loadSequenceRef.current + 1; + loadSequenceRef.current = sequence; + setLoading(true); + setError(null); + setImage(null); + try { + const response = await invoke("fs_read_workspace_image", { + workdir: request.workdir, + path: request.path, + }); + if (loadSequenceRef.current !== sequence) return; + setImage(response); + } catch (loadError) { + if (loadSequenceRef.current !== sequence) return; + setImage(null); + setError(toMessage(loadError, t("workspaceImagePreview.openFailed"))); + } finally { + if (loadSequenceRef.current === sequence) { + setLoading(false); + } + } + }, + [t], + ); + + useEffect(() => { + if (!openRequest) return; + void loadImage(openRequest); + }, [loadImage, openRequest]); + + const source = image ? `data:${image.mimeType};base64,${image.data}` : ""; + const activePath = image?.path ?? openRequest?.path ?? ""; + + return ( +
+ +
+ +
+
+ {t("workspaceImagePreview.title")} +
+
{activePath}
+
+
+ + +
+
+ + {error ? ( +
+ +
{error}
+
+ ) : null} + +
+ {loading ? ( + + ) : source ? ( + {basename(activePath)} + ) : ( +
+ + {t("workspaceImagePreview.empty")} +
+ )} +
+ +
+ {activePath} + {image ? ( + + {image.mimeType} · {formatBytes(image.sizeBytes)} + + ) : null} +
+
+ ); +} diff --git a/crates/agent-gui/src/components/workspace-editor/workspaceImagePreview.ts b/crates/agent-gui/src/components/workspace-editor/workspaceImagePreview.ts new file mode 100644 index 00000000..d407f0e6 --- /dev/null +++ b/crates/agent-gui/src/components/workspace-editor/workspaceImagePreview.ts @@ -0,0 +1,18 @@ +const WORKSPACE_IMAGE_EXTENSIONS = new Set([ + "bmp", + "gif", + "ico", + "jpeg", + "jpg", + "png", + "svg", + "webp", +]); + +export function isWorkspaceImagePath(path: string) { + const normalized = path.trim().replace(/\\/g, "/"); + const name = normalized.slice(normalized.lastIndexOf("/") + 1); + const extensionIndex = name.lastIndexOf("."); + if (extensionIndex < 0) return false; + return WORKSPACE_IMAGE_EXTENSIONS.has(name.slice(extensionIndex + 1).toLowerCase()); +} diff --git a/crates/agent-gui/src/i18n/config.ts b/crates/agent-gui/src/i18n/config.ts index 808648e1..01a86bc6 100644 --- a/crates/agent-gui/src/i18n/config.ts +++ b/crates/agent-gui/src/i18n/config.ts @@ -427,6 +427,7 @@ export const translations: Record> = { "projectTools.fileTree.newFile": "新建文件", "projectTools.fileTree.newFolder": "新建文件夹", "projectTools.fileTree.openFile": "打开文件", + "projectTools.fileTree.previewImage": "预览图片", "projectTools.fileTree.rename": "重命名", "projectTools.fileTree.delete": "删除", "projectTools.fileTree.copyPath": "复制路径", @@ -468,6 +469,12 @@ export const translations: Record> = { "此文件有未保存修改。你可以保存、放弃修改,或返回继续编辑。", "workspaceEditor.reloadDirtyTitle": "重新加载前放弃当前修改?", "workspaceEditor.reloadDirtyDescription": "重新加载会用磁盘版本替换当前编辑内容。", + "workspaceImagePreview.loading": "正在加载图片...", + "workspaceImagePreview.title": "图片预览", + "workspaceImagePreview.reload": "重新加载", + "workspaceImagePreview.close": "关闭预览", + "workspaceImagePreview.openFailed": "打开图片失败", + "workspaceImagePreview.empty": "未选择图片", /* ── Settings Nav ── */ "settings.navSystem": "系统设置", @@ -1637,6 +1644,7 @@ export const translations: Record> = { "projectTools.fileTree.newFile": "New File", "projectTools.fileTree.newFolder": "New Folder", "projectTools.fileTree.openFile": "Open File", + "projectTools.fileTree.previewImage": "Preview Image", "projectTools.fileTree.rename": "Rename", "projectTools.fileTree.delete": "Delete", "projectTools.fileTree.copyPath": "Copy Path", @@ -1679,6 +1687,12 @@ export const translations: Record> = { "workspaceEditor.reloadDirtyTitle": "Discard current changes before reloading?", "workspaceEditor.reloadDirtyDescription": "Reloading replaces the current editor contents with the version on disk.", + "workspaceImagePreview.loading": "Loading image...", + "workspaceImagePreview.title": "Image Preview", + "workspaceImagePreview.reload": "Reload", + "workspaceImagePreview.close": "Close preview", + "workspaceImagePreview.openFailed": "Failed to open image", + "workspaceImagePreview.empty": "No image selected", /* ── Settings Nav ── */ "settings.navSystem": "System", diff --git a/crates/agent-gui/src/pages/ChatPage.tsx b/crates/agent-gui/src/pages/ChatPage.tsx index 021e2eb7..5738d592 100644 --- a/crates/agent-gui/src/pages/ChatPage.tsx +++ b/crates/agent-gui/src/pages/ChatPage.tsx @@ -28,6 +28,8 @@ import { SharedHistoryManagerModal } from "../components/chat/SharedHistoryManag import { Ban, PanelRightClose, PanelRightOpen, Terminal, Upload } from "../components/icons"; import { ProjectToolsPanel } from "../components/project-tools/ProjectToolsPanel"; import type { WorkspaceCodeEditorOpenRequest } from "../components/workspace-editor/WorkspaceCodeEditorOverlay"; +import type { WorkspaceImagePreviewOpenRequest } from "../components/workspace-editor/WorkspaceImagePreviewOverlay"; +import { isWorkspaceImagePath } from "../components/workspace-editor/workspaceImagePreview"; import { Button } from "../components/ui/button"; import { useConfirmDialog } from "../components/ui/confirm-dialog"; import { useLocale } from "../i18n"; @@ -199,6 +201,13 @@ const WorkspaceCodeEditorOverlay = lazy(async () => { }; }); +const WorkspaceImagePreviewOverlay = lazy(async () => { + const module = await import("../components/workspace-editor/WorkspaceImagePreviewOverlay"); + return { + default: module.WorkspaceImagePreviewOverlay, + }; +}); + type ChatPageProps = { settings: AppSettings; setSettings: (updater: (prev: AppSettings) => AppSettings) => void; @@ -690,9 +699,7 @@ export function ChatPage(props: ChatPageProps) { const [sidebarOpen, setSidebarOpen] = useState(true); const [activeView, setActiveView] = useState<"chat" | "skills-hub" | "mcp-hub">("chat"); const [projectToolsPanelOpen, setProjectToolsPanelOpen] = useState(false); - const projectToolsFileTreeOpenCount = - settings.customSettings.projectToolsFileTree.openProjectPathKeys.length; - const previousProjectToolsFileTreeOpenCountRef = useRef(projectToolsFileTreeOpenCount); + const previousProjectToolsFileTreeOpenRef = useRef(false); const [workspaceEditorMounted, setWorkspaceEditorMounted] = useState(false); const [workspaceEditorOpen, setWorkspaceEditorOpen] = useState(false); const [workspaceEditorCleanupPending, setWorkspaceEditorCleanupPending] = useState(false); @@ -700,6 +707,11 @@ export function ChatPage(props: ChatPageProps) { useState(null); const [workspaceEditorCloseRequestId, setWorkspaceEditorCloseRequestId] = useState(0); const workspaceEditorRequestIdRef = useRef(0); + const [workspaceImagePreviewMounted, setWorkspaceImagePreviewMounted] = useState(false); + const [workspaceImagePreviewOpen, setWorkspaceImagePreviewOpen] = useState(false); + const [workspaceImagePreviewOpenRequest, setWorkspaceImagePreviewOpenRequest] = + useState(null); + const workspaceImagePreviewRequestIdRef = useRef(0); const [projectTerminalSessions, setProjectTerminalSessions] = useState([]); const [remoteRuntimeStatus, setRemoteRuntimeStatus] = useState(() => buildFallbackGatewayStatus(settings.remote), @@ -1461,14 +1473,31 @@ export function ChatPage(props: ChatPageProps) { const terminalProjectPathKey = terminalProjectPath ? workspaceProjectPathKey(terminalProjectPath) : ""; + const projectToolsFileTreeOpen = isProjectToolsFileTreeOpen( + settings.customSettings, + terminalProjectPathKey, + ); const terminalDisabledMessage = !isAgentMode ? "Project tools require Agent project mode." : !terminalProjectPath ? "Select a project to use project tools." : undefined; - const handleOpenEditableFile = useCallback( + const handleOpenWorkspaceFile = useCallback( (path: string) => { if (!terminalProjectPath || !terminalProjectPathKey) return; + if (isWorkspaceImagePath(path)) { + workspaceImagePreviewRequestIdRef.current += 1; + setWorkspaceImagePreviewMounted(true); + setWorkspaceImagePreviewOpen(true); + setWorkspaceImagePreviewOpenRequest({ + id: workspaceImagePreviewRequestIdRef.current, + projectPathKey: terminalProjectPathKey, + workdir: terminalProjectPath, + path, + }); + return; + } + setWorkspaceImagePreviewOpen(false); workspaceEditorRequestIdRef.current += 1; setWorkspaceEditorCleanupPending(false); setWorkspaceEditorMounted(true); @@ -1485,22 +1514,35 @@ export function ChatPage(props: ChatPageProps) { const requestWorkspaceEditorClose = useCallback(() => { setWorkspaceEditorCloseRequestId((current) => current + 1); }, []); + const requestWorkspaceImagePreviewClose = useCallback(() => { + setWorkspaceImagePreviewOpen(false); + }, []); + const handleWorkspaceImagePreviewClosed = useCallback(() => { + setWorkspaceImagePreviewOpen(false); + setWorkspaceImagePreviewMounted(false); + setWorkspaceImagePreviewOpenRequest(null); + }, []); useEffect(() => { - const previousOpenCount = previousProjectToolsFileTreeOpenCountRef.current; - previousProjectToolsFileTreeOpenCountRef.current = projectToolsFileTreeOpenCount; - if (projectToolsFileTreeOpenCount > 0 && workspaceEditorCleanupPending) { + const previousOpen = previousProjectToolsFileTreeOpenRef.current; + previousProjectToolsFileTreeOpenRef.current = projectToolsFileTreeOpen; + if (projectToolsFileTreeOpen && workspaceEditorCleanupPending) { setWorkspaceEditorCleanupPending(false); } - if (previousOpenCount > 0 && projectToolsFileTreeOpenCount === 0 && workspaceEditorMounted) { + if (previousOpen && !projectToolsFileTreeOpen && workspaceEditorMounted) { setWorkspaceEditorCleanupPending(true); setWorkspaceEditorOpen(true); requestWorkspaceEditorClose(); } + if (previousOpen && !projectToolsFileTreeOpen && workspaceImagePreviewMounted) { + requestWorkspaceImagePreviewClose(); + } }, [ - projectToolsFileTreeOpenCount, + projectToolsFileTreeOpen, requestWorkspaceEditorClose, + requestWorkspaceImagePreviewClose, workspaceEditorCleanupPending, workspaceEditorMounted, + workspaceImagePreviewMounted, ]); useEffect(() => { if (!terminalProjectPathKey) { @@ -4503,6 +4545,25 @@ export function ChatPage(props: ChatPageProps) { /> ) : null} + {workspaceImagePreviewMounted ? ( + + +
+ {t("workspaceImagePreview.loading")} +
+ + } + > + +
+ ) : null} { composerRef.current?.insertCommitMention(commit); composerRef.current?.focus();