workspaces to dashboards, imap email sync, ffmpeg tool, tts fix, file browser refresh, automation sidebar reorder

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-02 20:00:45 +00:00
co-authored by Claude Opus 4.6
parent bd362dc586
commit 927267e041
98 changed files with 1176 additions and 802 deletions
@@ -22,6 +22,7 @@ export const GoogleAccount = () => {
const [isLoading, setIsLoading] = useState(true);
const [status, setStatus] = useState<GoogleStatus>({ connected: false, email: null, picture: null, configured: false });
const [lastSyncAt, setLastSyncAt] = useState<string | null>(null);
const [dismissedError, setDismissedError] = useState(false);
const { jobs, createJob, refetch } = useJobs({ type: 'gmail-sync' });
const activeJob = jobs.find((j) => j.status === 'queued' || j.status === 'running');
const lastJob = jobs[0];
@@ -143,11 +144,15 @@ export const GoogleAccount = () => {
</div>
</div>
)}
{!activeJob && lastJob?.status === 'failed' && (
<div className="flex items-center gap-2 text-xs text-red-500">
{!activeJob && lastJob?.status === 'failed' && !dismissedError && (
<button
onClick={() => setDismissedError(true)}
className="flex items-center gap-2 text-xs text-red-500 hover:opacity-70 transition-opacity cursor-pointer text-left"
title="Click to dismiss"
>
<XCircle className="h-3.5 w-3.5 shrink-0" />
Last sync failed{lastJob.error ? `: ${lastJob.error}` : ''}
</div>
</button>
)}
{!activeJob && lastSyncAt && (
<div className="flex items-center gap-2 text-xs text-duck-dark/50 dark:text-foreground/50">
@@ -93,7 +93,7 @@ export const AIModels = () => {
<Label className="grid gap-2">
<span className="text-duck-dark/70 dark:text-foreground/70">Default Project Model</span>
<p className="text-xs text-duck-dark/40 dark:text-foreground/40">Used when starting a new chat inside a project workspace</p>
<p className="text-xs text-duck-dark/40 dark:text-foreground/40">Used when starting a new chat inside a project dashboard</p>
{renderModelSelect(projectModel, setProjectModel, 'Same as chat default')}
</Label>
@@ -11,14 +11,24 @@ type DockPillProps = {
visible: boolean;
onAction: (path: string) => void;
onDragStart: (ev: DragEvent, path: string) => void;
onDropOnPill?: (draggedPath: string) => void;
dropIndicator?: 'left' | 'right' | null;
onDragOverPill?: (ev: DragEvent) => void;
onDragLeavePill?: () => void;
};
const DockPill = ({ label, path, color, visible, onAction, onDragStart }: DockPillProps) => (
const DockPill = ({
label, path, color, visible, onAction, onDragStart, onDropOnPill, dropIndicator, onDragOverPill, onDragLeavePill,
}: DockPillProps) => (
<span
draggable
onDragStart={(ev) => onDragStart(ev, path)}
className="inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-medium cursor-grab active:cursor-grabbing select-none border border-duck-dark/15 dark:border-foreground/15 bg-background/60 text-duck-dark/80 dark:text-foreground/80 hover:bg-duck-dark/5 dark:hover:bg-foreground/5 transition-colors"
onDragOver={onDragOverPill}
onDragLeave={onDragLeavePill}
onDrop={onDropOnPill ? (ev) => { ev.preventDefault(); ev.stopPropagation(); const p = ev.dataTransfer.getData('text/plain'); if (p) onDropOnPill(p); } : undefined}
className={`relative inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-medium cursor-grab active:cursor-grabbing select-none border border-duck-dark/15 dark:border-foreground/15 bg-background/60 text-duck-dark/80 dark:text-foreground/80 hover:bg-duck-dark/5 dark:hover:bg-foreground/5 transition-colors ${dropIndicator === 'left' ? 'ring-l-2 ring-duck-teal' : ''} ${dropIndicator === 'right' ? 'ring-r-2 ring-duck-teal' : ''}`}
>
{dropIndicator === 'left' && <span className="absolute -left-1 top-1 bottom-1 w-0.5 rounded-full bg-duck-teal" />}
<span className="w-2 h-2 rounded-full shrink-0" style={{ background: color }} />
{label}
<button
@@ -27,6 +37,7 @@ const DockPill = ({ label, path, color, visible, onAction, onDragStart }: DockPi
>
{visible ? <X className="h-3 w-3" /> : <Plus className="h-3 w-3" />}
</button>
{dropIndicator === 'right' && <span className="absolute -right-1 top-1 bottom-1 w-0.5 rounded-full bg-duck-teal" />}
</span>
);
@@ -73,6 +84,7 @@ const DropZone = ({ label, children, onDrop }: DropZoneProps) => {
export const DockSettings = () => {
const { items, allItems, setItems, reset } = useDock(ALL_DOCK_ITEMS, DEFAULT_DOCK_PATHS);
const [dropTarget, setDropTarget] = useState<{ path: string; side: 'left' | 'right' } | null>(null);
const visiblePaths = new Set(items.map((i) => i.to));
const hiddenItems = allItems.filter((i) => !visiblePaths.has(i.to));
@@ -97,8 +109,42 @@ export const DockSettings = () => {
[items, setItems],
);
const insertAt = useCallback(
(draggedPath: string, targetPath: string, side: 'left' | 'right') => {
const paths = items.filter((i) => i.to !== draggedPath).map((i) => i.to);
const targetIdx = paths.indexOf(targetPath);
if (targetIdx === -1) return;
const insertIdx = side === 'left' ? targetIdx : targetIdx + 1;
paths.splice(insertIdx, 0, draggedPath);
setItems(paths);
setDropTarget(null);
},
[items, setItems],
);
const handlePillDragOver = useCallback((ev: DragEvent, pillPath: string) => {
ev.preventDefault();
ev.dataTransfer.dropEffect = 'move';
const rect = (ev.currentTarget as HTMLElement).getBoundingClientRect();
const midX = rect.left + rect.width / 2;
const side = ev.clientX < midX ? 'left' : 'right';
setDropTarget((prev) => (prev?.path === pillPath && prev?.side === side ? prev : { path: pillPath, side }));
}, []);
const handlePillDragLeave = useCallback(() => setDropTarget(null), []);
const handlePillDrop = useCallback(
(draggedPath: string, targetPath: string) => {
if (draggedPath === targetPath) { setDropTarget(null); return; }
const side = dropTarget?.path === targetPath ? dropTarget.side : 'right';
insertAt(draggedPath, targetPath, side);
},
[dropTarget, insertAt],
);
const onDropVisible = useCallback(
(path: string) => {
setDropTarget(null);
if (visiblePaths.has(path)) return;
addItem(path);
},
@@ -107,6 +153,7 @@ export const DockSettings = () => {
const onDropHidden = useCallback(
(path: string) => {
setDropTarget(null);
if (!visiblePaths.has(path)) return;
removeItem(path);
},
@@ -126,6 +173,10 @@ export const DockSettings = () => {
visible
onAction={removeItem}
onDragStart={onDragStart}
dropIndicator={dropTarget?.path === item.to ? dropTarget.side : null}
onDragOverPill={(ev) => handlePillDragOver(ev, item.to)}
onDragLeavePill={handlePillDragLeave}
onDropOnPill={(draggedPath) => handlePillDrop(draggedPath, item.to)}
/>
))}
</DropZone>