Skip to content
64 changes: 58 additions & 6 deletions desktop/src/app/AppShell.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ChevronLeft, ChevronRight } from "lucide-react";
import * as React from "react";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { useQueryClient } from "@tanstack/react-query";
import { Outlet, useLocation } from "@tanstack/react-router";

Expand Down Expand Up @@ -68,6 +69,22 @@ const LazySettingsScreen = React.lazy(async () => {
return { default: module.SettingsScreen };
});

const WINDOW_DRAG_HANDLE_HEIGHT = 44;
const WINDOW_DRAG_INTERACTIVE_SELECTOR =
'button, a, input, textarea, select, [role="button"], [contenteditable="true"]';

function isWindowDragHandleEvent(event: MouseEvent | PointerEvent) {
if (event.clientY > WINDOW_DRAG_HANDLE_HEIGHT) {
return false;
}

const target = event.target;
return !(
target instanceof Element &&
target.closest(WINDOW_DRAG_INTERACTIVE_SELECTOR)
);
}

function toSearchHit(target: DesktopNotificationTarget): SearchHit | null {
if (!target.eventId) {
return null;
Expand Down Expand Up @@ -468,6 +485,36 @@ export function AppShell() {
};
}, [handleCloseSettings, handleOpenSettings, settingsOpen]);

React.useEffect(() => {
function handlePointerDown(event: PointerEvent) {
if (event.button !== 0 || event.detail > 1) {
return;
}

if (!isWindowDragHandleEvent(event)) {
return;
}

void getCurrentWindow().startDragging();
}

function handleDoubleClick(event: MouseEvent) {
if (event.button !== 0 || !isWindowDragHandleEvent(event)) {
return;
}

event.preventDefault();
void getCurrentWindow().toggleMaximize();
}

window.addEventListener("pointerdown", handlePointerDown, true);
window.addEventListener("dblclick", handleDoubleClick, true);
return () => {
window.removeEventListener("pointerdown", handlePointerDown, true);
window.removeEventListener("dblclick", handleDoubleClick, true);
};
}, []);

return (
<PreventSleepProvider>
<ChannelNavigationProvider channels={channels}>
Expand All @@ -482,29 +529,34 @@ export function AppShell() {
<HuddleProvider>
<div className="flex h-dvh flex-col overflow-hidden overscroll-none">
<SidebarProvider className="min-h-0 flex-1 overflow-hidden">
<div className="fixed left-[80px] top-[8px] z-50 flex items-center gap-1.5">
<SidebarTrigger className="h-6 w-6 text-muted-foreground/70 hover:bg-muted/60 hover:text-foreground" />
<div
aria-hidden="true"
className="fixed inset-x-0 top-0 z-20 h-10 cursor-default select-none"
data-tauri-drag-region
/>
<div className="fixed left-[80px] top-[9px] z-50 flex items-center gap-0.5">
<SidebarTrigger className="h-[22px] w-[22px] text-muted-foreground/70 hover:bg-muted/60 hover:text-foreground" />
<Button
aria-label="Go back"
className="h-6 w-6 text-muted-foreground/70 hover:bg-muted/60 hover:text-foreground"
className="h-[22px] w-[22px] text-muted-foreground/70 hover:bg-muted/60 hover:text-foreground"
data-testid="global-back"
disabled={!canGoBack}
onClick={goBack}
size="icon"
variant="ghost"
>
<ChevronLeft className="h-3.5 w-3.5" />
<ChevronLeft className="h-3 w-3" />
</Button>
<Button
aria-label="Go forward"
className="h-6 w-6 text-muted-foreground/70 hover:bg-muted/60 hover:text-foreground"
className="h-[22px] w-[22px] text-muted-foreground/70 hover:bg-muted/60 hover:text-foreground"
data-testid="global-forward"
disabled={!canGoForward}
onClick={goForward}
size="icon"
variant="ghost"
>
<ChevronRight className="h-3.5 w-3.5" />
<ChevronRight className="h-3 w-3" />
</Button>
</div>
<div className="fixed right-[16px] top-[8px] z-50">
Expand Down
9 changes: 5 additions & 4 deletions desktop/src/features/agents/ui/AgentsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@ export function AgentsScreen() {
<ChatHeader
description="Choose personas from Persona Catalog, create local ACP workers, and monitor the relay-visible agent directory."
mode="agents"
overlaysContent
title="Agents"
/>

<React.Suspense fallback={<ViewLoadingFallback kind="agents" />}>
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
<React.Suspense fallback={<ViewLoadingFallback kind="agents" />}>
<AgentsView />
</div>
</React.Suspense>
</React.Suspense>
</div>
</>
);
}
2 changes: 1 addition & 1 deletion desktop/src/features/agents/ui/AgentsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export function AgentsView() {

return (
<>
<div className="flex-1 overflow-y-auto overflow-x-hidden overscroll-contain px-4 py-4 sm:px-6">
<div className="flex-1 overflow-y-auto overflow-x-hidden overscroll-contain px-4 pb-4 pt-14 sm:px-6">
<div className="mx-auto flex w-full max-w-6xl flex-col gap-6">
<div className="flex flex-col gap-6">
<PersonasSection
Expand Down
17 changes: 9 additions & 8 deletions desktop/src/features/channels/ui/ChannelMembersBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,10 @@ export function ChannelMembersBar({

return (
<React.Fragment>
<div className="flex items-center gap-2">
<div className="flex items-center gap-1">
<Button
aria-label="Add agent"
className="h-9 w-9 rounded-full"
className="h-7 w-7 rounded-full"
data-testid="channel-add-bot-trigger"
disabled={!canAddAgents}
onClick={() => {
Expand All @@ -99,10 +99,11 @@ export function ChannelMembersBar({
type="button"
variant="outline"
>
<Plus className="h-4 w-4" />
<Plus className="h-3 w-3" />
</Button>

<HuddleIndicator
className="h-7 w-7"
channelId={channel.id}
onStart={async () => {
try {
Expand All @@ -119,28 +120,28 @@ export function ChannelMembersBar({

<Button
aria-label={`View channel members (${memberCount})`}
className="h-9 gap-1.5 rounded-full px-3"
className="h-7 gap-1 rounded-full px-2"
data-testid="channel-members-trigger"
onClick={onToggleMembers}
type="button"
variant="outline"
>
<Users className="h-4 w-4" />
<span className="min-w-[1ch] text-sm font-medium tabular-nums">
<Users className="h-3 w-3" />
<span className="min-w-[1ch] text-[11px] font-medium tabular-nums">
{memberCount}
</span>
</Button>

<Button
aria-label="Manage channel"
className="h-9 w-9 rounded-full"
className="h-7 w-7 rounded-full"
data-testid="channel-management-trigger"
onClick={onManageChannel}
size="icon"
type="button"
variant="outline"
>
<Settings2 className="h-4 w-4" />
<Settings2 className="h-3 w-3" />
</Button>
</div>

Expand Down
1 change: 1 addition & 0 deletions desktop/src/features/channels/ui/ChannelScreenHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export function ChannelScreenHeader({
}
channelType={activeChannel?.channelType}
description={getChannelDescription(activeChannel)}
overlaysContent
statusBadge={
<ChannelHeaderStatusBadge
channelType={activeChannel?.channelType}
Expand Down
4 changes: 2 additions & 2 deletions desktop/src/features/channels/ui/EphemeralChannelBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ export function EphemeralChannelBadge({
className={cn(
"inline-flex items-center gap-1 rounded-full font-medium text-slate-500 dark:text-slate-400",
isHeader
? "h-5 w-5 justify-center border border-sky-500/20 bg-sky-500/5 p-0 text-xs"
? "h-4 w-4 justify-center border border-border/70 bg-muted/35 p-0 text-[11px] text-muted-foreground"
: "shrink-0 h-4 w-4 justify-center border border-sky-500/15 bg-slate-500/5 p-0 text-slate-500/80 dark:text-slate-400/80",
)}
data-testid={testId}
title={display.tooltipLabel}
>
<Clock className={cn(isHeader ? "h-3 w-3" : "h-2.5 w-2.5")} />
<Clock className={cn(isHeader ? "h-2 w-2" : "h-2.5 w-2.5")} />
</span>
);
}
36 changes: 24 additions & 12 deletions desktop/src/features/chat/ui/ChatHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
import type * as React from "react";

import type { ChannelType, ChannelVisibility } from "@/shared/api/types";
import { cn } from "@/shared/lib/cn";
import { useSidebar } from "@/shared/ui/sidebar";

type ChatHeaderProps = {
actions?: React.ReactNode;
Expand All @@ -19,9 +21,12 @@ type ChatHeaderProps = {
channelType?: ChannelType;
visibility?: ChannelVisibility;
mode?: "home" | "channel" | "agents" | "workflows" | "pulse";
overlaysContent?: boolean;
statusBadge?: React.ReactNode;
};

const HEADER_ICON_CLASS = "h-3.5 w-3.5 text-muted-foreground";

function ChannelIcon({
channelType,
visibility,
Expand All @@ -32,34 +37,34 @@ function ChannelIcon({
mode?: "home" | "channel" | "agents" | "workflows" | "pulse";
}) {
if (mode === "home") {
return <Home className="h-5 w-5 text-primary" />;
return <Home className={HEADER_ICON_CLASS} />;
}

if (mode === "agents") {
return <Bot className="h-5 w-5 text-primary" />;
return <Bot className={HEADER_ICON_CLASS} />;
}

if (mode === "workflows") {
return <Zap className="h-5 w-5 text-primary" />;
return <Zap className={HEADER_ICON_CLASS} />;
}

if (mode === "pulse") {
return <Activity className="h-5 w-5 text-primary" />;
return <Activity className={HEADER_ICON_CLASS} />;
}

if (channelType === "dm") {
return <CircleDot className="h-5 w-5 text-primary" />;
return <CircleDot className={HEADER_ICON_CLASS} />;
}

if (visibility === "private") {
return <Lock className="h-5 w-5 text-primary" />;
return <Lock className={HEADER_ICON_CLASS} />;
}

if (channelType === "forum") {
return <FileText className="h-5 w-5 text-primary" />;
return <FileText className={HEADER_ICON_CLASS} />;
}

return <Hash className="h-5 w-5 text-primary" />;
return <Hash className={HEADER_ICON_CLASS} />;
}

export function ChatHeader({
Expand All @@ -69,32 +74,39 @@ export function ChatHeader({
channelType,
visibility,
mode = "channel",
overlaysContent = false,
statusBadge,
}: ChatHeaderProps) {
const trimmedDescription = description.trim();
const { state: sidebarState } = useSidebar();
const reserveGlobalControls = sidebarState === "collapsed";

return (
<header
className="relative z-20 flex min-w-0 shrink-0 items-center gap-3 bg-background/25 px-4 pb-2 pt-6 shadow-[0_4px_24px_rgba(0,0,0,0.06)] backdrop-blur-xl supports-[backdrop-filter]:bg-background/20 dark:shadow-[0_4px_24px_rgba(0,0,0,0.25)] sm:px-6"
className={cn(
"relative z-30 flex min-h-11 min-w-0 shrink-0 cursor-default select-none items-center gap-2.5 bg-background/70 py-1.5 pl-4 pr-2 shadow-[0_4px_24px_rgba(0,0,0,0.06)] backdrop-blur-xl transition-[margin,padding] duration-200 ease-linear supports-[backdrop-filter]:bg-background/55 dark:shadow-[0_4px_24px_rgba(0,0,0,0.25)] sm:pl-6 sm:pr-3",
overlaysContent && "-mb-11",
reserveGlobalControls && "md:pl-40",
)}
data-testid="chat-header"
data-tauri-drag-region
>
<div className="min-w-0 flex-1">
<div className="flex min-w-0 flex-wrap items-center gap-2">
<div className="flex min-w-0 flex-wrap items-center gap-1">
<ChannelIcon
channelType={channelType}
mode={mode}
visibility={visibility}
/>
<h1
className="min-w-0 truncate text-lg font-semibold tracking-tight"
className="min-w-0 truncate text-sm font-semibold leading-5 tracking-tight"
data-testid="chat-title"
title={trimmedDescription || undefined}
>
{title}
</h1>
{statusBadge ? (
<div className="flex shrink-0 flex-wrap items-center gap-2">
<div className="flex shrink-0 flex-wrap items-center gap-1">
{statusBadge}
</div>
) : null}
Expand Down
1 change: 1 addition & 0 deletions desktop/src/features/home/ui/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export function HomeScreen({
<ChatHeader
description="Personalized activity feed for mentions, reminders, channel activity, and agent work."
mode="home"
overlaysContent
title="Home"
/>

Expand Down
6 changes: 3 additions & 3 deletions desktop/src/features/home/ui/HomeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ type FeedFilter =

function HomeLoadingState() {
return (
<div className="flex-1 overflow-y-auto overflow-x-hidden overscroll-contain px-4 py-3 sm:px-6">
<div className="flex-1 overflow-y-auto overflow-x-hidden overscroll-contain px-4 pb-3 pt-14 sm:px-6">
<div className="mx-auto flex w-full max-w-5xl flex-col gap-4">
<div className="grid gap-4">
{["mentions", "actions"].map((section) => (
Expand Down Expand Up @@ -138,7 +138,7 @@ export function HomeView({

if (!feed) {
return (
<div className="flex-1 overflow-y-auto overflow-x-hidden overscroll-contain px-4 py-3 sm:px-6">
<div className="flex-1 overflow-y-auto overflow-x-hidden overscroll-contain px-4 pb-3 pt-14 sm:px-6">
<div className="mx-auto flex w-full max-w-3xl flex-col gap-4">
<div className="rounded-md border border-destructive/30 bg-destructive/5 px-4 py-5">
<p className="text-base font-semibold tracking-tight">
Expand All @@ -162,7 +162,7 @@ export function HomeView({
const showActivity = filter === "all" || filter === "activity";
const showAgentActivity = filter === "all" || filter === "agent_activity";
return (
<div className="flex-1 overflow-y-auto overflow-x-hidden overscroll-contain px-4 py-3 sm:px-6">
<div className="flex-1 overflow-y-auto overflow-x-hidden overscroll-contain px-4 pb-3 pt-14 sm:px-6">
<div className="mx-auto flex w-full max-w-5xl flex-col gap-4">
<div className="flex items-center gap-1.5">
{FILTER_OPTIONS.map((option) => (
Expand Down
Loading
Loading