From 4cf5892975a9bc4a4684030cecd9463f0dda598e Mon Sep 17 00:00:00 2001 From: preetsuthar17 Date: Thu, 19 Mar 2026 16:30:01 +0530 Subject: [PATCH] feat(ui,api): ChatGPT-style sidebar, conversation delete, tooltips, Inter font UI: - Redesign sidebar to ChatGPT flat style with collapsible agent sections, animated expand/collapse (grid-template-rows), and proper mobile drawer - Add conversation delete via three-dot dropdown menu with confirmation dialog - Add shadcn Tooltip and DropdownMenu components with tooltips on all icon buttons (send, refresh, open instance, sidebar toggle, new chat, etc.) - Switch font from Geist to Inter - Simplify create form: only preset + name visible by default, rest under animated "Advanced options" collapsible - Auto-generate spritz name on page load - Auto-focus composer when conversation opens or agent finishes responding - Replace all space-y/space-x with flexbox gap - Add will-change hints to animated elements - Add scrollbar-gutter: stable globally API: - Add DELETE /acp/conversations/:id endpoint for conversation deletion with ownership authorization --- api/acp_conversations.go | 21 ++ api/main.go | 1 + ui/package.json | 3 +- ui/pnpm-lock.yaml | 22 +- ui/src/components/acp/composer.tsx | 33 +-- ui/src/components/acp/sidebar.tsx | 298 +++++++++++++++---------- ui/src/components/create-form.tsx | 27 ++- ui/src/components/layout.tsx | 13 +- ui/src/components/ui/dropdown-menu.tsx | 266 ++++++++++++++++++++++ ui/src/components/ui/sonner.tsx | 27 ++- ui/src/components/ui/tooltip.tsx | 64 ++++++ ui/src/index.css | 4 +- ui/src/pages/chat.tsx | 113 ++++++++-- 13 files changed, 714 insertions(+), 178 deletions(-) create mode 100644 ui/src/components/ui/dropdown-menu.tsx create mode 100644 ui/src/components/ui/tooltip.tsx diff --git a/api/acp_conversations.go b/api/acp_conversations.go index bfbfbff..3cb9b7d 100644 --- a/api/acp_conversations.go +++ b/api/acp_conversations.go @@ -177,6 +177,27 @@ func (s *server) updateACPConversation(c echo.Context) error { return writeJSON(c, http.StatusOK, conversation) } +func (s *server) deleteACPConversation(c echo.Context) error { + if !s.acp.enabled { + return writeError(c, http.StatusNotFound, "acp disabled") + } + principal, ok := principalFromContext(c) + if s.auth.enabled() && (!ok || principal.ID == "") { + return writeError(c, http.StatusUnauthorized, "unauthenticated") + } + if err := authorizeHumanOnly(principal, s.auth.enabled()); err != nil { + return writeForbidden(c) + } + conversation, err := s.getAuthorizedConversation(c.Request().Context(), principal, s.requestNamespace(c), c.Param("id")) + if err != nil { + return s.writeACPConversationError(c, err) + } + if err := s.client.Delete(c.Request().Context(), conversation); err != nil { + return writeError(c, http.StatusInternalServerError, err.Error()) + } + return c.NoContent(http.StatusNoContent) +} + func decodeACPBody(c echo.Context, target any) error { if c.Request().Body == nil || c.Request().ContentLength == 0 { return nil diff --git a/api/main.go b/api/main.go index 64a2070..4ed5544 100644 --- a/api/main.go +++ b/api/main.go @@ -256,6 +256,7 @@ func (s *server) registerRoutes(e *echo.Echo) { secured.GET("/acp/conversations/:id", s.getACPConversation) secured.POST("/acp/conversations/:id/bootstrap", s.bootstrapACPConversation) secured.PATCH("/acp/conversations/:id", s.updateACPConversation) + secured.DELETE("/acp/conversations/:id", s.deleteACPConversation) secured.GET("/acp/conversations/:id/connect", s.openACPConversationConnection) secured.POST("/spritzes/:name/ssh", s.mintSSHCert) if s.terminal.enabled { diff --git a/ui/package.json b/ui/package.json index 7510b3e..42f1f94 100644 --- a/ui/package.json +++ b/ui/package.json @@ -11,12 +11,13 @@ }, "dependencies": { "@base-ui/react": "^1.3.0", - "@fontsource-variable/geist": "^5.2.8", + "@fontsource-variable/inter": "^5.2.8", "@tailwindcss/vite": "^4.2.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "flowtoken": "^1.0.40", "lucide-react": "^0.577.0", + "next-themes": "^0.4.6", "react": "^19.1.0", "react-dom": "^19.1.0", "react-router-dom": "^7.13.1", diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 2500230..9dbfc2a 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -11,7 +11,7 @@ importers: '@base-ui/react': specifier: ^1.3.0 version: 1.3.0(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@fontsource-variable/geist': + '@fontsource-variable/inter': specifier: ^5.2.8 version: 5.2.8 '@tailwindcss/vite': @@ -29,6 +29,9 @@ importers: lucide-react: specifier: ^0.577.0 version: 0.577.0(react@19.2.4) + next-themes: + specifier: ^0.4.6 + version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: specifier: ^19.1.0 version: 19.2.4 @@ -491,8 +494,8 @@ packages: '@floating-ui/utils@0.2.11': resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} - '@fontsource-variable/geist@5.2.8': - resolution: {integrity: sha512-cJ6m9e+8MQ5dCYJsLylfZrgBh6KkG4bOLckB35Tr9J/EqdkEM6QllH5PxqP1dhTvFup+HtMRPuz9xOjxXJggxw==} + '@fontsource-variable/inter@5.2.8': + resolution: {integrity: sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ==} '@hono/node-server@1.19.11': resolution: {integrity: sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==} @@ -2546,6 +2549,12 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} + next-themes@0.4.6: + resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} + peerDependencies: + react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} @@ -3715,7 +3724,7 @@ snapshots: '@floating-ui/utils@0.2.11': {} - '@fontsource-variable/geist@5.2.8': {} + '@fontsource-variable/inter@5.2.8': {} '@hono/node-server@1.19.11(hono@4.12.8)': dependencies: @@ -5653,6 +5662,11 @@ snapshots: negotiator@1.0.0: {} + next-themes@0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + node-domexception@1.0.0: {} node-fetch@3.3.2: diff --git a/ui/src/components/acp/composer.tsx b/ui/src/components/acp/composer.tsx index 33b5864..cf02e67 100644 --- a/ui/src/components/acp/composer.tsx +++ b/ui/src/components/acp/composer.tsx @@ -1,5 +1,6 @@ import { useState, useRef, useCallback, useImperativeHandle, forwardRef } from 'react'; import { SendIcon, SquareIcon } from 'lucide-react'; +import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'; const TERMINAL_STATUSES = ['connected', 'completed', 'disconnected', 'no acp-ready instances']; @@ -79,19 +80,25 @@ export const Composer = forwardRef(function Compo className="block w-full min-h-[24px] max-h-[180px] resize-none border-none bg-transparent rounded-t-[28px] px-5 pt-4 pb-1 font-inherit text-sm leading-[1.55] outline-none placeholder:text-[#999] focus:outline-none focus:ring-0 [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none] overflow-y-auto" />
- + + + {promptInFlight ? ( + + ) : ( + + )} + + } + /> + {promptInFlight ? 'Stop' : 'Send'} +
diff --git a/ui/src/components/acp/sidebar.tsx b/ui/src/components/acp/sidebar.tsx index 8b32207..0f10a86 100644 --- a/ui/src/components/acp/sidebar.tsx +++ b/ui/src/components/acp/sidebar.tsx @@ -4,13 +4,21 @@ import { PlusIcon, PencilIcon, LayoutGridIcon, - MessageSquareIcon, + Trash2Icon, + EllipsisIcon, + ChevronRightIcon, } from 'lucide-react'; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, +} from '@/components/ui/dropdown-menu'; import { cn } from '@/lib/utils'; +import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'; import type { ConversationInfo } from '@/types/acp'; import type { Spritz } from '@/types/spritz'; -/* Sidebar collapse/expand toggle icon matching the original acp-sidebar-toggle */ function SidebarToggleIcon({ collapsed }: { collapsed: boolean }) { return collapsed ? ( @@ -29,6 +37,7 @@ interface SidebarProps { selectedConversationId: string | null; onSelectConversation: (conversation: ConversationInfo) => void; onNewConversation: (spritzName: string) => void; + onDeleteConversation: (conversationId: string) => void; collapsed: boolean; onToggleCollapse: () => void; mobileOpen: boolean; @@ -40,6 +49,7 @@ export function Sidebar({ selectedConversationId, onSelectConversation, onNewConversation, + onDeleteConversation, collapsed, onToggleCollapse, mobileOpen, @@ -47,56 +57,94 @@ export function Sidebar({ }: SidebarProps) { const firstAgentName = agents.length > 0 ? agents[0].spritz.metadata.name : null; - function renderSidebarInner(isCollapsed: boolean, closeMobile?: () => void) { + /* ── Collapsed desktop sidebar ── */ + function renderCollapsed() { + return ( + + ); + } + + /* ── Expanded sidebar (desktop + mobile) ── */ + function renderExpanded(closeMobile?: () => void) { const close = closeMobile ?? (() => {}); return ( -