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 desktop/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export default defineConfig({
"**/sidebar-more-unread-overlap.spec.ts",
"**/home-collapsed-top-chrome.spec.ts",
"**/thread-unread.spec.ts",
"**/workspace-rail.spec.ts",
"**/thread-reply-anchor-roleplay.spec.ts",
"**/threadpane-ultrawide.spec.ts",
"**/animated-avatar.spec.ts",
Expand Down
19 changes: 18 additions & 1 deletion desktop/src/app/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,13 @@ import { useDueReminderBadgeCount } from "@/features/reminders/hooks";
import { RemindMeLaterProvider } from "@/features/reminders/ui/RemindMeLaterProvider";
import { useReminderNotifications } from "@/features/reminders/useReminderNotifications";
import { AppSidebar } from "@/features/sidebar/ui/AppSidebar";
import { WorkspaceRail } from "@/features/sidebar/ui/WorkspaceRail";
import { useChannelMutes } from "@/features/sidebar/lib/useChannelMutes";
import { useChannelStars } from "@/features/sidebar/lib/useChannelStars";
import { useWorkspaces } from "@/features/workspaces/useWorkspaces";
import { useApplyTemplate } from "@/features/channel-templates/useApplyTemplate";
import { relayClient } from "@/shared/api/relayClient";
import { useFeatureEnabled } from "@/shared/features";
import { useIdentityQuery } from "@/shared/api/hooks";
import { useRelayAutoHeal } from "@/shared/api/useRelayAutoHeal";
import { useDeferredStartup } from "@/shared/hooks/useDeferredStartup";
Expand All @@ -91,6 +93,7 @@ export function AppShell() {
useWebviewScrollBoundaryLock();

const workspacesHook = useWorkspaces();
const workspaceRailEnabled = useFeatureEnabled("workspaceRail");
const [isAddWorkspaceOpen, setIsAddWorkspaceOpen] = React.useState(false);
const [isChannelManagementOpen, setIsChannelManagementOpen] =
React.useState(false);
Expand Down Expand Up @@ -606,15 +609,29 @@ export function AppShell() {
>
<div
className={cn(
"buzz-huddle-app-surface z-10 flex min-h-0 flex-col overflow-hidden bg-background",
"buzz-huddle-app-surface z-10 flex min-h-0 flex-row overflow-hidden bg-background",
isHuddleDrawerOpen && "buzz-huddle-app-surface-open",
)}
>
{workspaceRailEnabled ? (
<WorkspaceRail
activeWorkspaceId={
workspacesHook.activeWorkspace?.id ?? null
}
onAddWorkspace={() => setIsAddWorkspaceOpen(true)}
onSwitchWorkspace={workspacesHook.switchWorkspace}
workspaces={workspacesHook.workspaces}
/>
) : null}
<SidebarProvider className="min-h-0 flex-1 flex-col overflow-hidden">
{!settingsOpen ? (
<AppTopChrome
canGoBack={canGoBack}
canGoForward={canGoForward}
hasWorkspaceRail={
workspaceRailEnabled &&
workspacesHook.workspaces.length > 1
}
onGoBack={goBack}
onGoForward={goForward}
/>
Expand Down
18 changes: 12 additions & 6 deletions desktop/src/app/AppTopChrome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type AppTopChromeProps = {
canGoForward: boolean;
onGoBack: () => void;
onGoForward: () => void;
hasWorkspaceRail?: boolean;
};

const TOP_CHROME_ICON_BUTTON_CLASS =
Expand Down Expand Up @@ -54,17 +55,22 @@ export function AppTopChrome({
canGoForward,
onGoBack,
onGoForward,
hasWorkspaceRail = false,
}: AppTopChromeProps) {
const topChromeRef = React.useRef<HTMLDivElement>(null);
const isFullscreen = useIsFullscreen();
// On macOS the traffic-light buttons overlay the chrome (see
// `trafficLightPosition` in `tauri.conf.json`), so the nav row clears their
// x-position and shifts to align the nav icon centers with the native dot
// centers. In fullscreen those buttons hide, so use the standard alignment.
const navRowPaddingClass =
isMacPlatform() && !isFullscreen ? "pl-20" : "pl-3";
const navRowAlignmentClass =
isMacPlatform() && !isFullscreen ? "translate-y-[3px]" : null;
// x-position. When the workspace rail is present it already occupies the far
// left, so the nav row only needs to clear the lights past the rail edge
// rather than the full offset. In fullscreen those buttons hide.
const macChrome = isMacPlatform() && !isFullscreen;
const navRowPaddingClass = macChrome
? hasWorkspaceRail
? "pl-8"
: "pl-20"
: "pl-3";
const navRowAlignmentClass = macChrome ? "translate-y-[3px]" : null;

React.useEffect(() => {
const topChrome = topChromeRef.current;
Expand Down
193 changes: 49 additions & 144 deletions desktop/src/features/agent-memory/ui/MemorySection.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
import * as React from "react";
import {
AlertTriangle,
Brain,
ChevronDown,
Copy,
RefreshCw,
} from "lucide-react";
import { AlertTriangle, Brain, ChevronDown, RefreshCw } from "lucide-react";

import { useAgentMemoryGraph } from "@/features/agent-memory/hooks";
import type { MemoryTreeNode } from "@/features/agent-memory/lib/buildMemoryGraph";
Expand All @@ -14,7 +8,6 @@ import { cn } from "@/shared/lib/cn";
import { Button, type ButtonProps } from "@/shared/ui/button";
import { Skeleton } from "@/shared/ui/skeleton";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
import { toast } from "sonner";

const MEMORY_LIST_PREVIEW_LIMIT = 3;

Expand Down Expand Up @@ -268,14 +261,6 @@ function MemoryGraphView({
</p>
) : null}

<div className="mb-1 flex items-center justify-end">
<MemoryCopyButton
label={`Copy all ${entries.length} memories`}
successMessage="Copied memories to clipboard"
value={formatMemoriesForClipboard(entries)}
/>
</div>

<div className="space-y-2" data-testid="agent-memory-list">
{visibleEntries.map((entry) => (
<MemoryEntryAccordion
Expand Down Expand Up @@ -371,66 +356,6 @@ function flattenTreeDescendants(node: MemoryTreeNode): EngramEntry[] {
return entries;
}

function formatMemoryForClipboard(entry: EngramEntry): string {
return `${entry.slug}\n${entry.body}`;
}

function formatMemoriesForClipboard(entries: EngramEntry[]): string {
return entries.map(formatMemoryForClipboard).join("\n\n");
}

function MemoryCopyButton({
label,
successMessage,
value,
}: {
label: string;
successMessage: string;
value: string;
}) {
const [copying, setCopying] = React.useState(false);

const handleCopy = React.useCallback(
async (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
event.stopPropagation();
setCopying(true);

try {
await navigator.clipboard.writeText(value);
toast.success(successMessage);
} catch (error) {
console.error("Failed to copy memory", error);
toast.error("Failed to copy memory");
} finally {
setCopying(false);
}
},
[successMessage, value],
);

return (
<Tooltip>
<TooltipTrigger asChild>
<Button
aria-label={label}
className="h-7 w-7 text-muted-foreground hover:text-foreground"
data-testid="agent-memory-copy"
disabled={copying}
onClick={handleCopy}
size="icon"
type="button"
variant="ghost"
>
<Copy className="h-4 w-4" />
<span className="sr-only">{label}</span>
</Button>
</TooltipTrigger>
<TooltipContent>{label}</TooltipContent>
</Tooltip>
);
}

const MEMORY_REF_PATTERN = /\[\[([^\]]+)\]\]/g;

function MemoryBodyText({ body }: { body: string }) {
Expand Down Expand Up @@ -579,33 +504,43 @@ function MemoryEntryAccordion({
}, [open]);

const content = (
<div className="min-w-0 flex-1">
<div
className={cn(
"text-sm font-semibold text-foreground",
!open && "line-clamp-2",
)}
ref={titleRef}
>
{hasDanglingRefs ? (
<AlertTriangle className="mr-1 inline-block h-4 w-4 align-[-2px] text-warning" />
) : null}
<MemorySlugTitle slug={entry.slug} />
</div>
<div
className={cn(
"mt-1 text-xs leading-5 text-foreground/70",
open ? "whitespace-pre-wrap wrap-break-word" : "line-clamp-2",
)}
ref={bodyRef}
>
{isEmpty ? (
<span className="italic text-foreground/50">(empty)</span>
) : (
<MemoryBodyText body={entry.body} />
)}
<>
<div className="min-w-0 flex-1">
<div
className={cn(
"text-sm font-semibold text-foreground",
!open && "line-clamp-2",
)}
ref={titleRef}
>
{hasDanglingRefs ? (
<AlertTriangle className="mr-1 inline-block h-4 w-4 align-[-2px] text-warning" />
) : null}
<MemorySlugTitle slug={entry.slug} />
</div>
<div
className={cn(
"mt-1 text-xs leading-5 text-foreground/70",
open ? "whitespace-pre-wrap wrap-break-word" : "line-clamp-2",
)}
ref={bodyRef}
>
{isEmpty ? (
<span className="italic text-foreground/50">(empty)</span>
) : (
<MemoryBodyText body={entry.body} />
)}
</div>
</div>
</div>
{canExpand ? (
<ChevronDown
className={cn(
"mt-0.5 h-4 w-4 shrink-0 text-muted-foreground transition-transform",
open && "rotate-180",
)}
/>
) : null}
</>
);

return (
Expand All @@ -614,49 +549,19 @@ function MemoryEntryAccordion({
ref={articleRef}
>
{canExpand ? (
<div className="flex items-start gap-2 px-4 py-3 transition-colors hover:bg-muted/50">
<button
aria-expanded={open}
className="min-w-0 flex-1 text-left"
onClick={() => setOpen((value) => !value)}
type="button"
>
{content}
{hasDanglingRefs && open ? (
<MemoryDanglingRefsHint slugs={danglingRefsForEntry} />
) : null}
</button>
<MemoryCopyButton
label={`Copy ${entry.slug} memory`}
successMessage="Copied memory to clipboard"
value={formatMemoryForClipboard(entry)}
/>
<Button
aria-expanded={open}
aria-label={open ? "Collapse memory" : "Expand memory"}
className="h-7 w-7 text-muted-foreground hover:text-foreground"
onClick={() => setOpen((value) => !value)}
size="icon"
type="button"
variant="ghost"
>
<ChevronDown
className={cn(
"h-4 w-4 transition-transform",
open && "rotate-180",
)}
/>
</Button>
</div>
<button
aria-expanded={open}
className="w-full px-4 py-3 text-left transition-colors hover:bg-muted/50"
onClick={() => setOpen((value) => !value)}
type="button"
>
<div className="flex items-start gap-3">{content}</div>
{hasDanglingRefs && open ? (
<MemoryDanglingRefsHint slugs={danglingRefsForEntry} />
) : null}
</button>
) : (
<div className="flex items-start gap-2 px-4 py-3">
{content}
<MemoryCopyButton
label={`Copy ${entry.slug} memory`}
successMessage="Copied memory to clipboard"
value={formatMemoryForClipboard(entry)}
/>
</div>
<div className="flex items-start gap-3 px-4 py-3">{content}</div>
)}
</article>
);
Expand Down
Loading
Loading