diff --git a/apps/x/apps/renderer/src/App.css b/apps/x/apps/renderer/src/App.css index b83e7dd7..252a145f 100644 --- a/apps/x/apps/renderer/src/App.css +++ b/apps/x/apps/renderer/src/App.css @@ -1240,10 +1240,14 @@ } .graph-view { - background-color: var(--background); + background-color: #f8f8f9; user-select: none; } +.dark .graph-view { + background-color: #0b0b0d; +} + .graph-view::before { content: ''; position: absolute; diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx index 5aa4e411..560fa90b 100644 --- a/apps/x/apps/renderer/src/App.tsx +++ b/apps/x/apps/renderer/src/App.tsx @@ -51,6 +51,7 @@ import { import { Message, MessageContent, + MessageCopyButton, MessageResponse, } from '@/components/ai-elements/message'; import { @@ -5891,14 +5892,17 @@ function App() { {item.content && ( - - - {item.content} - - +
+ + + {item.content} + + + +
)} ) @@ -5906,26 +5910,29 @@ function App() { const { message, files } = parseAttachedFiles(item.content) return ( - - {files.length > 0 && ( -
- {files.map((filePath, index) => ( - - @{wikiLabel(filePath)} - - ))} -
- )} - - {message} - -
+
+ + {files.length > 0 && ( +
+ {files.map((filePath, index) => ( + + @{wikiLabel(filePath)} + + ))} +
+ )} + + {message} + +
+ +
) } @@ -6173,6 +6180,20 @@ function App() { onOpenApps={openAppsView} recentRuns={runs} onOpenRun={(rid) => void navigateToView({ type: 'chat', runId: rid })} + onRenameRun={(rid, title) => { + void window.ipc.invoke('sessions:setTitle', { sessionId: rid, title }) + .then(() => setRuns((prev) => prev.map((r) => (r.id === rid ? { ...r, title } : r)))) + .catch((err) => console.error('Failed to rename chat:', err)) + }} + onDeleteRun={(rid) => { + void window.ipc.invoke('sessions:delete', { sessionId: rid }) + .then(() => { + setRuns((prev) => prev.filter((r) => r.id !== rid)) + const openTab = chatTabs.find((t) => t.runId === rid) + if (openTab) closeChatTab(openTab.id) + }) + .catch((err) => console.error('Failed to delete chat:', err)) + }} onOpenChatHistory={() => void navigateToView({ type: 'chat-history' })} onOpenEmail={(threadId) => openEmailView(threadId)} onOpenHome={() => void navigateToView({ type: 'home' })} diff --git a/apps/x/apps/renderer/src/components/ai-elements/message.tsx b/apps/x/apps/renderer/src/components/ai-elements/message.tsx index ec3acfc1..fcb5250c 100644 --- a/apps/x/apps/renderer/src/components/ai-elements/message.tsx +++ b/apps/x/apps/renderer/src/components/ai-elements/message.tsx @@ -14,8 +14,10 @@ import { import { cn } from "@/lib/utils"; import type { FileUIPart, UIMessage } from "ai"; import { + CheckIcon, ChevronLeftIcon, ChevronRightIcon, + CopyIcon, PaperclipIcon, XIcon, } from "lucide-react"; @@ -38,6 +40,37 @@ export const Message = ({ className, from, ...props }: MessageProps) => ( /> ); +/** + * Minimal copy-to-clipboard affordance for a message bubble. Invisible until + * the surrounding Message (`.group`) is hovered. + */ +export const MessageCopyButton = ({ + text, + className, +}: { + text: string; + className?: string; +}) => { + const [copied, setCopied] = useState(false); + return ( + + ); +}; + export type MessageContentProps = HTMLAttributes; export const MessageContent = ({ @@ -49,7 +82,7 @@ export const MessageContent = ({ data-slot="message-content" className={cn( "is-user:dark flex w-fit max-w-full min-w-0 flex-col gap-2 overflow-hidden text-sm", - "group-[.is-user]:ml-auto group-[.is-user]:rounded-lg group-[.is-user]:bg-secondary group-[.is-user]:px-4 group-[.is-user]:py-3 group-[.is-user]:text-foreground", + "group-[.is-user]:ml-auto group-[.is-user]:rounded-2xl group-[.is-user]:rounded-tr-md group-[.is-user]:bg-secondary group-[.is-user]:px-4 group-[.is-user]:py-2.5 group-[.is-user]:text-foreground", "group-[.is-assistant]:w-full group-[.is-assistant]:text-foreground", className )} diff --git a/apps/x/apps/renderer/src/components/apps/apps-view.tsx b/apps/x/apps/renderer/src/components/apps/apps-view.tsx index 9fdb89de..04a01bce 100644 --- a/apps/x/apps/renderer/src/components/apps/apps-view.tsx +++ b/apps/x/apps/renderer/src/components/apps/apps-view.tsx @@ -58,8 +58,8 @@ const CARD_CSS = ` --ma-pat-opacity:0.05; --ma-glow-opacity:0.10; --ma-glow-hover-opacity:0.16; --ma-badge-mix:15%; --ma-pill-mix:13%; --ma-tint:20%; --ma-tint-hover:26%; } -.ma-inner { max-width:1120px; margin:0 auto; padding:clamp(20px,3.5cqw,34px) clamp(16px,3cqw,30px) 48px; } -.ma-h1 { font-size:clamp(19px,2.6cqw,24px); font-weight:650; letter-spacing:-0.02em; color:var(--ma-h1); margin:0 0 4px; } +.ma-inner { max-width:1120px; margin:0 auto; padding:34px 30px 48px; } +.ma-h1 { font-size:24px; font-weight:650; letter-spacing:-0.02em; color:var(--ma-h1); margin:0 0 4px; } .ma-sub { font-size:clamp(13px,1.5cqw,14px); color:var(--ma-sub); margin:0 0 clamp(14px,2cqw,20px); } .ma-tabs { display:flex; gap:6px; margin-bottom:clamp(14px,2cqw,22px); } .ma-tab { border:1px solid var(--ma-border); background:transparent; color:var(--ma-sub); border-radius:999px; padding:5px 14px; font-size:13px; font-weight:600; cursor:pointer; } diff --git a/apps/x/apps/renderer/src/components/bases-view.tsx b/apps/x/apps/renderer/src/components/bases-view.tsx index b1f5413c..3fe191e6 100644 --- a/apps/x/apps/renderer/src/components/bases-view.tsx +++ b/apps/x/apps/renderer/src/components/bases-view.tsx @@ -466,7 +466,7 @@ export function BasesView({ tree, onSelectNote, config, onConfigChange, isDefaul return (
{/* Toolbar */} -
+
-

+

Persistent agents that fire on a schedule or in response to events. Toggle a task inactive to pause it.

-
+
+
{loading ? (
@@ -1826,16 +1824,17 @@ export function BgTasksView({ onCreateWithCopilot, onEditWithCopilot, initialSlu {isRunning ? ( -
+
- Updating… + Updating
)} +
-
-
- -
-
-
What are we working on?
-
Ask anything, or start with a suggestion.
-
-
- +
-
- Get started +
+ What are we working on?
-
- {SUGGESTED_ACTIONS.map((action) => ( - - ))} +
+ Ask anything, or start with a suggestion.
- +
+ {SUGGESTED_ACTIONS.map((action, i) => ( + + ))} +
+ +
) } diff --git a/apps/x/apps/renderer/src/components/home-view.tsx b/apps/x/apps/renderer/src/components/home-view.tsx index 43b5978b..cc834004 100644 --- a/apps/x/apps/renderer/src/components/home-view.tsx +++ b/apps/x/apps/renderer/src/components/home-view.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import { ArrowRight, Bot, Calendar, Clock, ExternalLink, FileText, Mail, MessageSquare, Mic, Plus, Video } from 'lucide-react' import { extractConferenceLink } from '@/lib/calendar-event' import { ToolConnectionsCard } from '@/components/tool-connections-card' +import { SlackIcon } from '@/components/onboarding/provider-icons' interface TreeNode { path: string @@ -331,15 +332,12 @@ export function HomeView({ {/* Up-next hero */} {nextEvent && ( -
-
- -
+
-
+
Up next · {nextEvent.isAllDay ? 'today' : relativeFromNow(nextEvent.start)}
-
{nextEvent.summary}
+
{nextEvent.summary}
{nextEvent.isAllDay ? 'All day' : `${timeOfDay(nextEvent.start)}${nextEvent.end ? ` – ${timeOfDay(nextEvent.end)}` : ''}`} {nextEvent.location ? ` · ${nextEvent.location}` : ''} @@ -432,7 +430,7 @@ export function HomeView({ {slackEnabled && (
- + Slack Latest messages @@ -548,9 +546,6 @@ export function HomeView({ onClick={onOpenChat} className="flex items-center gap-3.5 rounded-xl border border-border bg-card p-4 text-left transition-colors hover:bg-accent" > -
- -
Ask anything — create presentations, do research, collaborate on docs. diff --git a/apps/x/apps/renderer/src/components/knowledge-view.tsx b/apps/x/apps/renderer/src/components/knowledge-view.tsx index 1d5b1397..862bf416 100644 --- a/apps/x/apps/renderer/src/components/knowledge-view.tsx +++ b/apps/x/apps/renderer/src/components/knowledge-view.tsx @@ -1,6 +1,5 @@ import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react' import { - ArrowLeft, ChevronRight, Copy, ExternalLink, @@ -70,20 +69,6 @@ const HIDDEN_PATHS = new Set(['knowledge/Meetings', 'knowledge/Workspace']) // Theme-aware accent palette for folder avatars — colored letter on a faint // tint of the same hue. Mirrors the design's six-colour rotation. -const AVATAR_PALETTE = [ - 'bg-indigo-500/10 text-indigo-600 dark:text-indigo-400', - 'bg-violet-500/10 text-violet-600 dark:text-violet-400', - 'bg-amber-500/10 text-amber-600 dark:text-amber-400', - 'bg-rose-500/10 text-rose-600 dark:text-rose-400', - 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400', - 'bg-sky-500/10 text-sky-600 dark:text-sky-400', -] as const - -function avatarClass(name: string): string { - let hash = 0 - for (let i = 0; i < name.length; i++) hash = (hash * 31 + name.charCodeAt(i)) >>> 0 - return AVATAR_PALETTE[hash % AVATAR_PALETTE.length] -} function isMarkdown(node: TreeNode): boolean { return node.kind === 'file' && node.name.toLowerCase().endsWith('.md') @@ -203,11 +188,11 @@ export function KnowledgeView({ const currentFolder = folderPath ? findNode(tree, folderPath) : null return ( -
-
+
+
-

Brain

-

+

Brain

+

{totalNotes} {totalNotes === 1 ? 'note' : 'notes'} across {folders.length}{' '} {folders.length === 1 ? 'folder' : 'folders'}

@@ -242,12 +227,12 @@ export function KnowledgeView({ {graphContent}
) : mode === 'basis' ? ( -
+
{basisContent}
) : (
-
+
{currentFolder ? ( ) : ( <> - + {folders.length === 0 ? ( ) : ( -
+
{folders.map((node, i) => (
0 && 'border-t border-border/60')}> 0 && (
-
+
{looseNotes.map((node, i) => (
0 && 'border-t border-border/60')}> - + {label} ) @@ -409,7 +394,7 @@ function QuickAction({ function SectionHeader({ label, aside }: { label: string; aside?: string }) { return (
- + {label} {aside && {aside}} @@ -425,20 +410,6 @@ function EmptyState({ text }: { text: string }) { ) } -function FolderAvatar({ name, className }: { name: string; className?: string }) { - return ( -
- {name.charAt(0).toUpperCase() || '?'} -
- ) -} - function FolderCard({ node, actions, @@ -472,9 +443,8 @@ function FolderCard({ onOpenFolder(node.path) } }} - className="group flex w-full cursor-pointer items-start gap-3 px-4 py-3 text-left transition-colors hover:bg-accent/50" + className="group flex w-full cursor-pointer items-center gap-3 px-4 py-3 text-left transition-colors hover:bg-accent/50" > -
{renameActive ? ( )} -
- {count} {count === 1 ? 'note' : 'notes'} +
+ + {count} {count === 1 ? 'note' : 'notes'} + + {peek.length > 0 && ( + + {peek.map((n) => ( + + {' · '} + + + ))} + + )}
- {peek.length > 0 && ( -
- {peek.map((n) => ( - - ))} -
- )}
-
+
{modified} @@ -565,17 +539,6 @@ function FolderDetail({ return ( <>
-
-

+

Upcoming events and meeting notes.

+
-
+
{loading ? (
@@ -1329,6 +1327,7 @@ export function MeetingsView({ onOpenNote, onTakeMeetingNotes, meetingState, mee
)}
+
) diff --git a/apps/x/apps/renderer/src/components/onboarding/provider-icons.tsx b/apps/x/apps/renderer/src/components/onboarding/provider-icons.tsx index c58c7bf0..24eea525 100644 --- a/apps/x/apps/renderer/src/components/onboarding/provider-icons.tsx +++ b/apps/x/apps/renderer/src/components/onboarding/provider-icons.tsx @@ -75,6 +75,38 @@ export function SlackIcon({ className }: IconProps) { ) } +export function GitHubIcon({ className }: IconProps) { + return ( + + + + ) +} + +export function DiscordIcon({ className }: IconProps) { + return ( + + + + ) +} + +export function WhatsAppIcon({ className }: IconProps) { + return ( + + + + ) +} + +export function TelegramIcon({ className }: IconProps) { + return ( + + + + ) +} + export function FirefliesIcon({ className }: IconProps) { return ( diff --git a/apps/x/apps/renderer/src/components/settings-dialog.tsx b/apps/x/apps/renderer/src/components/settings-dialog.tsx index 248c6cc6..4b56eb51 100644 --- a/apps/x/apps/renderer/src/components/settings-dialog.tsx +++ b/apps/x/apps/renderer/src/components/settings-dialog.tsx @@ -2,7 +2,7 @@ import * as React from "react" import { useState, useEffect, useCallback, useMemo } from "react" -import { Server, Key, Shield, Palette, Monitor, Sun, Moon, Loader2, CheckCircle2, Plus, X, Wrench, Search, ChevronRight, Link2, Tags, Mail, BookOpen, User, Plug, HelpCircle, MessageCircle, Bug, Terminal, AlertTriangle, RefreshCw, PanelRight, Bell, Smartphone } from "lucide-react" +import { Server, Key, Shield, Palette, Monitor, Sun, Moon, Loader2, CheckCircle2, Plus, X, Wrench, Search, ChevronRight, Link2, Tags, Mail, BookOpen, User, Plug, HelpCircle, MessageCircle, Terminal, AlertTriangle, RefreshCw, PanelRight, Bell, Smartphone } from "lucide-react" import { Dialog, @@ -23,6 +23,7 @@ import { Switch } from "@/components/ui/switch" import { cn } from "@/lib/utils" import { useTheme } from "@/contexts/theme-context" import { toast } from "sonner" +import { AnthropicIcon, DiscordIcon, GenericApiIcon, GitHubIcon, GoogleIcon, OllamaIcon, OpenAIIcon, OpenRouterIcon, VercelIcon } from "@/components/onboarding/provider-icons" import { AccountSettings } from "@/components/settings/account-settings" import { ConnectedAccountsSettings } from "@/components/settings/connected-accounts-settings" import { MobileChannelsSettings } from "@/components/settings/mobile-channels-settings" @@ -112,6 +113,13 @@ const tabs: TabConfig[] = [ }, ] +/** Sidebar nav grouping: identity first, capabilities, then app-level. */ +const NAV_SECTIONS: { label: string | null; ids: ConfigTab[] }[] = [ + { label: null, ids: ["account", "connections", "mobile"] }, + { label: "Configure", ids: ["models", "mcp", "security", "code-mode", "note-tagging"] }, + { label: "App", ids: ["appearance", "notifications", "help"] }, +] + interface SettingsDialogProps { /** Optional trigger element. Omit when controlling `open` externally. */ children?: React.ReactNode @@ -136,9 +144,7 @@ function HelpSettings() { className="w-full justify-start gap-3 h-auto py-3" onClick={() => window.open("https://github.com/rowboatlabs/rowboat/issues/new", "_blank")} > -
- -
+
Report a bug Send feedback to the Rowboat team @@ -149,9 +155,7 @@ function HelpSettings() { className="w-full justify-start gap-3 h-auto py-3" onClick={() => window.open("https://discord.com/invite/wajrgmJQ6b", "_blank")} > -
- -
+
Join our Discord Chat with the community @@ -162,9 +166,7 @@ function HelpSettings() { className="w-full justify-start gap-3 h-auto py-3" onClick={() => window.open("mailto:contact@rowboatlabs.com", "_blank")} > -
- -
+
Contact us contact@rowboatlabs.com @@ -313,17 +315,17 @@ interface LlmModelOption { release_date?: string } -const primaryProviders: Array<{ id: LlmProviderFlavor; name: string; description: string }> = [ - { id: "openai", name: "OpenAI", description: "GPT models" }, - { id: "anthropic", name: "Anthropic", description: "Claude models" }, - { id: "google", name: "Gemini", description: "Google AI Studio" }, - { id: "ollama", name: "Ollama (Local)", description: "Run models locally" }, +const primaryProviders: Array<{ id: LlmProviderFlavor; name: string; description: string; icon: React.ElementType }> = [ + { id: "openai", name: "OpenAI", description: "GPT models", icon: OpenAIIcon }, + { id: "anthropic", name: "Anthropic", description: "Claude models", icon: AnthropicIcon }, + { id: "google", name: "Gemini", description: "Google AI Studio", icon: GoogleIcon }, + { id: "ollama", name: "Ollama (Local)", description: "Run models locally", icon: OllamaIcon }, ] -const moreProviders: Array<{ id: LlmProviderFlavor; name: string; description: string }> = [ - { id: "openrouter", name: "OpenRouter", description: "Multiple models, one key" }, - { id: "aigateway", name: "AI Gateway (Vercel)", description: "Vercel's AI Gateway" }, - { id: "openai-compatible", name: "OpenAI-Compatible", description: "Custom OpenAI-compatible API" }, +const moreProviders: Array<{ id: LlmProviderFlavor; name: string; description: string; icon: React.ElementType }> = [ + { id: "openrouter", name: "OpenRouter", description: "Multiple models, one key", icon: OpenRouterIcon }, + { id: "aigateway", name: "AI Gateway (Vercel)", description: "Vercel's AI Gateway", icon: VercelIcon }, + { id: "openai-compatible", name: "OpenAI-Compatible", description: "Custom OpenAI-compatible API", icon: GenericApiIcon }, ] const preferredDefaults: Partial> = { @@ -647,7 +649,7 @@ function ModelSettings({ dialogOpen, rowboatConnected = false }: { dialogOpen: b } }, [defaultProvider, providerConfigs, rowboatConnected]) - const renderProviderCard = (p: { id: LlmProviderFlavor; name: string; description: string }) => { + const renderProviderCard = (p: { id: LlmProviderFlavor; name: string; description: string; icon: React.ElementType }) => { const isDefault = defaultProvider === p.id const isSelected = provider === p.id const hasModel = providerConfigs[p.id].models[0]?.trim().length > 0 @@ -665,7 +667,8 @@ function ModelSettings({ dialogOpen, rowboatConnected = false }: { dialogOpen: b : "border-border hover:bg-accent" )} > -
+
+ {p.name} {isDefault && !rowboatConnected && ( @@ -1869,7 +1872,11 @@ function AgentStatusRow({ const ready = installed && status?.signedIn return (
- + {agent === 'claude' ? ( + + ) : ( + + )}
{name}
@@ -2323,34 +2330,47 @@ export function SettingsDialog({ children, defaultTab = "account", open: control
{/* Sidebar */}
-
-

Settings

+
+

Settings

-
{/* Main content */}
{/* Header */} -
-

{activeTabConfig.label}

-

+

+

{activeTabConfig.label}

+

{activeTab === "models" && rowboatConnected ? "Select your default models" : activeTabConfig.description} @@ -2358,7 +2378,7 @@ export function SettingsDialog({ children, defaultTab = "account", open: control

{/* Content */} -
+
{activeTab === "account" ? ( ) : activeTab === "connections" ? ( diff --git a/apps/x/apps/renderer/src/components/settings/connected-accounts-settings.tsx b/apps/x/apps/renderer/src/components/settings/connected-accounts-settings.tsx index 4b9eb802..08db114d 100644 --- a/apps/x/apps/renderer/src/components/settings/connected-accounts-settings.tsx +++ b/apps/x/apps/renderer/src/components/settings/connected-accounts-settings.tsx @@ -1,9 +1,9 @@ "use client" import * as React from "react" -import { Loader2, Mic, Mail, Calendar, MessageSquare } from "lucide-react" +import { Loader2, Calendar } from "lucide-react" +import { FirefliesIcon, GoogleIcon, SlackIcon } from "@/components/onboarding/provider-icons" import { Button } from "@/components/ui/button" -import { Separator } from "@/components/ui/separator" import { Switch } from "@/components/ui/switch" import { Textarea } from "@/components/ui/textarea" import { GoogleClientIdModal } from "@/components/google-client-id-modal" @@ -47,9 +47,7 @@ export function ConnectedAccountsSettings({ dialogOpen }: ConnectedAccountsSetti className="flex items-center justify-between gap-2 rounded-md px-3 py-2 hover:bg-accent/50 transition-colors" >
-
- {icon} -
+ {icon}
{displayName} {state.isLoading ? ( @@ -145,9 +143,7 @@ export function ConnectedAccountsSettings({ dialogOpen }: ConnectedAccountsSetti {c.useComposioForGoogle ? (
-
- -
+
Gmail {c.gmailLoading ? ( @@ -189,14 +185,12 @@ export function ConnectedAccountsSettings({ dialogOpen }: ConnectedAccountsSetti
) : ( - c.providers.includes('google') && renderOAuthProvider('google', 'Google', , 'Sync emails and calendar') + c.providers.includes('google') && renderOAuthProvider('google', 'Google', , 'Sync emails and calendar') )} {c.useComposioForGoogleCalendar && (
-
- -
+
Google Calendar {c.googleCalendarLoading ? ( @@ -238,7 +232,6 @@ export function ConnectedAccountsSettings({ dialogOpen }: ConnectedAccountsSetti
)} - )} @@ -252,14 +245,13 @@ export function ConnectedAccountsSettings({ dialogOpen }: ConnectedAccountsSetti
{/* Fireflies */} - {renderOAuthProvider('fireflies-ai', 'Fireflies', , 'AI meeting transcripts')} + {renderOAuthProvider('fireflies-ai', 'Fireflies', , 'AI meeting transcripts')} )} {/* Team Communication Section */} <> - -
+
Team Communication @@ -267,9 +259,7 @@ export function ConnectedAccountsSettings({ dialogOpen }: ConnectedAccountsSetti
-
- -
+
Slack {c.slackLoading ? ( @@ -408,8 +398,7 @@ export function ConnectedAccountsSettings({ dialogOpen }: ConnectedAccountsSetti {/* Knowledge Sources Section */} {c.slackEnabled && ( <> - -
+
Knowledge Sources @@ -417,9 +406,7 @@ export function ConnectedAccountsSettings({ dialogOpen }: ConnectedAccountsSetti
-
- -
+
Slack to knowledge diff --git a/apps/x/apps/renderer/src/components/settings/mobile-channels-settings.tsx b/apps/x/apps/renderer/src/components/settings/mobile-channels-settings.tsx index 9974ee94..0e34c08e 100644 --- a/apps/x/apps/renderer/src/components/settings/mobile-channels-settings.tsx +++ b/apps/x/apps/renderer/src/components/settings/mobile-channels-settings.tsx @@ -2,10 +2,10 @@ import { useCallback, useEffect, useState } from "react" import type { z } from "zod" -import { Coffee, Loader2, MessageCircle, Send, Smartphone } from "lucide-react" +import { Coffee, Loader2, Smartphone } from "lucide-react" +import { TelegramIcon, WhatsAppIcon } from "@/components/onboarding/provider-icons" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" -import { Separator } from "@/components/ui/separator" import { Switch } from "@/components/ui/switch" import { toast } from "sonner" import type { ChannelsConfig, ChannelsStatus } from "@x/shared/src/channels.js" @@ -104,9 +104,7 @@ export function MobileChannelsSettings({ dialogOpen }: { dialogOpen: boolean }) {/* Caffeinate */}
-
- -
+
Caffeinate @@ -131,15 +129,12 @@ export function MobileChannelsSettings({ dialogOpen }: { dialogOpen: boolean }) />
- {/* WhatsApp */}
-
- -
+
WhatsApp @@ -228,15 +223,12 @@ export function MobileChannelsSettings({ dialogOpen }: { dialogOpen: boolean }) )}
- {/* Telegram */}
-
- -
+
Telegram diff --git a/apps/x/apps/renderer/src/components/sidebar-content.tsx b/apps/x/apps/renderer/src/components/sidebar-content.tsx index b6b42c9e..2d17d957 100644 --- a/apps/x/apps/renderer/src/components/sidebar-content.tsx +++ b/apps/x/apps/renderer/src/components/sidebar-content.tsx @@ -15,7 +15,11 @@ import { Home, LayoutGrid, Mic, + MoreVertical, + Pencil, + Pin, SquarePen, + Trash2, Plug, LoaderIcon, Mail, @@ -61,6 +65,12 @@ import { TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" import { cn } from "@/lib/utils" import { isOutOfCredits, CREDIT_EXHAUSTED_EVENT, CREDIT_REPLENISHED_EVENT } from "@/lib/credit-status" import { SettingsDialog } from "@/components/settings-dialog" @@ -127,6 +137,8 @@ type ServiceEventType = z.infer const MAX_SYNC_EVENTS = 1000 const RUN_STALE_MS = 2 * 60 * 60 * 1000 +const PINNED_CHATS_STORAGE_KEY = 'x:pinned-chats' +const MAX_PINNED_CHATS = 3 const SERVICE_LABELS: Record = { gmail: "Syncing Gmail", @@ -171,6 +183,10 @@ type SidebarContentPanelProps = { onOpenAgent?: (slug: string) => void recentRuns?: { id: string; title?: string; createdAt: string; modifiedAt?: string }[] onOpenRun?: (runId: string) => void + /** Persist a custom chat title (sessions:setTitle) and refresh the runs list. */ + onRenameRun?: (runId: string, title: string) => void + /** Delete the chat's session (sessions:delete) and refresh the runs list. */ + onDeleteRun?: (runId: string) => void onOpenChatHistory?: () => void onOpenEmail?: (threadId?: string) => void onOpenHome?: () => void @@ -422,6 +438,8 @@ export function SidebarContentPanel({ onOpenApps, recentRuns = [], onOpenRun, + onRenameRun, + onDeleteRun, onOpenChatHistory, onOpenEmail, onOpenHome, @@ -548,16 +566,54 @@ export function SidebarContentPanel({ .slice(0, 10) }, [tree]) - // Chats: the 5 most recently modified chats, newest first. + // Pinned chats: a per-machine UI preference, persisted in localStorage. + const [pinnedChatIds, setPinnedChatIds] = useState(() => { + try { + const raw = window.localStorage.getItem(PINNED_CHATS_STORAGE_KEY) + const parsed: unknown = raw ? JSON.parse(raw) : [] + return Array.isArray(parsed) ? parsed.filter((x): x is string => typeof x === 'string') : [] + } catch { + return [] + } + }) + const toggleChatPin = useCallback((chatId: string) => { + const isPinned = pinnedChatIds.includes(chatId) + if (!isPinned && pinnedChatIds.length >= MAX_PINNED_CHATS) { + toast(`You can pin up to ${MAX_PINNED_CHATS} chats`, 'error') + return + } + const next = isPinned ? pinnedChatIds.filter((id) => id !== chatId) : [...pinnedChatIds, chatId] + try { + window.localStorage.setItem(PINNED_CHATS_STORAGE_KEY, JSON.stringify(next)) + } catch { /* ignore */ } + setPinnedChatIds(next) + }, [pinnedChatIds]) + + // Chats: pinned first, then the most recently modified, 10 rows total. const recentChats = React.useMemo(() => { const chatRecency = (r: { createdAt: string; modifiedAt?: string }) => { const ms = new Date(r.modifiedAt ?? r.createdAt).getTime() return Number.isFinite(ms) ? ms : 0 } - return [...recentRuns] - .sort((a, b) => chatRecency(b) - chatRecency(a)) - .slice(0, 10) - }, [recentRuns]) + const sorted = [...recentRuns].sort((a, b) => chatRecency(b) - chatRecency(a)) + const pinned = sorted.filter((r) => pinnedChatIds.includes(r.id)) + const rest = sorted.filter((r) => !pinnedChatIds.includes(r.id)) + return [...pinned, ...rest.slice(0, Math.max(0, 10 - pinned.length))] + }, [recentRuns, pinnedChatIds]) + + // Chat pending delete confirmation, if any. + const [deleteChatTarget, setDeleteChatTarget] = useState<{ id: string; title: string } | null>(null) + + // Inline chat rename: which row is editing and its draft text. + const [renamingChatId, setRenamingChatId] = useState(null) + const [renameDraft, setRenameDraft] = useState('') + const commitChatRename = useCallback((chatId: string) => { + const title = renameDraft.trim() + const current = recentChats.find((c) => c.id === chatId) + setRenamingChatId(null) + if (!title || title === (current?.title ?? '')) return + onRenameRun?.(chatId, title) + }, [renameDraft, recentChats, onRenameRun]) // Workspace count for the Workspaces sublabel — top-level dir children of // knowledge/Workspace (matches WorkspaceView's root listing). @@ -737,9 +793,16 @@ export function SidebarContentPanel({ {/* Top spacer to clear the traffic lights + fixed toggle row */}
{/* Quick actions */} -
+
{onNewChat && ( - + )} knowledgeActions.createNote()} /> @@ -764,9 +827,9 @@ export function SidebarContentPanel({ data-tour-id="nav-email" isActive={activeNav === 'email'} onClick={() => onOpenEmail?.()} - className={previewEmail ? 'h-auto py-1.5' : undefined} + className={previewEmail ? 'h-auto items-start py-1.5' : undefined} > - +
Email {previewEmail && ( @@ -782,14 +845,22 @@ export function SidebarContentPanel({ )} + {codeModeEnabled && ( + + + + Code + + + )} - +
Meetings {meetingSublabel && ( @@ -861,22 +932,14 @@ export function SidebarContentPanel({
) : null}
- {codeModeEnabled && ( - - - - Code - - - )} knowledgeActions.openKnowledgeView()} - className={knowledgeUpdatedLabel ? 'h-auto py-1.5' : undefined} + className={knowledgeUpdatedLabel ? 'h-auto items-start py-1.5' : undefined} > - +
Brain {knowledgeUpdatedLabel && ( @@ -890,16 +953,26 @@ export function SidebarContentPanel({
+ + + + Apps + + - +
- Background agents + Background agents {bgAgentsLabel && ( - - - - Apps - - knowledgeActions.openWorkspaceAt()} - className="h-auto py-1.5" + className="h-auto items-start py-1.5" > - +
- Workspaces + Workspaces {workspaceCount === 0 ? 'No workspaces' : `${workspaceCount} workspace${workspaceCount === 1 ? '' : 's'}`} @@ -964,10 +1027,75 @@ export function SidebarContentPanel({ {recentChats.map((chat) => ( - onOpenRun?.(chat.id)}> - - {chat.title || '(Untitled chat)'} - + {renamingChatId === chat.id ? ( +
+ + setRenameDraft(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault() + commitChatRename(chat.id) + } else if (e.key === 'Escape') { + e.preventDefault() + setRenamingChatId(null) + } + }} + onBlur={() => commitChatRename(chat.id)} + className="h-6 min-w-0 flex-1 rounded-sm border border-border bg-background px-1.5 text-sm outline-none focus:ring-1 focus:ring-ring" + /> +
+ ) : ( + <> + onOpenRun?.(chat.id)} className={onRenameRun ? 'pr-7' : undefined}> + + {chat.title || '(Untitled chat)'} + {pinnedChatIds.includes(chat.id) && ( + + )} + + {onRenameRun && ( + + + + + + toggleChatPin(chat.id)}> + + {pinnedChatIds.includes(chat.id) ? 'Unpin' : 'Pin'} + + { + setRenameDraft(chat.title || '') + setRenamingChatId(chat.id) + }} + > + + Rename + + {onDeleteRun && ( + setDeleteChatTarget({ id: chat.id, title: chat.title || '(Untitled chat)' })} + > + + Delete + + )} + + + )} + + )}
))} {onOpenChatHistory && ( @@ -986,6 +1114,28 @@ export function SidebarContentPanel({ )} + { if (!open) setDeleteChatTarget(null) }}> + + + Delete chat? + + “{deleteChatTarget?.title}” and its full history will be permanently deleted. + + + + Cancel + { + if (deleteChatTarget) onDeleteRun?.(deleteChatTarget.id) + setDeleteChatTarget(null) + }} + > + Delete + + + + {/* Billing / upgrade CTA or Log in CTA */} {isRowboatConnected && billing ? (() => { @@ -1398,7 +1548,7 @@ path: ${currentRelativePath} if (!hasDeepgramKey) return null - const actionClass = "flex h-9 flex-1 items-center justify-center rounded-md border border-sidebar-border text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground transition-colors" + const actionClass = "flex size-8 shrink-0 items-center justify-center rounded-md text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground transition-colors" const iconClass = "text-sidebar-foreground/70 hover:text-sidebar-foreground hover:bg-sidebar-accent rounded p-1.5 transition-colors" return ( @@ -1432,7 +1582,7 @@ function ActionButton({ icon: Icon, label, onClick }: { icon: typeof Mic; label: type="button" onClick={onClick} aria-label={label} - className="flex h-9 flex-1 items-center justify-center rounded-md border border-sidebar-border text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground transition-colors" + className="flex size-8 shrink-0 items-center justify-center rounded-md text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground transition-colors" > diff --git a/apps/x/apps/renderer/src/components/tool-connections-card.tsx b/apps/x/apps/renderer/src/components/tool-connections-card.tsx index 70c0d3fe..cc99fae0 100644 --- a/apps/x/apps/renderer/src/components/tool-connections-card.tsx +++ b/apps/x/apps/renderer/src/components/tool-connections-card.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useState } from 'react' -import { ArrowRight, Plug } from 'lucide-react' +import { ArrowRight } from 'lucide-react' import { SettingsDialog } from '@/components/settings-dialog' import { cn } from '@/lib/utils' @@ -55,7 +55,7 @@ function ToolkitPreviewIcon({ ) } -export function ToolConnectionsCard({ className }: { className?: string }) { +export function ToolConnectionsCard({ className, compact = false }: { className?: string; compact?: boolean }) { const [toolkitPreviews, setToolkitPreviews] = useState(cachedToolkitPreviews ?? []) const [toolkitLogosLoaded, setToolkitLogosLoaded] = useState(cachedToolkitLogosLoaded) const [connectionsSettingsOpen, setConnectionsSettingsOpen] = useState(false) @@ -101,11 +101,8 @@ export function ToolConnectionsCard({ className }: { className?: string }) { <>
-
- -
-
+
Bring context from and take action in the apps you already use.
@@ -119,7 +116,10 @@ export function ToolConnectionsCard({ className }: { className?: string }) { {breadcrumbs.map((crumb, idx) => { const isLast = idx === breadcrumbs.length - 1 @@ -482,12 +480,13 @@ export function WorkspaceView({ tree, initialPath, actions, onNavigate, onOpenNo
+
{items.length === 0 ? (
@@ -594,6 +593,7 @@ export function WorkspaceView({ tree, initialPath, actions, onNavigate, onOpenNo })}
)} +
{dropEnabled && isDraggingOver && (