feat: improve agent workspace flows and directory suggestions

This commit is contained in:
Mohamed Boudra
2026-02-16 23:19:01 +07:00
parent 22da0951d5
commit 170ee27813
49 changed files with 3053 additions and 293 deletions
@@ -216,9 +216,6 @@ export function useCommandCenter() {
const handleSelectAgent = useCallback(
(agent: AggregatedAgent) => {
didNavigateRef.current = true;
const session = useSessionStore.getState().sessions[agent.serverId];
session?.client?.clearAgentAttention(agent.id);
const shouldReplace = Boolean(parseHostAgentRouteFromPathname(pathname));
const navigate = shouldReplace ? router.replace : router.push;
+39 -1
View File
@@ -1,5 +1,6 @@
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { Platform } from "react-native";
import { getIsTauriMac } from "@/constants/layout";
import { useAggregatedAgents } from "./use-aggregated-agents";
type FaviconStatus = "none" | "running" | "attention";
@@ -34,6 +35,21 @@ function deriveFaviconStatus(
return "none";
}
function deriveMacDockBadgeCount(
agents: ReturnType<typeof useAggregatedAgents>["agents"]
): number | undefined {
const attentionCount = agents.filter(
(agent) =>
agent.requiresAttention &&
(agent.attentionReason === "permission" || agent.attentionReason === "finished")
).length;
if (attentionCount > 0) {
return attentionCount;
}
return undefined;
}
function getFaviconUri(status: FaviconStatus, colorScheme: ColorScheme): string {
const image = FAVICON_IMAGES[colorScheme][status];
if (typeof image === "object" && "uri" in image) {
@@ -73,9 +89,25 @@ function getSystemColorScheme(): ColorScheme {
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
async function updateMacDockBadge(count?: number) {
if (Platform.OS !== "web" || typeof window === "undefined" || !getIsTauriMac()) return;
const tauriWindow = (window as any).__TAURI__?.window?.getCurrentWindow?.();
if (!tauriWindow || typeof tauriWindow.setBadgeCount !== "function") {
return;
}
try {
await tauriWindow.setBadgeCount(count);
} catch (error) {
console.warn("[useFaviconStatus] Failed to update macOS dock badge", error);
}
}
export function useFaviconStatus() {
const { agents } = useAggregatedAgents();
const [colorScheme, setColorScheme] = useState<ColorScheme>(getSystemColorScheme);
const lastDockBadgeCountRef = useRef<number | undefined>(undefined);
// Listen for system color scheme changes
useEffect(() => {
@@ -96,5 +128,11 @@ export function useFaviconStatus() {
const status = deriveFaviconStatus(agents);
updateFavicon(status, colorScheme);
const dockBadgeCount = deriveMacDockBadgeCount(agents);
if (dockBadgeCount !== lastDockBadgeCountRef.current) {
lastDockBadgeCountRef.current = dockBadgeCount;
void updateMacDockBadge(dockBadgeCount);
}
}, [agents, colorScheme]);
}
+198 -60
View File
@@ -1,4 +1,4 @@
import { useState, useCallback, useRef, useEffect } from "react";
import { useState, useRef, useEffect } from "react";
import { Platform } from "react-native";
import type { ImageAttachment } from "@/components/message-input";
@@ -13,11 +13,75 @@ interface UseFileDropZoneReturn {
}
const IS_WEB = Platform.OS === "web";
const IMAGE_MIME_BY_EXTENSION: Record<string, string> = {
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".gif": "image/gif",
".webp": "image/webp",
".bmp": "image/bmp",
".svg": "image/svg+xml",
".heic": "image/heic",
".heif": "image/heif",
".avif": "image/avif",
".tif": "image/tiff",
".tiff": "image/tiff",
};
type TauriDragDropPayload =
| {
type: "enter";
paths: string[];
}
| {
type: "over";
}
| {
type: "drop";
paths: string[];
}
| {
type: "leave";
};
type TauriDragDropEvent = {
payload: TauriDragDropPayload;
};
function isImageFile(file: File): boolean {
return file.type.startsWith("image/");
}
function isTauriEnvironment(): boolean {
return typeof window !== "undefined" && (window as any).__TAURI__ !== undefined;
}
function getFileExtension(path: string): string {
const normalizedPath = path.split("#", 1)[0]?.split("?", 1)[0] ?? path;
const extensionIndex = normalizedPath.lastIndexOf(".");
if (extensionIndex < 0) {
return "";
}
return normalizedPath.slice(extensionIndex).toLowerCase();
}
function isImagePath(path: string): boolean {
return getFileExtension(path) in IMAGE_MIME_BY_EXTENSION;
}
function filePathToImageAttachment(path: string): ImageAttachment {
const extension = getFileExtension(path);
const mimeType = IMAGE_MIME_BY_EXTENSION[extension] ?? "image/jpeg";
const convertFileSrc = (window as any).__TAURI__?.core?.convertFileSrc;
const uri =
typeof convertFileSrc === "function" ? convertFileSrc(path) : path;
return {
uri,
mimeType,
};
}
async function fileToImageAttachment(file: File): Promise<ImageAttachment> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
@@ -62,78 +126,152 @@ export function useFileDropZone({
useEffect(() => {
if (!IS_WEB) return;
const element = containerRef.current;
if (!element) return;
let disposed = false;
let cleanup: (() => void) | undefined;
function handleDragEnter(e: DragEvent) {
e.preventDefault();
e.stopPropagation();
if (disabled) return;
dragCounterRef.current++;
if (e.dataTransfer?.types.includes("Files")) {
setIsDragging(true);
async function setupTauriDragDrop(): Promise<boolean> {
if (!isTauriEnvironment()) {
return false;
}
}
function handleDragOver(e: DragEvent) {
e.preventDefault();
e.stopPropagation();
if (disabled) return;
if (e.dataTransfer) {
e.dataTransfer.dropEffect = "copy";
const tauriWindow = (window as any).__TAURI__?.window?.getCurrentWindow?.();
if (!tauriWindow || typeof tauriWindow.onDragDropEvent !== "function") {
return false;
}
}
function handleDragLeave(e: DragEvent) {
e.preventDefault();
e.stopPropagation();
if (disabled) return;
dragCounterRef.current--;
if (dragCounterRef.current === 0) {
setIsDragging(false);
}
}
async function handleDrop(e: DragEvent) {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
dragCounterRef.current = 0;
if (disabled) return;
const files = Array.from(e.dataTransfer?.files ?? []);
const imageFiles = files.filter(isImageFile);
if (imageFiles.length === 0) return;
try {
const attachments = await Promise.all(
imageFiles.map(fileToImageAttachment)
const unlisten = await tauriWindow.onDragDropEvent(
(event: TauriDragDropEvent) => {
const payload = event.payload;
if (payload.type === "leave") {
setIsDragging(false);
return;
}
if (payload.type === "enter" || payload.type === "over") {
if (!disabled) {
setIsDragging(true);
}
return;
}
// Drop always ends the current drag operation.
setIsDragging(false);
if (disabled) return;
const imagePaths = payload.paths.filter(isImagePath);
if (imagePaths.length === 0) {
return;
}
const attachments = imagePaths.map(filePathToImageAttachment);
onFilesDroppedRef.current(attachments);
}
);
onFilesDroppedRef.current(attachments);
if (disposed) {
unlisten();
return true;
}
cleanup = unlisten;
return true;
} catch (error) {
console.error("[useFileDropZone] Failed to process dropped files:", error);
console.warn("[useFileDropZone] Failed to listen for Tauri drag-drop:", error);
return false;
}
}
element.addEventListener("dragenter", handleDragEnter);
element.addEventListener("dragover", handleDragOver);
element.addEventListener("dragleave", handleDragLeave);
element.addEventListener("drop", handleDrop);
function setupDomDragDrop() {
const element = containerRef.current;
if (!element) {
return;
}
function handleDragEnter(e: DragEvent) {
e.preventDefault();
e.stopPropagation();
if (disabled) return;
dragCounterRef.current++;
if (e.dataTransfer?.types.includes("Files")) {
setIsDragging(true);
}
}
function handleDragOver(e: DragEvent) {
e.preventDefault();
e.stopPropagation();
if (disabled) return;
if (e.dataTransfer) {
e.dataTransfer.dropEffect = "copy";
}
}
function handleDragLeave(e: DragEvent) {
e.preventDefault();
e.stopPropagation();
if (disabled) return;
dragCounterRef.current--;
if (dragCounterRef.current === 0) {
setIsDragging(false);
}
}
async function handleDrop(e: DragEvent) {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
dragCounterRef.current = 0;
if (disabled) return;
const files = Array.from(e.dataTransfer?.files ?? []);
const imageFiles = files.filter(isImageFile);
if (imageFiles.length === 0) return;
try {
const attachments = await Promise.all(
imageFiles.map(fileToImageAttachment)
);
onFilesDroppedRef.current(attachments);
} catch (error) {
console.error("[useFileDropZone] Failed to process dropped files:", error);
}
}
element.addEventListener("dragenter", handleDragEnter);
element.addEventListener("dragover", handleDragOver);
element.addEventListener("dragleave", handleDragLeave);
element.addEventListener("drop", handleDrop);
cleanup = () => {
element.removeEventListener("dragenter", handleDragEnter);
element.removeEventListener("dragover", handleDragOver);
element.removeEventListener("dragleave", handleDragLeave);
element.removeEventListener("drop", handleDrop);
};
}
void (async () => {
const tauriListenersAttached = await setupTauriDragDrop();
if (disposed || tauriListenersAttached) {
return;
}
setupDomDragDrop();
})();
return () => {
element.removeEventListener("dragenter", handleDragEnter);
element.removeEventListener("dragover", handleDragOver);
element.removeEventListener("dragleave", handleDragLeave);
element.removeEventListener("drop", handleDrop);
disposed = true;
cleanup?.();
};
}, [disabled]);