-
-
Notifications
You must be signed in to change notification settings - Fork 12
feat(assistant): taOS Assistant slide-over panel v1 #293
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,13 +21,16 @@ import { LoginScreen } from "@/components/LoginScreen"; | |
| import { NotificationToasts } from "@/components/NotificationToast"; | ||
| import { NotificationCentre } from "@/components/NotificationCentre"; | ||
| import { useNotificationStore } from "@/stores/notification-store"; | ||
| import { TaosAssistantPanel } from "@/components/TaosAssistantPanel"; | ||
| import { useTaosAgentStore } from "@/stores/taos-agent-store"; | ||
|
|
||
| interface SystemShortcutsProps { | ||
| toggleSearch: () => void; | ||
| toggleLaunchpad: () => void; | ||
| toggleAssistant: () => void; | ||
| } | ||
|
|
||
| function SystemShortcuts({ toggleSearch, toggleLaunchpad }: SystemShortcutsProps) { | ||
| function SystemShortcuts({ toggleSearch, toggleLaunchpad, toggleAssistant }: SystemShortcutsProps) { | ||
| const windows = useProcessStore((s) => s.windows); | ||
| const closeWindow = useProcessStore((s) => s.closeWindow); | ||
| const minimizeWindow = useProcessStore((s) => s.minimizeWindow); | ||
|
|
@@ -72,6 +75,7 @@ function SystemShortcuts({ toggleSearch, toggleLaunchpad }: SystemShortcutsProps | |
|
|
||
| useShortcut("Ctrl+Space", toggleSearch, "Toggle search palette", "system"); | ||
| useShortcut("Ctrl+l", toggleLaunchpad, "Toggle launchpad", "system"); | ||
| useShortcut("Ctrl+/", toggleAssistant, "Toggle taOS Assistant", "system"); | ||
| useShortcut("Ctrl+w", closeFocused, "Close focused window", "system"); | ||
| useShortcut("Ctrl+m", minimizeFocused, "Minimize focused window", "system"); | ||
| useShortcut("Ctrl+f", maximizeFocused, "Maximize/restore focused window", "system"); | ||
|
|
@@ -135,6 +139,7 @@ export function App() { | |
|
|
||
| const toggleLaunchpad = useCallback(() => setLaunchpadOpen((v) => !v), []); | ||
| const toggleSearch = useCallback(() => setSearchOpen((v) => !v), []); | ||
| const toggleAssistant = useCallback(() => useTaosAgentStore.getState().togglePanel(), []); | ||
|
|
||
| // Listen for launchpad open event from context menu | ||
| useEffect(() => { | ||
|
|
@@ -213,7 +218,7 @@ export function App() { | |
| if (mode === "desktop") { | ||
| return ( | ||
| <ShortcutProvider> | ||
| <SystemShortcuts toggleSearch={toggleSearch} toggleLaunchpad={toggleLaunchpad} /> | ||
| <SystemShortcuts toggleSearch={toggleSearch} toggleLaunchpad={toggleLaunchpad} toggleAssistant={toggleAssistant} /> | ||
| <LoginGate> | ||
| {!launched && <LoginScreen onLaunch={() => setLaunched(true)} />} | ||
| {launched && !isFullscreen && ( | ||
|
|
@@ -227,13 +232,14 @@ export function App() { | |
| )} | ||
| <div className={`transition-all duration-500 ${launched ? "opacity-100 scale-100" : "opacity-0 scale-95"}`}> | ||
| <div className="h-screen w-screen flex flex-col overflow-hidden bg-shell-bg text-shell-text"> | ||
| <TopBar onSearchOpen={toggleSearch} /> | ||
| <TopBar onSearchOpen={toggleSearch} onAssistantOpen={toggleAssistant} /> | ||
| <Desktop /> | ||
| <Dock onLaunchpadOpen={toggleLaunchpad} /> | ||
| <Launchpad open={launchpadOpen} onClose={() => setLaunchpadOpen(false)} onOpenApp={(wid) => setActiveWindowId(wid)} /> | ||
| <SearchPalette open={searchOpen} onClose={() => setSearchOpen(false)} onOpenApp={(wid) => setActiveWindowId(wid)} /> | ||
| <NotificationToasts /> | ||
| <NotificationCentre /> | ||
| <TaosAssistantPanel /> | ||
| </div> | ||
| </div> | ||
| </LoginGate> | ||
|
|
@@ -244,7 +250,7 @@ export function App() { | |
| // Mobile/Tablet layout — no login gate, no fullscreen button (PWA is already fullscreen) | ||
| return ( | ||
| <ShortcutProvider> | ||
| <SystemShortcuts toggleSearch={toggleSearch} toggleLaunchpad={toggleLaunchpad} /> | ||
| <SystemShortcuts toggleSearch={toggleSearch} toggleLaunchpad={toggleLaunchpad} toggleAssistant={toggleAssistant} /> | ||
|
Comment on lines
252
to
+253
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't register the assistant shortcut on layouts that never render the assistant. In the mobile/tablet branch, 🤖 Prompt for AI Agents |
||
| <div className="taos-wallpaper h-screen w-screen flex flex-col text-shell-text" style={{ backgroundColor: wallpaperFallback, ["--wallpaper-desktop" as never]: wallpaperImage, ["--wallpaper-mobile" as never]: wallpaperMobileImage }}> | ||
| <div className={`flex-1 flex flex-col overflow-hidden transition-all duration-500 ${launched ? "opacity-100 scale-100" : "opacity-0 scale-95"}`}> | ||
| <MobileTopBar | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| import { render, screen, fireEvent } from "@testing-library/react"; | ||
| import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; | ||
| import { TaosAssistantPanel } from "./TaosAssistantPanel"; | ||
| import { useTaosAgentStore } from "@/stores/taos-agent-store"; | ||
|
|
||
| // Mock child that opens a modal — keeps the test surface narrow | ||
| vi.mock("./TaosAssistantSettings", () => ({ | ||
| TaosAssistantSettings: ({ open }: { open: boolean }) => | ||
| open ? <div data-testid="settings-modal" /> : null, | ||
| })); | ||
|
|
||
| // Mock fetch so settings load doesn't throw in jsdom | ||
| const mockFetch = vi.fn().mockResolvedValue({ | ||
| ok: true, | ||
| json: async () => ({ model: null }), | ||
| }); | ||
| vi.stubGlobal("fetch", mockFetch); | ||
|
|
||
| function resetStore() { | ||
| useTaosAgentStore.setState({ | ||
| isOpen: true, | ||
| messages: [], | ||
| model: null, | ||
| streaming: false, | ||
| }); | ||
| } | ||
|
|
||
| describe("TaosAssistantPanel", () => { | ||
| beforeEach(() => { | ||
| resetStore(); | ||
| mockFetch.mockClear(); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| vi.clearAllMocks(); | ||
| }); | ||
|
|
||
| it("renders nothing when closed", () => { | ||
| useTaosAgentStore.setState({ isOpen: false }); | ||
| const { container } = render(<TaosAssistantPanel />); | ||
| expect(container.firstChild).toBeNull(); | ||
| }); | ||
|
|
||
| it("shows title when open", () => { | ||
| render(<TaosAssistantPanel />); | ||
| expect(screen.getByRole("dialog", { name: /taOS Assistant/i })).toBeInTheDocument(); | ||
| expect(screen.getByText("taOS Assistant")).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("shows empty state with pick-model button when no model", () => { | ||
| render(<TaosAssistantPanel />); | ||
| expect(screen.getByText("Pick a model to get started")).toBeInTheDocument(); | ||
| expect(screen.getByRole("button", { name: /choose a model/i })).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("shows message input disabled when no model", () => { | ||
| render(<TaosAssistantPanel />); | ||
| const textarea = screen.getByRole("textbox", { name: /message taOS Assistant/i }); | ||
| expect(textarea).toBeDisabled(); | ||
| }); | ||
|
|
||
| it("shows hint when model is set and no messages", () => { | ||
| useTaosAgentStore.setState({ model: "qwen3" }); | ||
| render(<TaosAssistantPanel />); | ||
| expect(screen.getByText(/ask me anything about taOS/i)).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("renders user and assistant messages", () => { | ||
| useTaosAgentStore.setState({ | ||
| model: "qwen3", | ||
| messages: [ | ||
| { role: "user", content: "Hello there", ts: 1 }, | ||
| { role: "assistant", content: "Hi! How can I help?", ts: 2 }, | ||
| ], | ||
| }); | ||
| render(<TaosAssistantPanel />); | ||
| expect(screen.getByText("Hello there")).toBeInTheDocument(); | ||
| expect(screen.getByText("Hi! How can I help?")).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("close button calls closePanel", () => { | ||
| render(<TaosAssistantPanel />); | ||
| const closeBtn = screen.getByRole("button", { name: /close taOS Assistant/i }); | ||
| fireEvent.click(closeBtn); | ||
| expect(useTaosAgentStore.getState().isOpen).toBe(false); | ||
| }); | ||
|
|
||
| it("settings cog opens settings modal", () => { | ||
| render(<TaosAssistantPanel />); | ||
| const cogBtn = screen.getByRole("button", { name: /assistant settings/i }); | ||
| fireEvent.click(cogBtn); | ||
| expect(screen.getByTestId("settings-modal")).toBeInTheDocument(); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pass an open action to the top-bar button, not a toggle.
TopBaradvertisesonAssistantOpen, but this wiring closes the panel when it is already open. That makes the "Open taOS Assistant" button behave like a hidden toggle and diverges from the documented close affordances.Suggested change
Also applies to: 235-235
🤖 Prompt for AI Agents