-
-
Calendar
-
+
diff --git a/apps/desktop/src/sidebar/custom-sidebar-header.test.tsx b/apps/desktop/src/sidebar/custom-sidebar-header.test.tsx
new file mode 100644
index 0000000000..b9d62179e0
--- /dev/null
+++ b/apps/desktop/src/sidebar/custom-sidebar-header.test.tsx
@@ -0,0 +1,110 @@
+import { cleanup, fireEvent, render, screen } from "@testing-library/react";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+
+const mocks = vi.hoisted(() => ({
+ canGoBack: false,
+ canGoNext: false,
+ chatMode: "FloatingClosed",
+ currentTab: { type: "settings" } as { type: string } | null,
+ goBack: vi.fn(),
+ goNext: vi.fn(),
+ openCurrent: vi.fn(),
+ select: vi.fn(),
+ sendEvent: vi.fn(),
+ tabs: [] as { type: string }[],
+}));
+
+vi.mock("~/contexts/shell", () => ({
+ useShell: () => ({
+ chat: {
+ mode: mocks.chatMode,
+ sendEvent: mocks.sendEvent,
+ },
+ }),
+}));
+
+vi.mock("~/store/zustand/tabs", () => ({
+ useTabs: (selector: (state: unknown) => unknown) =>
+ selector({
+ currentTab: mocks.currentTab,
+ canGoBack: mocks.canGoBack,
+ canGoNext: mocks.canGoNext,
+ goBack: mocks.goBack,
+ goNext: mocks.goNext,
+ openCurrent: mocks.openCurrent,
+ select: mocks.select,
+ tabs: mocks.tabs,
+ }),
+}));
+
+import { CustomSidebarHeader } from "./custom-sidebar-header";
+
+describe("CustomSidebarHeader", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ beforeEach(() => {
+ mocks.canGoBack = false;
+ mocks.canGoNext = false;
+ mocks.chatMode = "FloatingClosed";
+ mocks.currentTab = { type: "settings" };
+ mocks.goBack.mockClear();
+ mocks.goNext.mockClear();
+ mocks.openCurrent.mockClear();
+ mocks.select.mockClear();
+ mocks.sendEvent.mockClear();
+ mocks.tabs = [];
+ });
+
+ it("opens home from the back button", () => {
+ render(
);
+
+ fireEvent.click(screen.getByRole("button", { name: "Go home" }));
+
+ expect(mocks.openCurrent).toHaveBeenCalledWith({ type: "empty" });
+ });
+
+ it("selects an existing home tab from the back button", () => {
+ const homeTab = { type: "empty" };
+ mocks.tabs = [homeTab];
+
+ render(
);
+
+ fireEvent.click(screen.getByRole("button", { name: "Go home" }));
+
+ expect(mocks.select).toHaveBeenCalledWith(homeTab);
+ expect(mocks.openCurrent).not.toHaveBeenCalled();
+ });
+
+ it("closes floating chat before opening home", () => {
+ mocks.chatMode = "FloatingOpen";
+
+ render(
);
+
+ fireEvent.click(screen.getByRole("button", { name: "Go home" }));
+
+ expect(mocks.sendEvent).toHaveBeenCalledWith({ type: "CLOSE" });
+ expect(mocks.openCurrent).not.toHaveBeenCalled();
+ });
+
+ it("renders history controls when requested", () => {
+ mocks.canGoBack = true;
+ mocks.canGoNext = true;
+
+ render(
);
+
+ fireEvent.click(screen.getByRole("button", { name: "Go back" }));
+ fireEvent.click(screen.getByRole("button", { name: "Go forward" }));
+
+ expect(mocks.goBack).toHaveBeenCalledTimes(1);
+ expect(mocks.goNext).toHaveBeenCalledTimes(1);
+ });
+
+ it("hides history controls by default", () => {
+ render(
);
+
+ expect(screen.queryByRole("button", { name: "Go back" })).toBeNull();
+ expect(screen.queryByRole("button", { name: "Go forward" })).toBeNull();
+ });
+});
diff --git a/apps/desktop/src/sidebar/custom-sidebar-header.tsx b/apps/desktop/src/sidebar/custom-sidebar-header.tsx
new file mode 100644
index 0000000000..821d22c5b8
--- /dev/null
+++ b/apps/desktop/src/sidebar/custom-sidebar-header.tsx
@@ -0,0 +1,128 @@
+import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react";
+import { useCallback } from "react";
+
+import { cn } from "@hypr/utils";
+
+import { useShell } from "~/contexts/shell";
+import { useTabs } from "~/store/zustand/tabs";
+
+export function CustomSidebarHeader({
+ title,
+ children,
+ showHistoryControls = false,
+}: {
+ title: string;
+ children?: React.ReactNode;
+ showHistoryControls?: boolean;
+}) {
+ const { chat } = useShell();
+ const currentTab = useTabs((state) => state.currentTab);
+ const tabs = useTabs((state) => state.tabs);
+ const select = useTabs((state) => state.select);
+ const openCurrent = useTabs((state) => state.openCurrent);
+ const goBack = useTabs((state) => state.goBack);
+ const goNext = useTabs((state) => state.goNext);
+ const canGoBack = useTabs((state) => state.canGoBack);
+ const canGoNext = useTabs((state) => state.canGoNext);
+
+ const handleBack = useCallback(() => {
+ if (chat.mode === "FloatingOpen") {
+ chat.sendEvent({ type: "CLOSE" });
+ return;
+ }
+
+ if (currentTab?.type === "onboarding" || currentTab?.type === "empty") {
+ return;
+ }
+
+ const existingHomeTab = tabs.find((tab) => tab.type === "empty");
+ if (existingHomeTab) {
+ select(existingHomeTab);
+ return;
+ }
+
+ openCurrent({ type: "empty" });
+ }, [chat, currentTab, openCurrent, select, tabs]);
+
+ return (
+
+
+
+
+
+ {showHistoryControls ? (
+ <>
+
+
+
+
+
+
+ >
+ ) : null}
+
+ {title}
+
+
+ {children ? (
+
+ {children}
+
+ ) : null}
+
+ );
+}
+
+function CustomSidebarHeaderButton({
+ children,
+ disabled = false,
+ label,
+ onClick,
+ title,
+}: {
+ children: React.ReactNode;
+ disabled?: boolean;
+ label: string;
+ onClick: () => void;
+ title?: string;
+}) {
+ return (
+
+ );
+}
diff --git a/apps/desktop/src/sidebar/index.test.tsx b/apps/desktop/src/sidebar/index.test.tsx
new file mode 100644
index 0000000000..21a23a4a43
--- /dev/null
+++ b/apps/desktop/src/sidebar/index.test.tsx
@@ -0,0 +1,110 @@
+import { render, screen } from "@testing-library/react";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+const mocks = vi.hoisted(() => ({
+ currentTab: { type: "empty" } as { type: string } | null,
+ showDevtool: false,
+ sidebarTimelineEnabled: false,
+}));
+
+vi.mock("~/contexts/shell", () => ({
+ useShell: () => ({
+ leftsidebar: {
+ showDevtool: mocks.showDevtool,
+ },
+ }),
+}));
+
+vi.mock("~/shared/config", () => ({
+ useConfigValue: () => mocks.sidebarTimelineEnabled,
+}));
+
+vi.mock("~/store/zustand/tabs", () => ({
+ useTabs: (
+ selector: (state: { currentTab: typeof mocks.currentTab }) => unknown,
+ ) => selector({ currentTab: mocks.currentTab }),
+}));
+
+vi.mock("~/sidebar/timeline", () => ({
+ TimelineView: ({
+ showOpenCalendarButton = true,
+ topChromeInset = false,
+ }: {
+ showOpenCalendarButton?: boolean;
+ topChromeInset?: boolean;
+ }) => (
+
+ ),
+}));
+
+vi.mock("~/sidebar/toast", () => ({
+ ToastArea: () =>
,
+}));
+
+vi.mock("~/sidebar/calendar", () => ({
+ CalendarNav: () =>
,
+}));
+
+vi.mock("~/sidebar/contacts", () => ({
+ ContactsNav: () =>
,
+}));
+
+vi.mock("~/sidebar/settings", () => ({
+ SettingsNav: () =>
,
+}));
+
+vi.mock("~/sidebar/templates", () => ({
+ TemplatesNav: () =>
,
+}));
+
+vi.mock("~/sidebar/devtool", () => ({
+ DevtoolView: () =>
,
+}));
+
+import { LeftSidebar } from "./index";
+
+describe("LeftSidebar", () => {
+ beforeEach(() => {
+ mocks.currentTab = { type: "empty" };
+ mocks.showDevtool = false;
+ mocks.sidebarTimelineEnabled = false;
+ });
+
+ it("uses the timeline layout without a duplicate sidebar top offset", () => {
+ mocks.sidebarTimelineEnabled = true;
+
+ const { container } = render(
);
+
+ expect(screen.getByTestId("timeline-view")).toBeTruthy();
+ expect(
+ screen
+ .getByTestId("timeline-view")
+ .getAttribute("data-show-open-calendar-button"),
+ ).toBe("true");
+ expect(
+ screen.getByTestId("timeline-view").getAttribute("data-top-chrome-inset"),
+ ).toBe("true");
+ expect(container.firstElementChild?.className).toContain("pt-0");
+ });
+
+ it.each([
+ ["settings", "settings-nav"],
+ ["calendar", "calendar-nav"],
+ ["contacts", "contacts-nav"],
+ ["templates", "templates-nav"],
+ ])("keeps %s below the window chrome", (type, testId) => {
+ mocks.sidebarTimelineEnabled = true;
+ mocks.currentTab = { type };
+
+ const { container } = render(
);
+ const classList = container.firstElementChild?.className.split(" ") ?? [];
+
+ expect(screen.getByTestId(testId)).toBeTruthy();
+ expect(classList).toContain("pt-11");
+ expect(classList).not.toContain("pt-0");
+ });
+});
diff --git a/apps/desktop/src/sidebar/index.tsx b/apps/desktop/src/sidebar/index.tsx
index e392f264f5..476413fed7 100644
--- a/apps/desktop/src/sidebar/index.tsx
+++ b/apps/desktop/src/sidebar/index.tsx
@@ -1,5 +1,7 @@
import { lazy, Suspense } from "react";
+import { cn } from "@hypr/utils";
+
import { CalendarNav } from "./calendar";
import { ContactsNav } from "./contacts";
import { SettingsNav } from "./settings";
@@ -8,6 +10,7 @@ import { TimelineView } from "./timeline";
import { ToastArea } from "./toast";
import { useShell } from "~/contexts/shell";
+import { useConfigValue } from "~/shared/config";
import { useTabs } from "~/store/zustand/tabs";
const DevtoolView = lazy(() =>
@@ -17,6 +20,7 @@ const DevtoolView = lazy(() =>
export function LeftSidebar() {
const { leftsidebar } = useShell();
const currentTab = useTabs((state) => state.currentTab);
+ const sidebarTimelineEnabled = useConfigValue("sidebar_timeline_enabled");
const isSettingsMode = currentTab?.type === "settings";
const isCalendarMode = currentTab?.type === "calendar";
@@ -24,9 +28,16 @@ export function LeftSidebar() {
const isTemplatesMode = currentTab?.type === "templates";
const isSpecialMode =
isSettingsMode || isCalendarMode || isContactsMode || isTemplatesMode;
+ const isTimelineSidebarLayout =
+ sidebarTimelineEnabled && !leftsidebar.showDevtool && !isSpecialMode;
return (
-
+
{leftsidebar.showDevtool ? (
@@ -42,7 +53,7 @@ export function LeftSidebar() {
) : isTemplatesMode ? (
) : (
-
+
)}
{!leftsidebar.showDevtool && !isSpecialMode && }
diff --git a/apps/desktop/src/sidebar/settings.tsx b/apps/desktop/src/sidebar/settings.tsx
index fdfa77e49a..482833a11a 100644
--- a/apps/desktop/src/sidebar/settings.tsx
+++ b/apps/desktop/src/sidebar/settings.tsx
@@ -16,6 +16,8 @@ import { useCallback } from "react";
import { cn } from "@hypr/utils";
+import { CustomSidebarHeader } from "./custom-sidebar-header";
+
import { type SettingsTab, useTabs } from "~/store/zustand/tabs";
type SettingsNavItem =
@@ -110,9 +112,7 @@ export function SettingsNav() {
return (
-
-
Settings
-
+
{groups.map((group) => (
diff --git a/apps/desktop/src/sidebar/timeline/anchor.ts b/apps/desktop/src/sidebar/timeline/anchor.ts
index cd919a078d..e761a0db64 100644
--- a/apps/desktop/src/sidebar/timeline/anchor.ts
+++ b/apps/desktop/src/sidebar/timeline/anchor.ts
@@ -18,25 +18,32 @@ export function useAnchor() {
);
}, []);
- const scrollToAnchor = useCallback(() => {
- const container = containerRef.current;
- if (!container || !anchorNode) {
- return;
- }
+ const scrollToAnchor = useCallback(
+ (options?: { behavior?: ScrollBehavior; viewportRatio?: number }) => {
+ const container = containerRef.current;
+ if (!container || !anchorNode) {
+ return;
+ }
- const containerRect = container.getBoundingClientRect();
- const anchorRect = anchorNode.getBoundingClientRect();
- const anchorCenter =
- anchorRect.top -
- containerRect.top +
- container.scrollTop +
- anchorRect.height / 2;
- const targetScrollTop = Math.max(
- anchorCenter - container.clientHeight / 2,
- 0,
- );
- container.scrollTo({ top: targetScrollTop, behavior: "smooth" });
- }, [anchorNode]);
+ const containerRect = container.getBoundingClientRect();
+ const anchorRect = anchorNode.getBoundingClientRect();
+ const anchorCenter =
+ anchorRect.top -
+ containerRect.top +
+ container.scrollTop +
+ anchorRect.height / 2;
+ const viewportRatio = options?.viewportRatio ?? 0.5;
+ const targetScrollTop = Math.max(
+ anchorCenter - container.clientHeight * viewportRatio,
+ 0,
+ );
+ container.scrollTo({
+ top: targetScrollTop,
+ behavior: options?.behavior ?? "smooth",
+ });
+ },
+ [anchorNode],
+ );
useEffect(() => {
const container = containerRef.current;
@@ -79,7 +86,10 @@ export function useAutoScrollToAnchor({
anchorNode,
deps = [],
}: {
- scrollFn: () => void;
+ scrollFn: (options?: {
+ behavior?: ScrollBehavior;
+ viewportRatio?: number;
+ }) => void;
isVisible: boolean;
anchorNode: HTMLDivElement | null;
deps?: DependencyList;
@@ -94,7 +104,7 @@ export function useAutoScrollToAnchor({
hasInitialScrolledRef.current = true;
requestAnimationFrame(() => {
- scrollFn();
+ scrollFn({ behavior: "auto", viewportRatio: 0.15 });
});
}, [anchorNode, scrollFn]);
diff --git a/apps/desktop/src/sidebar/timeline/index.tsx b/apps/desktop/src/sidebar/timeline/index.tsx
index 9c27a7442f..8117186e4a 100644
--- a/apps/desktop/src/sidebar/timeline/index.tsx
+++ b/apps/desktop/src/sidebar/timeline/index.tsx
@@ -1,4 +1,9 @@
-import { CalendarDaysIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
+import {
+ ArrowDownIcon,
+ ArrowUpIcon,
+ CalendarDaysIcon,
+ SunIcon,
+} from "lucide-react";
import {
type ReactNode,
useCallback,
@@ -44,7 +49,13 @@ import { useTabs } from "~/store/zustand/tabs";
import { useTimelineSelection } from "~/store/zustand/timeline-selection";
import { useUndoDelete } from "~/store/zustand/undo-delete";
-export function TimelineView() {
+export function TimelineView({
+ showOpenCalendarButton = true,
+ topChromeInset = false,
+}: {
+ showOpenCalendarButton?: boolean;
+ topChromeInset?: boolean;
+} = {}) {
const timezone = useConfigValue("timezone") || undefined;
const { timelineEventsTable, timelineSessionsTable } = useTimelineTables();
const allBuckets = useTimelineData({
@@ -54,6 +65,7 @@ export function TimelineView() {
});
const [showIgnored, setShowIgnored] = useState(false);
const [isScrolledToTop, setIsScrolledToTop] = useState(true);
+ const [isScrolledToBottom, setIsScrolledToBottom] = useState(true);
const { isIgnored } = useIgnoredEvents();
const openNew = useTabs((state) => state.openNew);
@@ -90,8 +102,9 @@ export function TimelineView() {
);
}, [timelineEventsTable, showIgnored, isIgnored]);
- const showOpenCalendarButton = useMemo(
+ const showOpenCalendarChip = useMemo(
() =>
+ showOpenCalendarButton &&
isScrolledToTop &&
hasTimelineItemsAfterTomorrow({
timelineEventsTable: visibleTimelineEventsTable,
@@ -103,6 +116,7 @@ export function TimelineView() {
visibleTimelineEventsTable,
timelineSessionsTable,
timezone,
+ showOpenCalendarButton,
],
);
@@ -116,6 +130,9 @@ export function TimelineView() {
[visibleTimelineEventsTable, timelineSessionsTable, timezone],
);
+ const reserveOpenCalendarChipSpace =
+ topChromeInset && showOpenCalendarButton && hasMoreFutureItems;
+
const hasToday = useMemo(
() => buckets.some((bucket) => bucket.label === "Today"),
[buckets],
@@ -161,7 +178,12 @@ export function TimelineView() {
}
const updateScrollPosition = () => {
+ const maxScrollTop = Math.max(
+ 0,
+ container.scrollHeight - container.clientHeight,
+ );
setIsScrolledToTop(container.scrollTop <= 12);
+ setIsScrolledToBottom(maxScrollTop - container.scrollTop <= 12);
};
updateScrollPosition();
@@ -172,7 +194,14 @@ export function TimelineView() {
return () => {
container.removeEventListener("scroll", updateScrollPosition);
};
- }, [containerRef]);
+ }, [containerRef, buckets.length, flatItemKeys.length]);
+
+ const scrollFadeMask = useMemo(() => {
+ const topFadeEnd = isScrolledToTop ? "0px" : "28px";
+ const bottomFadeStart = isScrolledToBottom ? "100%" : "calc(100% - 28px)";
+
+ return `linear-gradient(to bottom, transparent 0, #000 ${topFadeEnd}, #000 ${bottomFadeStart}, transparent 100%)`;
+ }, [isScrolledToTop, isScrolledToBottom]);
const todayBucketLength = useMemo(() => {
const b = buckets.find((bucket) => bucket.label === "Today");
@@ -295,8 +324,24 @@ export function TimelineView() {
"scrollbar-hide flex h-full flex-col overflow-y-auto",
"rounded-xl",
])}
+ style={{
+ WebkitMaskImage: scrollFadeMask,
+ maskImage: scrollFadeMask,
+ }}
>
- {hasMoreFutureItems &&
}
+ {(topChromeInset || hasMoreFutureItems) && (
+
+ )}
{buckets.map((bucket, index) => {
const isToday = bucket.label === "Today";
const shouldRenderIndicatorBefore =
@@ -361,9 +406,26 @@ export function TimelineView() {
)}
- {(showOpenCalendarButton || (!isTodayVisible && isScrolledPastToday)) && (
-
- {showOpenCalendarButton && (
+ {topChromeInset && (
+
+ )}
+
+ {(showOpenCalendarChip || (!isTodayVisible && isScrolledPastToday)) && (
+
+ {showOpenCalendarChip && (
)}
{!isTodayVisible && !isScrolledPastToday && (
-
-
- Go back to now
-
+ />
)}
);
}
+function TimelineNowChip({
+ className,
+ direction,
+ onClick,
+}: {
+ className?: string;
+ direction: "up" | "down";
+ onClick: () => void;
+}) {
+ const DirectionIcon = direction === "up" ? ArrowUpIcon : ArrowDownIcon;
+
+ return (
+
+ {direction === "up" ? : null}
+
+ Now
+ {direction === "down" ? : null}
+
+ );
+}
+
function TodayBucket({
items,
precision,
diff --git a/apps/desktop/src/sidebar/timeline/item.tsx b/apps/desktop/src/sidebar/timeline/item.tsx
index 541c0a4959..bf8bab2a7f 100644
--- a/apps/desktop/src/sidebar/timeline/item.tsx
+++ b/apps/desktop/src/sidebar/timeline/item.tsx
@@ -173,7 +173,6 @@ const EventItem = memo(
const eventId = item.id;
const trackingIdEvent = item.data.tracking_id_event;
const title = item.data.title || "Untitled";
- const calendarId = item.data.calendar_id ?? null;
const recurrenceSeriesId = item.data.recurrence_series_id;
const {
@@ -297,7 +296,7 @@ const EventItem = memo(
-
+
diff --git a/apps/desktop/src/sidebar/use-custom-sidebar.ts b/apps/desktop/src/sidebar/use-custom-sidebar.ts
index 6a51c1cad7..e2d8340b21 100644
--- a/apps/desktop/src/sidebar/use-custom-sidebar.ts
+++ b/apps/desktop/src/sidebar/use-custom-sidebar.ts
@@ -9,10 +9,21 @@ const CUSTOM_SIDEBAR_TYPES: Tab["type"][] = [
"templates",
];
+const LEFT_SURFACE_CUSTOM_SIDEBAR_TYPES: Tab["type"][] = [
+ "calendar",
+ "settings",
+ "contacts",
+ "templates",
+];
+
export function hasCustomSidebarTab(tab: Tab | null): boolean {
return tab !== null && CUSTOM_SIDEBAR_TYPES.includes(tab.type);
}
+export function hasLeftSurfaceCustomSidebarTab(tab: Tab | null): boolean {
+ return tab !== null && LEFT_SURFACE_CUSTOM_SIDEBAR_TYPES.includes(tab.type);
+}
+
export function useCustomSidebarEffect(
active: boolean,
leftsidebar: {
diff --git a/apps/desktop/src/store/tinybase/persister/settings/persister.test.ts b/apps/desktop/src/store/tinybase/persister/settings/persister.test.ts
index 8f087ba85b..5ddbd7c0b2 100644
--- a/apps/desktop/src/store/tinybase/persister/settings/persister.test.ts
+++ b/apps/desktop/src/store/tinybase/persister/settings/persister.test.ts
@@ -424,6 +424,7 @@ describe("settingsPersister roundtrip", () => {
general: {
autostart: true,
floating_bar_enabled: false,
+ sidebar_timeline_enabled: true,
save_recordings: true,
},
notification: {
@@ -441,6 +442,7 @@ describe("settingsPersister roundtrip", () => {
expect(result.general).toEqual({
autostart: true,
floating_bar_enabled: false,
+ sidebar_timeline_enabled: true,
});
expect(result.notification).toEqual({ event: false });
});
diff --git a/apps/desktop/src/store/tinybase/store/settings.ts b/apps/desktop/src/store/tinybase/store/settings.ts
index b3c6f2146f..bb959a9b45 100644
--- a/apps/desktop/src/store/tinybase/store/settings.ts
+++ b/apps/desktop/src/store/tinybase/store/settings.ts
@@ -46,6 +46,11 @@ export const SETTINGS_MAPPING = {
path: ["general", "floating_bar_enabled"],
default: true as boolean,
},
+ sidebar_timeline_enabled: {
+ type: "boolean",
+ path: ["general", "sidebar_timeline_enabled"],
+ default: false as boolean,
+ },
save_recordings: {
type: "boolean",
path: ["general", "save_recordings"],
diff --git a/apps/desktop/src/store/zustand/tabs/chat-mode.test.ts b/apps/desktop/src/store/zustand/tabs/chat-mode.test.ts
index f095d48b0b..13dd8c155a 100644
--- a/apps/desktop/src/store/zustand/tabs/chat-mode.test.ts
+++ b/apps/desktop/src/store/zustand/tabs/chat-mode.test.ts
@@ -12,25 +12,20 @@ describe("Chat Mode", () => {
expect(useTabs.getState().chatMode).toBe("FloatingClosed");
});
- test("TOGGLE from FloatingClosed → RightPanelOpen", () => {
+ test("TOGGLE from FloatingClosed to FloatingOpen", () => {
useTabs.getState().transitionChatMode({ type: "TOGGLE" });
- expect(useTabs.getState().chatMode).toBe("RightPanelOpen");
+ expect(useTabs.getState().chatMode).toBe("FloatingOpen");
});
- test("TOGGLE from RightPanelOpen → FloatingClosed", () => {
+ test("TOGGLE from FloatingOpen to FloatingClosed", () => {
useTabs.getState().transitionChatMode({ type: "TOGGLE" });
useTabs.getState().transitionChatMode({ type: "TOGGLE" });
expect(useTabs.getState().chatMode).toBe("FloatingClosed");
});
- test("OPEN from FloatingClosed → RightPanelOpen", () => {
+ test("OPEN from FloatingClosed to FloatingOpen", () => {
useTabs.getState().transitionChatMode({ type: "OPEN" });
- expect(useTabs.getState().chatMode).toBe("RightPanelOpen");
- });
-
- test("OPEN_RIGHT_PANEL from FloatingClosed → RightPanelOpen", () => {
- useTabs.getState().transitionChatMode({ type: "OPEN_RIGHT_PANEL" });
- expect(useTabs.getState().chatMode).toBe("RightPanelOpen");
+ expect(useTabs.getState().chatMode).toBe("FloatingOpen");
});
test("no-op when event is irrelevant for current state", () => {
@@ -42,21 +37,21 @@ describe("Chat Mode", () => {
const session = createSessionTab();
useTabs.getState().openNew(session);
useTabs.getState().transitionChatMode({ type: "OPEN" });
- expect(useTabs.getState().chatMode).toBe("RightPanelOpen");
+ expect(useTabs.getState().chatMode).toBe("FloatingOpen");
const sessionTab = useTabs
.getState()
.tabs.find((t) => t.type === "sessions")!;
useTabs.getState().close(sessionTab);
- expect(useTabs.getState().chatMode).toBe("RightPanelOpen");
+ expect(useTabs.getState().chatMode).toBe("FloatingOpen");
});
- test("closeAll leaves the right panel chat mode unchanged", () => {
+ test("closeAll leaves the floating chat mode unchanged", () => {
const session = createSessionTab();
useTabs.getState().openNew(session);
useTabs.getState().transitionChatMode({ type: "OPEN" });
useTabs.getState().closeAll();
- expect(useTabs.getState().chatMode).toBe("RightPanelOpen");
+ expect(useTabs.getState().chatMode).toBe("FloatingOpen");
});
});
diff --git a/apps/desktop/src/store/zustand/tabs/chat-mode.ts b/apps/desktop/src/store/zustand/tabs/chat-mode.ts
index 18e9435c7d..f2585790bc 100644
--- a/apps/desktop/src/store/zustand/tabs/chat-mode.ts
+++ b/apps/desktop/src/store/zustand/tabs/chat-mode.ts
@@ -1,10 +1,9 @@
import type { StoreApi } from "zustand";
-export type ChatMode = "RightPanelOpen" | "FloatingClosed";
+export type ChatMode = "FloatingOpen" | "FloatingClosed";
export type ChatEvent =
| { type: "OPEN" }
- | { type: "OPEN_RIGHT_PANEL" }
| { type: "CLOSE" }
| { type: "TOGGLE" };
@@ -18,18 +17,14 @@ export type ChatModeActions = {
const computeNextChatMode = (state: ChatMode, event: ChatEvent): ChatMode => {
switch (state) {
- case "RightPanelOpen":
+ case "FloatingOpen":
if (event.type === "CLOSE" || event.type === "TOGGLE") {
return "FloatingClosed";
}
return state;
case "FloatingClosed":
- if (
- event.type === "OPEN" ||
- event.type === "OPEN_RIGHT_PANEL" ||
- event.type === "TOGGLE"
- ) {
- return "RightPanelOpen";
+ if (event.type === "OPEN" || event.type === "TOGGLE") {
+ return "FloatingOpen";
}
return state;
default:
diff --git a/apps/desktop/src/templates/template-sidebar.tsx b/apps/desktop/src/templates/template-sidebar.tsx
index 0b9c5e4def..c2058395ef 100644
--- a/apps/desktop/src/templates/template-sidebar.tsx
+++ b/apps/desktop/src/templates/template-sidebar.tsx
@@ -16,6 +16,7 @@ import { getTemplateCopyTitle, type UserTemplate } from "./queries";
import { useTemplateTab } from "./utils";
import { useNativeContextMenu } from "~/shared/hooks/useNativeContextMenu";
+import { CustomSidebarHeader } from "~/sidebar/custom-sidebar-header";
import { type Tab } from "~/store/zustand/tabs";
type SortOption = "alphabetical" | "reverse-alphabetical";
@@ -292,52 +293,49 @@ export function TemplatesSidebarContent({
return (
-
-
Templates
-
- {userTemplates.length > 1 && (
-
-
-
+ {userTemplates.length > 1 && (
+
+
+
+
+
+
+
+
+ setSortOption("alphabetical")}
>
-
-
-
-
-
- setSortOption("alphabetical")}
- >
- A to Z
-
- setSortOption("reverse-alphabetical")}
- >
- Z to A
-
-
-
-
- )}
-
-
-
-
-
-
+ A to Z
+
+
setSortOption("reverse-alphabetical")}
+ >
+ Z to A
+
+
+
+
+ )}
+
+
+
+
+
-
+
diff --git a/packages/store/src/tinybase.ts b/packages/store/src/tinybase.ts
index f3603c0870..46377c425a 100644
--- a/packages/store/src/tinybase.ts
+++ b/packages/store/src/tinybase.ts
@@ -158,6 +158,7 @@ export const valueSchemaForTinybase = {
auto_stop_meetings: { type: "boolean" },
auto_start_scheduled_meetings: { type: "boolean" },
floating_bar_enabled: { type: "boolean" },
+ sidebar_timeline_enabled: { type: "boolean" },
save_recordings: { type: "boolean" },
audio_retention: { type: "string" },
notification_event: { type: "boolean" },
diff --git a/packages/store/src/zod.ts b/packages/store/src/zod.ts
index ceb6b3d9cc..b42803c840 100644
--- a/packages/store/src/zod.ts
+++ b/packages/store/src/zod.ts
@@ -265,6 +265,7 @@ export const generalSchema = z.object({
auto_stop_meetings: z.boolean().default(true),
auto_start_scheduled_meetings: z.boolean().default(true),
floating_bar_enabled: z.boolean().default(true),
+ sidebar_timeline_enabled: z.boolean().default(false),
telemetry_consent: z.boolean().default(true),
save_recordings: z.boolean().default(true),
audio_retention: z.string().default("forever"),
diff --git a/plugins/windows/src/window/v1.rs b/plugins/windows/src/window/v1.rs
index e41b1b23ab..817848b054 100644
--- a/plugins/windows/src/window/v1.rs
+++ b/plugins/windows/src/window/v1.rs
@@ -77,7 +77,7 @@ impl AppWindow {
.decorations(true)
.hidden_title(true)
.theme(Some(tauri::Theme::Light))
- .traffic_light_position(tauri::LogicalPosition::new(12.0, traffic_light_y + 4.0))
+ .traffic_light_position(tauri::LogicalPosition::new(12.0, traffic_light_y))
.title_bar_style(tauri::TitleBarStyle::Overlay);
}