Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 34 additions & 0 deletions crates/agent-gateway/web/src/app/GatewayApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,11 @@ export default function GatewayApp() {
// Per-conversation runtime workdir (drafts have no persisted summary yet).
const conversationWorkdirsRef = useRef<Map<string, string>>(new Map());
const displayedConversationWorkdirRef = useRef("");
const pendingUploadContextRef = useRef<{
conversationId: string;
workdir: string;
executionMode: string;
} | null>(null);
const displayedConversationBusyRef = useRef(false);
const historyLoadSequenceRef = useRef(0);
const visibleConversationRevisionRef = useRef(0);
Expand Down Expand Up @@ -3070,6 +3075,34 @@ export default function GatewayApp() {
currentConversationRuntimeWorkdir ||
(isAgentMode ? activeWorkspaceProjectPath || settings.system.workdir.trim() : "");
displayedConversationWorkdirRef.current = displayedConversationWorkdir;
// Pending uploads live under their conversation's workdir uploads/ tree.
// Switching conversations keeps every conversation's uploads, but a workdir
// change within the same conversation (a draft switching projects) makes its
// relative paths stale, and a mode flip away from tools invalidates all of
// them — mirroring the GUI-side rule in usePendingUploads.
useEffect(() => {
const executionMode = settings.system.executionMode;
const previous = pendingUploadContextRef.current;
pendingUploadContextRef.current = {
conversationId: displayedConversationId,
workdir: displayedConversationWorkdir,
executionMode,
};
if (!previous) return;
if (previous.executionMode !== executionMode) {
clearPendingUploads();
return;
}
if (previous.conversationId !== displayedConversationId) return;
if (previous.workdir === displayedConversationWorkdir) return;
setPendingUploadsForConversation(displayedConversationId, []);
}, [
clearPendingUploads,
displayedConversationId,
displayedConversationWorkdir,
settings.system.executionMode,
setPendingUploadsForConversation,
]);
useEffect(() => {
if (!api || !displayedConversationId) {
queuedChatTurnsRef.current = [];
Expand Down Expand Up @@ -3517,6 +3550,7 @@ export default function GatewayApp() {
ref={fileInputRef}
type="file"
multiple
aria-label={translate("chat.upload.selectFiles", settings.locale)}
className="gateway-hidden-file-input"
onChange={(event) => {
const files = Array.from(event.currentTarget.files ?? []);
Expand Down
4 changes: 0 additions & 4 deletions crates/agent-gateway/web/src/components/GatewayTranscript.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1373,10 +1373,6 @@ export function GatewayTranscript({
return (
<div className="gateway-transcript-shell">
<div className="gateway-chat-column gateway-empty-state">
<div className="pointer-events-none absolute inset-0 flex items-center justify-center overflow-hidden">
<div className="hero-glow-pulse h-[360px] w-[360px] rounded-full bg-[radial-gradient(closest-side,hsl(var(--foreground)/0.08),transparent_70%)] blur-3xl" />
</div>

<div className="relative flex flex-col items-center">
<div className="hero-entrance hero-icon-float mb-5 flex h-24 w-24 items-center justify-center">
<img
Expand Down
28 changes: 0 additions & 28 deletions crates/agent-gateway/web/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -892,34 +892,6 @@
}
}

/* Glow pulse animation */
.hero-glow-pulse {
animation:
heroGlowPulse 5s ease-in-out infinite,
heroFadeIn 1.2s ease-out both;
}

@keyframes heroGlowPulse {
0%,
100% {
opacity: 0.7;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.12);
}
}

@keyframes heroFadeIn {
from {
opacity: 0;
}
to {
opacity: 0.7;
}
}

/* Mention composer placeholder */
.mention-composer.is-empty::before {
content: attr(data-placeholder);
Expand Down
249 changes: 126 additions & 123 deletions crates/agent-gateway/web/src/pages/chat/ChatComposerBar.tsx

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions crates/agent-gui/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,6 @@ russh-sftp = "2.3.0"

[target.'cfg(target_os = "macos")'.dependencies]
objc2-app-kit = { version = "0.3", default-features = false, features = ["std", "NSButton", "NSControl", "NSView", "NSWindow"] }

[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_Storage_FileSystem"] }
31 changes: 26 additions & 5 deletions crates/agent-gui/src-tauri/src/commands/workspace/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,12 +233,33 @@ fn file_identity(meta: &fs::Metadata, _canon: &Path) -> String {
}

#[cfg(windows)]
fn file_identity(meta: &fs::Metadata, canon: &Path) -> String {
use std::os::windows::fs::MetadataExt;
match (meta.volume_serial_number(), meta.file_index()) {
(Some(volume), Some(index)) => format!("{volume}:{index}"),
_ => format!("path:{}", display_path(canon).to_lowercase()),
fn file_identity(_meta: &fs::Metadata, canon: &Path) -> String {
// std's volume_serial_number()/file_index() are unstable (windows_by_handle,
// rust-lang/rust#63010); query GetFileInformationByHandle directly instead.
fn identity_by_handle(path: &Path) -> Option<String> {
use std::os::windows::fs::OpenOptionsExt;
use std::os::windows::io::AsRawHandle;
use windows_sys::Win32::Storage::FileSystem::{
GetFileInformationByHandle, BY_HANDLE_FILE_INFORMATION, FILE_FLAG_BACKUP_SEMANTICS,
};

// access_mode(0): metadata-only handle, no read permission required.
// FILE_FLAG_BACKUP_SEMANTICS is required to open directories.
let file = fs::OpenOptions::new()
.access_mode(0)
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS)
.open(path)
.ok()?;
let mut info: BY_HANDLE_FILE_INFORMATION = unsafe { std::mem::zeroed() };
if unsafe { GetFileInformationByHandle(file.as_raw_handle() as _, &mut info) } == 0 {
return None;
}
let volume = info.dwVolumeSerialNumber;
let index = (u64::from(info.nFileIndexHigh) << 32) | u64::from(info.nFileIndexLow);
Some(format!("{volume}:{index}"))
}
identity_by_handle(canon)
.unwrap_or_else(|| format!("path:{}", display_path(canon).to_lowercase()))
}

fn levenshtein_at_most(a: &str, b: &str, max: usize) -> bool {
Expand Down
28 changes: 0 additions & 28 deletions crates/agent-gui/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -1093,34 +1093,6 @@
}
}

/* Glow pulse animation */
.hero-glow-pulse {
animation:
heroGlowPulse 5s ease-in-out infinite,
heroFadeIn 1.2s ease-out both;
}

@keyframes heroGlowPulse {
0%,
100% {
opacity: 0.7;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.12);
}
}

@keyframes heroFadeIn {
from {
opacity: 0;
}
to {
opacity: 0.7;
}
}

/* Mention composer placeholder */
.mention-composer.is-empty::before {
content: attr(data-placeholder);
Expand Down
6 changes: 3 additions & 3 deletions crates/agent-gui/src/pages/ChatPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1723,6 +1723,7 @@ export function ChatPage(props: ChatPageProps) {
} = usePendingUploads({
isAgentMode,
workdir: displayedConversationWorkdir,
conversationId: currentConversationId,
currentConversationIdRef,
composerRef,
setErrorMessage,
Expand Down Expand Up @@ -3314,9 +3315,8 @@ export function ChatPage(props: ChatPageProps) {

useEffect(() => {
currentConversationIdRef.current = currentConversationId;
setPendingUploadedFiles(
pendingUploadsByConversationRef.current.get(currentConversationId) ?? [],
);
// Per-conversation pending uploads are restored inside usePendingUploads
// when its conversationId param changes.
}, [currentConversationId]);

useEffect(() => {
Expand Down
Loading
Loading