Merge pull request #711 from rowboatlabs/fix/heading-ui-cleanup

UI polish: unified headers, settings redesign, chat management
This commit is contained in:
gagan 2026-07-10 01:13:56 +05:30 committed by GitHub
commit 45d9002a3a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 524 additions and 328 deletions

View file

@ -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;

View file

@ -51,6 +51,7 @@ import {
import {
Message,
MessageContent,
MessageCopyButton,
MessageResponse,
} from '@/components/ai-elements/message';
import {
@ -5891,14 +5892,17 @@ function App() {
<ChatMessageAttachments attachments={item.attachments} />
</MessageContent>
{item.content && (
<MessageContent>
<MessageResponse
components={streamdownComponents}
remarkPlugins={userMessageRemarkPlugins}
>
{item.content}
</MessageResponse>
</MessageContent>
<div className="flex flex-col items-end">
<MessageContent>
<MessageResponse
components={streamdownComponents}
remarkPlugins={userMessageRemarkPlugins}
>
{item.content}
</MessageResponse>
</MessageContent>
<MessageCopyButton text={item.content} className="mt-0.5" />
</div>
)}
</Message>
)
@ -5906,26 +5910,29 @@ function App() {
const { message, files } = parseAttachedFiles(item.content)
return (
<Message key={item.id} from={item.role} data-message-id={item.id}>
<MessageContent>
{files.length > 0 && (
<div className="flex flex-wrap gap-1.5 mb-2">
{files.map((filePath, index) => (
<span
key={index}
className="inline-flex items-center gap-1 text-xs bg-primary/10 text-primary px-2 py-0.5 rounded-full"
>
@{wikiLabel(filePath)}
</span>
))}
</div>
)}
<MessageResponse
components={streamdownComponents}
remarkPlugins={userMessageRemarkPlugins}
>
{message}
</MessageResponse>
</MessageContent>
<div className="flex flex-col items-end">
<MessageContent>
{files.length > 0 && (
<div className="flex flex-wrap gap-1.5 mb-2">
{files.map((filePath, index) => (
<span
key={index}
className="inline-flex items-center gap-1 text-xs bg-primary/10 text-primary px-2 py-0.5 rounded-full"
>
@{wikiLabel(filePath)}
</span>
))}
</div>
)}
<MessageResponse
components={streamdownComponents}
remarkPlugins={userMessageRemarkPlugins}
>
{message}
</MessageResponse>
</MessageContent>
<MessageCopyButton text={message} className="mt-0.5" />
</div>
</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' })}

View file

@ -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 (
<button
type="button"
aria-label="Copy message"
onClick={() => {
void navigator.clipboard.writeText(text);
setCopied(true);
window.setTimeout(() => setCopied(false), 1200);
}}
className={cn(
"shrink-0 rounded-md p-1.5 text-muted-foreground/60 opacity-0 transition-opacity hover:bg-accent hover:text-foreground group-hover:opacity-100",
className
)}
>
{copied ? <CheckIcon className="size-3.5" /> : <CopyIcon className="size-3.5" />}
</button>
);
};
export type MessageContentProps = HTMLAttributes<HTMLDivElement>;
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
)}

View file

@ -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; }

View file

@ -466,7 +466,7 @@ export function BasesView({ tree, onSelectNote, config, onConfigChange, isDefaul
return (
<div className="flex-1 flex flex-col overflow-hidden">
{/* Toolbar */}
<div className="shrink-0 border-b border-border px-4 py-2 flex items-center gap-3">
<div className="shrink-0 border-b border-border pr-4 py-2 flex items-center gap-3">
<Popover>
<PopoverTrigger asChild>
<button className="inline-flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground">
@ -884,7 +884,7 @@ function NoteRow({
const row = (
<tr
className="border-b border-border/50 hover:bg-accent/50 cursor-pointer transition-colors"
className="border-b border-black/10 dark:border-border/50 hover:bg-accent/50 cursor-pointer transition-colors"
onClick={() => onSelectNote(note.path)}
>
{visibleColumns.map((col) => (

View file

@ -1714,22 +1714,20 @@ export function BgTasksView({ onCreateWithCopilot, onEditWithCopilot, initialSlu
}
return (
<div className="flex h-full flex-col overflow-hidden">
<div className="shrink-0 border-b border-border px-6 py-5">
<div className="flex h-full flex-col overflow-hidden bg-[#f8f8f9] dark:bg-[#0b0b0d]">
<div className="mx-auto w-full max-w-[1120px] shrink-0 px-[30px] pt-[34px] pb-5">
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-2">
<ListChecks className="size-5 text-primary" />
<h2 className="text-base font-semibold text-foreground">Background tasks</h2>
</div>
<h2 className="text-[24px] font-[650] tracking-[-0.02em] text-[#0d0e11] dark:text-[#f4f5f7]">Background tasks</h2>
<Button size="sm" onClick={() => setShowNewDialog(true)}>
New task
</Button>
</div>
<p className="mt-1 text-xs text-muted-foreground">
<p className="mt-1 text-[14px] text-black/50 dark:text-white/[0.52]">
Persistent agents that fire on a schedule or in response to events. Toggle a task inactive to pause it.
</p>
</div>
<div className="flex-1 overflow-auto p-6">
<div className="flex-1 overflow-auto">
<div className="mx-auto h-full w-full max-w-[1120px] px-[30px] pb-12">
{loading ? (
<div className="flex h-full items-center justify-center">
<Loader2 className="size-5 animate-spin text-muted-foreground" />
@ -1826,16 +1824,17 @@ export function BgTasksView({ onCreateWithCopilot, onEditWithCopilot, initialSlu
</td>
<td className="px-4 py-3">
{isRunning ? (
<div className="flex items-center gap-2">
<div className="flex flex-wrap items-center gap-2 pl-7">
<span className="inline-flex items-center gap-1.5 rounded-md bg-primary/10 px-2 py-0.5 text-xs font-medium text-foreground animate-pulse">
<Loader2 className="size-3 animate-spin" />
Updating
Updating
</span>
<Button
variant="destructive"
size="sm"
onClick={() => handleStop(task.slug)}
disabled={isStopping}
className="h-auto gap-1.5 rounded-md px-2 py-0.5 text-xs font-medium"
>
{isStopping ? <Loader2 className="size-3 animate-spin" /> : <Square className="size-3" />}
Stop
@ -1866,6 +1865,7 @@ export function BgTasksView({ onCreateWithCopilot, onEditWithCopilot, initialSlu
</table>
</div>
)}
</div>
</div>
<NewTaskDialog

View file

@ -1,4 +1,4 @@
import { Bot, Mail, Sparkles, Telescope } from 'lucide-react'
import { ArrowRight, BookOpen, Mail, Zap } from 'lucide-react'
import { cn } from '@/lib/utils'
import { ToolConnectionsCard } from '@/components/tool-connections-card'
@ -12,53 +12,55 @@ interface ChatEmptyStateProps {
const SUGGESTED_ACTIONS: { icon: typeof Mail; title: string; sub: string; prompt: string }[] = [
{ icon: Mail, title: 'Draft a reply', sub: 'to an email', prompt: "Let's draft a reply to [name]'s email" },
{ icon: Bot, title: 'Set up a background agent', sub: 'that automates tasks', prompt: 'Set up a background agent that automates [task]' },
{ icon: Telescope, title: 'Research a topic', sub: 'create a local wiki for me', prompt: 'Research [topic] and create a local wiki for me' },
{ icon: Zap, title: 'Set up a background agent', sub: 'that automates tasks', prompt: 'Set up a background agent that automates [task]' },
{ icon: BookOpen, title: 'Research a topic', sub: 'create a local wiki for me', prompt: 'Research [topic] and create a local wiki for me' },
]
/**
* Empty-state body for the chat surface: greeting and starter action cards.
* Shown in both the side-pane copilot and full-screen chat.
* Shown in both the side-pane copilot and full-screen chat; the side pane
* (`wide` unset) uses slightly smaller type to fit the narrow column.
*/
export function ChatEmptyState({
onPickPrompt,
wide = false,
}: ChatEmptyStateProps) {
return (
<div className={cn('mx-auto flex w-full flex-col gap-6 px-2 py-6', wide ? 'max-w-2xl' : 'max-w-md')}>
<div className="flex items-center gap-3">
<div className="flex size-9 shrink-0 items-center justify-center rounded-[10px] border border-border bg-background text-foreground">
<Sparkles className="size-[17px]" />
</div>
<div>
<div className="text-base font-semibold tracking-tight">What are we working on?</div>
<div className="text-xs text-muted-foreground">Ask anything, or start with a suggestion.</div>
</div>
</div>
<div className={cn('mx-auto flex w-full flex-col gap-5 py-6', wide ? 'max-w-4xl px-4' : 'max-w-md px-2')}>
<div>
<div className="px-1 pb-2 text-[10.5px] font-semibold uppercase tracking-wider text-muted-foreground">
Get started
<div className={cn('font-semibold tracking-tight', wide ? 'text-2xl' : 'text-lg')}>
What are we working on?
</div>
<div className="flex flex-col gap-2">
{SUGGESTED_ACTIONS.map((action) => (
<button
key={action.title}
type="button"
onClick={() => onPickPrompt(action.prompt)}
className="flex items-start gap-2.5 rounded-lg border border-border bg-background px-3 py-2.5 text-left transition-colors hover:bg-accent"
>
<action.icon className="mt-0.5 size-3.5 shrink-0 text-muted-foreground" />
<div className="min-w-0 flex-1">
<div className="text-[12.8px] font-medium">{action.title}</div>
<div className="mt-0.5 text-[11.5px] text-muted-foreground">{action.sub}</div>
</div>
</button>
))}
<div className={cn('mt-1 text-muted-foreground', wide ? 'text-[15px]' : 'text-[13px]')}>
Ask anything, or start with a suggestion.
</div>
</div>
<ToolConnectionsCard />
<div className="overflow-hidden rounded-xl border border-border">
{SUGGESTED_ACTIONS.map((action, i) => (
<button
key={action.title}
type="button"
onClick={() => onPickPrompt(action.prompt)}
className={cn(
'group flex w-full items-center gap-1.5 text-left transition-colors hover:bg-accent/50',
wide ? 'px-3.5 py-3' : 'px-3 py-2.5',
i > 0 && 'border-t border-border/60',
)}
>
<action.icon className={cn('mr-2 shrink-0 text-foreground/80', wide ? 'size-4' : 'size-3.5')} strokeWidth={1.75} />
<span className={cn('shrink-0 font-medium text-foreground', wide ? 'text-sm' : 'text-[13px]')}>
{action.title}
</span>
<span className={cn('truncate text-muted-foreground', wide ? 'text-[13px]' : 'text-[12px]')}>
{action.sub}
</span>
<ArrowRight className={cn('ml-auto shrink-0 text-muted-foreground/50 transition-colors group-hover:text-foreground', wide ? 'size-3.5' : 'size-3')} />
</button>
))}
</div>
<ToolConnectionsCard compact={!wide} />
</div>
)
}

View file

@ -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 && (
<div className="flex items-center gap-[18px] rounded-xl bg-foreground p-[18px] text-background">
<div className="flex size-[52px] shrink-0 items-center justify-center rounded-xl bg-background/10">
<Mic className="size-[22px]" />
</div>
<div className="flex items-center gap-[18px] rounded-xl bg-foreground px-5 py-[18px] text-background">
<div className="min-w-0 flex-1">
<div className="mb-1 text-[11px] uppercase tracking-wide text-background/55">
<div className="mb-1.5 text-[11px] font-medium uppercase tracking-wider text-background/50">
Up next · {nextEvent.isAllDay ? 'today' : relativeFromNow(nextEvent.start)}
</div>
<div className="mb-0.5 truncate text-[17px] font-medium">{nextEvent.summary}</div>
<div className="mb-0.5 truncate text-[17px] font-semibold">{nextEvent.summary}</div>
<div className="truncate text-[13px] text-background/70">
{nextEvent.isAllDay ? 'All day' : `${timeOfDay(nextEvent.start)}${nextEvent.end ? ` ${timeOfDay(nextEvent.end)}` : ''}`}
{nextEvent.location ? ` · ${nextEvent.location}` : ''}
@ -432,7 +430,7 @@ export function HomeView({
{slackEnabled && (
<div className={CARD}>
<div className="mb-3 flex items-center gap-2">
<MessageSquare className="size-[15px]" />
<SlackIcon className="size-[15px]" />
<span className="text-sm font-medium">Slack</span>
<span className="flex-1" />
<span className="text-xs text-muted-foreground">Latest messages</span>
@ -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"
>
<div className="flex size-9 shrink-0 items-center justify-center rounded-lg border border-border bg-card text-muted-foreground">
<MessageSquare className="size-[15px]" />
</div>
<div className="min-w-0 flex-1 text-[13.5px] leading-snug">
<span className="font-medium">Ask anything</span>
<span className="text-muted-foreground"> create presentations, do research, collaborate on docs.</span>

View file

@ -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 (
<div className="flex h-full flex-col overflow-hidden">
<div className="shrink-0 flex items-start justify-between gap-4 border-b border-border px-8 py-6">
<div className="flex h-full flex-col overflow-hidden bg-[#f8f8f9] dark:bg-[#0b0b0d]">
<div className="mx-auto w-full max-w-[1120px] shrink-0 flex items-start justify-between gap-4 px-[30px] pt-[34px] pb-5">
<div className="min-w-0">
<h1 className="text-2xl font-bold tracking-tight">Brain</h1>
<p className="mt-1 text-sm text-muted-foreground">
<h1 className="text-[24px] font-[650] tracking-[-0.02em] text-[#0d0e11] dark:text-[#f4f5f7]">Brain</h1>
<p className="mt-1 text-[14px] text-black/50 dark:text-white/[0.52]">
{totalNotes} {totalNotes === 1 ? 'note' : 'notes'} across {folders.length}{' '}
{folders.length === 1 ? 'folder' : 'folders'}
</p>
@ -242,12 +227,12 @@ export function KnowledgeView({
{graphContent}
</div>
) : mode === 'basis' ? (
<div className="flex-1 min-h-0 overflow-hidden">
<div className="mx-auto flex w-full max-w-[1120px] flex-1 min-h-0 flex-col overflow-hidden px-[30px] pb-6">
{basisContent}
</div>
) : (
<div className="flex-1 overflow-y-auto">
<div className="mx-auto w-full max-w-3xl px-8 py-6">
<div className="mx-auto w-full max-w-[1120px] px-[30px] py-6">
{currentFolder ? (
<FolderDetail
folder={currentFolder}
@ -261,11 +246,11 @@ export function KnowledgeView({
/>
) : (
<>
<SectionHeader label={`Folders · ${folders.length}`} aside="Sorted by name" />
<SectionHeader label={`Folders · ${folders.length}`} />
{folders.length === 0 ? (
<EmptyState text="No folders yet." />
) : (
<div className="overflow-hidden rounded-xl border border-border">
<div className="overflow-hidden rounded-xl border border-black/15 dark:border-border">
{folders.map((node, i) => (
<div key={node.path} className={cn(i > 0 && 'border-t border-border/60')}>
<FolderCard
@ -285,7 +270,7 @@ export function KnowledgeView({
{looseNotes.length > 0 && (
<div className="mt-8">
<SectionHeader label={`Loose notes · ${looseNotes.length}`} />
<div className="overflow-hidden rounded-xl border border-border">
<div className="overflow-hidden rounded-xl border border-black/15 dark:border-border">
{looseNotes.map((node, i) => (
<div key={node.path} className={cn(i > 0 && 'border-t border-border/60')}>
<ItemRow
@ -398,9 +383,9 @@ function QuickAction({
<button
type="button"
onClick={onClick}
className="inline-flex items-center gap-2 rounded-lg border border-border bg-background px-3 py-2 text-sm text-foreground transition-colors hover:bg-accent"
className="inline-flex items-center gap-2 rounded-md px-2.5 py-1.5 text-sm text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
>
<Icon className="size-4 text-muted-foreground" />
<Icon className="size-4" />
<span>{label}</span>
</button>
)
@ -409,7 +394,7 @@ function QuickAction({
function SectionHeader({ label, aside }: { label: string; aside?: string }) {
return (
<div className="mb-2.5 flex items-center justify-between">
<span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
<span className="text-xs font-bold uppercase tracking-wider text-muted-foreground">
{label}
</span>
{aside && <span className="text-xs text-muted-foreground">{aside}</span>}
@ -425,20 +410,6 @@ function EmptyState({ text }: { text: string }) {
)
}
function FolderAvatar({ name, className }: { name: string; className?: string }) {
return (
<div
className={cn(
'flex size-8 shrink-0 items-center justify-center rounded-md text-[13px] font-bold',
avatarClass(name),
className,
)}
>
{name.charAt(0).toUpperCase() || '?'}
</div>
)
}
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"
>
<FolderAvatar name={node.name} className="mt-0.5" />
<div className="min-w-0 flex-1">
{renameActive ? (
<RenameField
@ -489,28 +459,32 @@ function FolderCard({
{node.name}
</span>
)}
<div className="mt-0.5 text-xs text-muted-foreground">
{count} {count === 1 ? 'note' : 'notes'}
<div className="mt-0.5 flex min-w-0 items-baseline gap-1.5 text-xs text-muted-foreground">
<span className="shrink-0">
{count} {count === 1 ? 'note' : 'notes'}
</span>
{peek.length > 0 && (
<span className="truncate text-muted-foreground/70">
{peek.map((n) => (
<span key={n.path}>
<span className="text-muted-foreground/40">{' · '}</span>
<button
type="button"
onClick={(e) => {
e.stopPropagation()
onOpenNote(n.path)
}}
className="transition-colors hover:text-foreground hover:underline"
>
{displayName(n)}
</button>
</span>
))}
</span>
)}
</div>
{peek.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1.5">
{peek.map((n) => (
<button
key={n.path}
type="button"
onClick={(e) => {
e.stopPropagation()
onOpenNote(n.path)
}}
className="max-w-[200px] truncate rounded-full border border-border/60 bg-muted px-2.5 py-0.5 text-xs text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
>
{displayName(n)}
</button>
))}
</div>
)}
</div>
<div className="flex shrink-0 items-center gap-2 pt-1">
<div className="flex shrink-0 items-center gap-2">
<span className="text-xs text-muted-foreground tabular-nums whitespace-nowrap">
{modified}
</span>
@ -565,17 +539,6 @@ function FolderDetail({
return (
<>
<div className="mb-4 flex min-w-0 items-center gap-1.5 text-sm">
<button
type="button"
onClick={() => {
const parent = crumbs.length >= 2 ? crumbs[crumbs.length - 2].path : null
onNavigate(parent)
}}
className="inline-flex items-center gap-1 rounded-md px-1.5 py-1 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
aria-label="Back"
>
<ArrowLeft className="size-4" />
</button>
<button
type="button"
onClick={() => onNavigate(null)}
@ -605,7 +568,7 @@ function FolderDetail({
{items.length === 0 ? (
<EmptyState text="This folder is empty." />
) : (
<div className="overflow-hidden rounded-xl border border-border">
<div className="overflow-hidden rounded-xl border border-black/15 dark:border-border">
{items.map((node, i) => (
<div key={node.path} className={cn(i > 0 && 'border-t border-border/60')}>
<ItemRow
@ -665,13 +628,6 @@ function ItemRow({
}}
className="group flex w-full cursor-pointer items-center gap-3 px-4 py-2.5 text-left transition-colors hover:bg-accent/50"
>
{isDir ? (
<FolderAvatar name={node.name} />
) : (
<div className="flex size-8 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground">
<FileText className="size-4" />
</div>
)}
<div className="min-w-0 flex-1">
{renameActive ? (
<RenameField
@ -682,16 +638,21 @@ function ItemRow({
onDone={onClearRename}
/>
) : (
<span className="block truncate text-sm text-foreground">{displayName(node)}</span>
)}
{isDir && (
<div className="mt-0.5 text-xs text-muted-foreground">
{count} {count === 1 ? 'note' : 'notes'}
</div>
<span className="block truncate text-sm font-semibold text-foreground">
{displayName(node)}
</span>
)}
</div>
<div className="flex shrink-0 items-center gap-2">
<span className="text-xs text-muted-foreground tabular-nums whitespace-nowrap">
<div className="flex shrink-0 items-center gap-2 text-xs text-muted-foreground">
{isDir && (
<>
<span className="whitespace-nowrap">
{count} {count === 1 ? 'note' : 'notes'}
</span>
<span className="text-muted-foreground/40">·</span>
</>
)}
<span className="tabular-nums whitespace-nowrap">
{modified}
</span>
{isDir && (

View file

@ -711,8 +711,8 @@ function UpcomingEvents({ onOpenNote }: { onOpenNote: (path: string) => void })
const todayKey = localDateKey(now)
return (
<section className="border-b border-border/60 px-6 pb-6 pt-5">
<div className="mx-auto w-full max-w-[760px]">
<section className="border-b border-border/60 pb-6 pt-5">
<div className="w-full">
<div className="mb-3 flex items-baseline justify-between">
<h3 className="text-sm font-semibold text-foreground flex items-center gap-2">
<Calendar className="size-4 text-muted-foreground" />
@ -1243,13 +1243,10 @@ export function MeetingsView({ onOpenNote, onTakeMeetingNotes, meetingState, mee
const isRecording = meetingState === 'recording'
return (
<div className="flex h-full flex-col overflow-hidden">
<div className="shrink-0 border-b border-border px-6 py-5">
<div className="flex h-full flex-col overflow-hidden bg-[#f8f8f9] dark:bg-[#0b0b0d]">
<div className="mx-auto w-full max-w-[1120px] shrink-0 px-[30px] pt-[34px] pb-5">
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-2">
<Mic className="size-5 text-primary" />
<h2 className="text-base font-semibold text-foreground">Meetings</h2>
</div>
<h2 className="text-[24px] font-[650] tracking-[-0.02em] text-[#0d0e11] dark:text-[#f4f5f7]">Meetings</h2>
<Button
type="button"
size="sm"
@ -1267,13 +1264,14 @@ export function MeetingsView({ onOpenNote, onTakeMeetingNotes, meetingState, mee
{meetingSummarizing ? 'Generating notes...' : getMeetingButtonLabel(meetingState)}
</Button>
</div>
<p className="mt-1 text-xs text-muted-foreground">
<p className="mt-1 text-[14px] text-black/50 dark:text-white/[0.52]">
Upcoming events and meeting notes.
</p>
</div>
<div className="flex-1 overflow-auto">
<div className="mx-auto w-full max-w-[1120px] px-[30px] pb-12">
<UpcomingEvents onOpenNote={onOpenNote} />
<div className="p-6">
<div className="pt-6">
{loading ? (
<div className="flex items-center justify-center py-10">
<Loader2 className="size-5 animate-spin text-muted-foreground" />
@ -1329,6 +1327,7 @@ export function MeetingsView({ onOpenNote, onTakeMeetingNotes, meetingState, mee
</div>
)}
</div>
</div>
</div>
</div>
)

View file

@ -75,6 +75,38 @@ export function SlackIcon({ className }: IconProps) {
)
}
export function GitHubIcon({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="currentColor" className={cn("size-5", className)}>
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
</svg>
)
}
export function DiscordIcon({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" className={cn("size-5", className)}>
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z" fill="#5865F2" />
</svg>
)
}
export function WhatsAppIcon({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" className={cn("size-5", className)}>
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413Z" fill="#25D366" />
</svg>
)
}
export function TelegramIcon({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" className={cn("size-5", className)}>
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z" fill="#26A5E4" />
</svg>
)
}
export function FirefliesIcon({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="currentColor" className={cn("size-5", className)}>

View file

@ -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")}
>
<div className="flex size-8 items-center justify-center rounded-md bg-destructive/10">
<Bug className="size-4 text-destructive" />
</div>
<GitHubIcon className="size-5 shrink-0" />
<div className="flex flex-col items-start">
<span className="text-sm font-medium">Report a bug</span>
<span className="text-xs text-muted-foreground">Send feedback to the Rowboat team</span>
@ -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")}
>
<div className="flex size-8 items-center justify-center rounded-md bg-[#5865F2]">
<MessageCircle className="size-4 text-white" />
</div>
<DiscordIcon className="size-5 shrink-0" />
<div className="flex flex-col items-start">
<span className="text-sm font-medium">Join our Discord</span>
<span className="text-xs text-muted-foreground">Chat with the community</span>
@ -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")}
>
<div className="flex size-8 items-center justify-center rounded-md bg-muted">
<Mail className="size-4" />
</div>
<Mail className="size-5 shrink-0" />
<div className="flex flex-col items-start">
<span className="text-sm font-medium">Contact us</span>
<span className="text-xs text-muted-foreground">contact@rowboatlabs.com</span>
@ -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<Record<LlmProviderFlavor, string>> = {
@ -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"
)}
>
<div className="flex items-center gap-1.5">
<div className="flex items-center gap-2">
<p.icon className="size-4 shrink-0" />
<span className="text-sm font-medium">{p.name}</span>
{isDefault && !rowboatConnected && (
<span className="rounded-full bg-primary/10 px-1.5 py-0.5 text-[10px] font-medium leading-none text-primary">
@ -1869,7 +1872,11 @@ function AgentStatusRow({
const ready = installed && status?.signedIn
return (
<div className="rounded-md border px-3 py-2.5 flex items-center gap-3">
<Terminal className="size-4 text-muted-foreground shrink-0" />
{agent === 'claude' ? (
<AnthropicIcon className="size-5 shrink-0" />
) : (
<OpenAIIcon className="size-5 shrink-0" />
)}
<div className="flex-1 min-w-0">
<div className="text-sm font-medium">{name}</div>
<div className="text-xs text-muted-foreground mt-0.5 flex items-center gap-3">
@ -2323,34 +2330,47 @@ export function SettingsDialog({ children, defaultTab = "account", open: control
<div className="flex h-full overflow-hidden">
{/* Sidebar */}
<div className="w-48 border-r bg-muted/30 p-2 flex flex-col">
<div className="px-2 py-3 mb-2">
<h2 className="font-semibold text-sm">Settings</h2>
<div className="px-2 pt-3.5 pb-3 mb-2">
<h2 className="font-semibold text-base tracking-tight">Settings</h2>
</div>
<nav className="flex flex-col gap-1">
{visibleTabs.map((tab) => (
<button
key={tab.id}
onClick={() => handleTabChange(tab.id)}
className={cn(
"flex items-center gap-2 px-2 py-2 rounded-md text-sm transition-colors text-left",
activeTab === tab.id
? "bg-background text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground hover:bg-background/50"
)}
>
<tab.icon className="size-4" />
{tab.label}
</button>
))}
<nav className="flex flex-col">
{NAV_SECTIONS.map((section) => {
const sectionTabs = visibleTabs.filter((tab) => section.ids.includes(tab.id))
if (sectionTabs.length === 0) return null
return (
<div key={section.label ?? "main"} className="flex flex-col gap-0.5">
{section.label ? (
<div className="px-2 pb-1 pt-4 text-[10.5px] font-semibold uppercase tracking-wider text-muted-foreground/70">
{section.label}
</div>
) : null}
{sectionTabs.map((tab) => (
<button
key={tab.id}
onClick={() => handleTabChange(tab.id)}
className={cn(
"flex items-center gap-2 px-2 py-1.5 rounded-md text-sm transition-colors text-left",
activeTab === tab.id
? "bg-background text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground hover:bg-background/50"
)}
>
<tab.icon className="size-4" />
{tab.label}
</button>
))}
</div>
)
})}
</nav>
</div>
{/* Main content */}
<div className="flex-1 flex flex-col min-w-0 min-h-0">
{/* Header */}
<div className="px-4 py-3 border-b">
<h3 className="font-medium text-sm">{activeTabConfig.label}</h3>
<p className="text-xs text-muted-foreground mt-0.5">
<div className="px-6 pb-4 pt-5">
<h3 className="text-lg font-semibold tracking-tight">{activeTabConfig.label}</h3>
<p className="text-sm text-muted-foreground mt-1">
{activeTab === "models" && rowboatConnected
? "Select your default models"
: activeTabConfig.description}
@ -2358,7 +2378,7 @@ export function SettingsDialog({ children, defaultTab = "account", open: control
</div>
{/* Content */}
<div className={cn("flex-1 p-4 min-h-0", (activeTab === "models" || activeTab === "connections" || activeTab === "mobile" || activeTab === "account" || activeTab === "code-mode" || activeTab === "notifications") ? "overflow-y-auto" : activeTab === "note-tagging" ? "overflow-hidden flex flex-col" : "overflow-hidden")}>
<div className={cn("flex-1 px-6 pb-5 min-h-0", (activeTab === "models" || activeTab === "connections" || activeTab === "mobile" || activeTab === "account" || activeTab === "code-mode" || activeTab === "notifications") ? "overflow-y-auto" : activeTab === "note-tagging" ? "overflow-hidden flex flex-col" : "overflow-hidden")}>
{activeTab === "account" ? (
<AccountSettings dialogOpen={open} />
) : activeTab === "connections" ? (

View file

@ -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"
>
<div className="flex items-center gap-2.5 min-w-0">
<div className="flex size-8 items-center justify-center rounded-md bg-muted">
{icon}
</div>
{icon}
<div className="flex flex-col min-w-0">
<span className="text-sm font-medium truncate">{displayName}</span>
{state.isLoading ? (
@ -145,9 +143,7 @@ export function ConnectedAccountsSettings({ dialogOpen }: ConnectedAccountsSetti
{c.useComposioForGoogle ? (
<div className="flex items-center justify-between gap-2 rounded-md px-3 py-2 hover:bg-accent/50 transition-colors">
<div className="flex items-center gap-2.5 min-w-0">
<div className="flex size-8 items-center justify-center rounded-md bg-muted">
<Mail className="size-4" />
</div>
<GoogleIcon className="size-5 shrink-0" />
<div className="flex flex-col min-w-0">
<span className="text-sm font-medium truncate">Gmail</span>
{c.gmailLoading ? (
@ -189,14 +185,12 @@ export function ConnectedAccountsSettings({ dialogOpen }: ConnectedAccountsSetti
</div>
</div>
) : (
c.providers.includes('google') && renderOAuthProvider('google', 'Google', <Mail className="size-4" />, 'Sync emails and calendar')
c.providers.includes('google') && renderOAuthProvider('google', 'Google', <GoogleIcon className="size-5" />, 'Sync emails and calendar')
)}
{c.useComposioForGoogleCalendar && (
<div className="flex items-center justify-between gap-2 rounded-md px-3 py-2 hover:bg-accent/50 transition-colors">
<div className="flex items-center gap-2.5 min-w-0">
<div className="flex size-8 items-center justify-center rounded-md bg-muted">
<Calendar className="size-4" />
</div>
<Calendar className="size-5 shrink-0" />
<div className="flex flex-col min-w-0">
<span className="text-sm font-medium truncate">Google Calendar</span>
{c.googleCalendarLoading ? (
@ -238,7 +232,6 @@ export function ConnectedAccountsSettings({ dialogOpen }: ConnectedAccountsSetti
</div>
</div>
)}
<Separator className="my-2" />
</>
)}
@ -252,14 +245,13 @@ export function ConnectedAccountsSettings({ dialogOpen }: ConnectedAccountsSetti
</div>
{/* Fireflies */}
{renderOAuthProvider('fireflies-ai', 'Fireflies', <Mic className="size-4" />, 'AI meeting transcripts')}
{renderOAuthProvider('fireflies-ai', 'Fireflies', <FirefliesIcon className="size-5" />, 'AI meeting transcripts')}
</>
)}
{/* Team Communication Section */}
<>
<Separator className="my-2" />
<div className="px-3 pt-1 pb-0.5">
<div className="px-3 pt-3 pb-0.5">
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
Team Communication
</span>
@ -267,9 +259,7 @@ export function ConnectedAccountsSettings({ dialogOpen }: ConnectedAccountsSetti
<div className="rounded-md px-3 py-2 hover:bg-accent/50 transition-colors">
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2.5 min-w-0">
<div className="flex size-8 items-center justify-center rounded-md bg-muted">
<MessageSquare className="size-4" />
</div>
<SlackIcon className="size-5 shrink-0" />
<div className="flex flex-col min-w-0">
<span className="text-sm font-medium truncate">Slack</span>
{c.slackLoading ? (
@ -408,8 +398,7 @@ export function ConnectedAccountsSettings({ dialogOpen }: ConnectedAccountsSetti
{/* Knowledge Sources Section */}
{c.slackEnabled && (
<>
<Separator className="my-2" />
<div className="px-3 pt-1 pb-0.5">
<div className="px-3 pt-3 pb-0.5">
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
Knowledge Sources
</span>
@ -417,9 +406,7 @@ export function ConnectedAccountsSettings({ dialogOpen }: ConnectedAccountsSetti
<div className="rounded-md px-3 py-2 hover:bg-accent/50 transition-colors">
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2.5 min-w-0">
<div className="flex size-8 items-center justify-center rounded-md bg-muted">
<MessageSquare className="size-4" />
</div>
<SlackIcon className="size-5 shrink-0" />
<div className="flex flex-col min-w-0">
<span className="text-sm font-medium truncate">Slack to knowledge</span>
<span className="text-xs text-muted-foreground truncate">

View file

@ -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 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2.5">
<div className="flex size-8 items-center justify-center rounded-md bg-muted">
<Coffee className="size-4" />
</div>
<Coffee className="size-5 shrink-0" />
<div className="flex flex-col">
<span className="text-sm font-medium">Caffeinate</span>
<span className="text-xs text-muted-foreground">
@ -131,15 +129,12 @@ export function MobileChannelsSettings({ dialogOpen }: { dialogOpen: boolean })
/>
</div>
<Separator />
{/* WhatsApp */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2.5">
<div className="flex size-8 items-center justify-center rounded-md bg-muted">
<MessageCircle className="size-4" />
</div>
<WhatsAppIcon className="size-5 shrink-0" />
<div className="flex flex-col">
<span className="text-sm font-medium">WhatsApp</span>
<span className="text-xs text-muted-foreground">
@ -228,15 +223,12 @@ export function MobileChannelsSettings({ dialogOpen }: { dialogOpen: boolean })
)}
</div>
<Separator />
{/* Telegram */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2.5">
<div className="flex size-8 items-center justify-center rounded-md bg-muted">
<Send className="size-4" />
</div>
<TelegramIcon className="size-5 shrink-0" />
<div className="flex flex-col">
<span className="text-sm font-medium">Telegram</span>
<span className="text-xs text-muted-foreground">

View file

@ -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<typeof ServiceEvent>
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<string, string> = {
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<string[]>(() => {
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<string | null>(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 */}
<div className="h-8" />
{/* Quick actions */}
<div className="titlebar-no-drag flex items-center gap-1.5 px-3 pb-2">
<div className="titlebar-no-drag flex items-center gap-1 pl-3 pr-6 pb-2">
{onNewChat && (
<ActionButton icon={SquarePen} label="New chat" onClick={onNewChat} />
<button
type="button"
onClick={onNewChat}
className="flex h-8 min-w-0 flex-1 items-center justify-center gap-1.5 rounded-md border border-sidebar-border text-[13px] font-medium text-sidebar-foreground transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
>
<SquarePen className="size-3.5" />
New chat
</button>
)}
<ActionButton icon={FilePlus} label="New note" onClick={() => knowledgeActions.createNote()} />
<VoiceNoteButton onNoteCreated={onVoiceNoteCreated} variant="action" />
@ -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}
>
<Mail className="size-4 shrink-0" />
<Mail className={cn('size-4 shrink-0', previewEmail && 'mt-0.5')} />
<div className="flex min-w-0 flex-1 flex-col">
<span className="truncate">Email</span>
{previewEmail && (
@ -782,14 +845,22 @@ export function SidebarContentPanel({
)}
</SidebarMenuButton>
</SidebarMenuItem>
{codeModeEnabled && (
<SidebarMenuItem>
<SidebarMenuButton data-tour-id="nav-code" isActive={activeNav === 'code'} onClick={onOpenCode}>
<Code2 className="size-4 shrink-0" />
<span className="flex-1 truncate">Code</span>
</SidebarMenuButton>
</SidebarMenuItem>
)}
<SidebarMenuItem>
<SidebarMenuButton
data-tour-id="nav-meetings"
isActive={activeNav === 'meetings'}
onClick={onOpenMeetings}
className={meetingSublabel ? 'h-auto py-1.5' : undefined}
className={meetingSublabel ? 'h-auto items-start py-1.5' : undefined}
>
<Mic className={cn('size-4 shrink-0', meetingIsRecording && 'text-red-500')} />
<Mic className={cn('size-4 shrink-0', meetingSublabel && 'mt-1', meetingIsRecording && 'text-red-500')} />
<div className="flex min-w-0 flex-1 flex-col">
<span className="truncate">Meetings</span>
{meetingSublabel && (
@ -861,22 +932,14 @@ export function SidebarContentPanel({
</div>
) : null}
</SidebarMenuItem>
{codeModeEnabled && (
<SidebarMenuItem>
<SidebarMenuButton data-tour-id="nav-code" isActive={activeNav === 'code'} onClick={onOpenCode}>
<Code2 className="size-4 shrink-0" />
<span className="flex-1 truncate">Code</span>
</SidebarMenuButton>
</SidebarMenuItem>
)}
<SidebarMenuItem>
<SidebarMenuButton
data-tour-id="nav-knowledge"
isActive={activeNav === 'knowledge'}
onClick={() => knowledgeActions.openKnowledgeView()}
className={knowledgeUpdatedLabel ? 'h-auto py-1.5' : undefined}
className={knowledgeUpdatedLabel ? 'h-auto items-start py-1.5' : undefined}
>
<FileText className="size-4 shrink-0" />
<FileText className={cn('size-4 shrink-0', knowledgeUpdatedLabel && 'mt-0.5')} />
<div className="flex min-w-0 flex-1 flex-col">
<span className="truncate">Brain</span>
{knowledgeUpdatedLabel && (
@ -890,16 +953,26 @@ export function SidebarContentPanel({
<div className="mx-3 my-2 border-t border-sidebar-border" />
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton
data-tour-id="nav-apps"
isActive={activeNav === 'apps'}
onClick={onOpenApps}
>
<LayoutGrid className="size-4 shrink-0" />
<span className="flex-1 truncate">Apps</span>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton
data-tour-id="nav-agents"
isActive={activeNav === 'agents'}
onClick={onOpenBgTasks}
className={bgAgentsLabel ? 'h-auto py-1.5' : undefined}
className={bgAgentsLabel ? 'h-auto items-start py-1.5' : undefined}
>
<Bot className="size-4 shrink-0 text-muted-foreground" />
<Bot className={cn('size-4 shrink-0', bgAgentsLabel && 'mt-0.5')} />
<div className="flex min-w-0 flex-1 flex-col">
<span className="truncate text-muted-foreground">Background agents</span>
<span className="truncate">Background agents</span>
{bgAgentsLabel && (
<span className={cn(
'truncate text-[11px]',
@ -911,26 +984,16 @@ export function SidebarContentPanel({
</div>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton
data-tour-id="nav-apps"
isActive={activeNav === 'apps'}
onClick={onOpenApps}
>
<LayoutGrid className="size-4 shrink-0 text-muted-foreground" />
<span className="flex-1 truncate text-muted-foreground">Apps</span>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton
data-tour-id="nav-workspaces"
isActive={activeNav === 'workspaces'}
onClick={() => knowledgeActions.openWorkspaceAt()}
className="h-auto py-1.5"
className="h-auto items-start py-1.5"
>
<Folder className="size-4 shrink-0 text-muted-foreground" />
<Folder className="mt-0.5 size-4 shrink-0" />
<div className="flex min-w-0 flex-1 flex-col">
<span className="truncate text-muted-foreground">Workspaces</span>
<span className="truncate">Workspaces</span>
<span className="truncate text-[11px] text-muted-foreground">
{workspaceCount === 0 ? 'No workspaces' : `${workspaceCount} workspace${workspaceCount === 1 ? '' : 's'}`}
</span>
@ -964,10 +1027,75 @@ export function SidebarContentPanel({
<SidebarMenu>
{recentChats.map((chat) => (
<SidebarMenuItem key={chat.id}>
<SidebarMenuButton onClick={() => onOpenRun?.(chat.id)}>
<MessageSquare className="size-4 shrink-0 text-muted-foreground" />
<span className="flex-1 truncate">{chat.title || '(Untitled chat)'}</span>
</SidebarMenuButton>
{renamingChatId === chat.id ? (
<div className="flex h-8 items-center gap-2 rounded-md px-2">
<MessageSquare className="size-4 shrink-0 text-muted-foreground" />
<input
autoFocus
value={renameDraft}
onChange={(e) => 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"
/>
</div>
) : (
<>
<SidebarMenuButton onClick={() => onOpenRun?.(chat.id)} className={onRenameRun ? 'pr-7' : undefined}>
<MessageSquare className="size-4 shrink-0 text-muted-foreground" />
<span className="flex-1 truncate">{chat.title || '(Untitled chat)'}</span>
{pinnedChatIds.includes(chat.id) && (
<Pin className="size-3 shrink-0 text-muted-foreground/70 transition-opacity group-hover/menu-item:opacity-0" />
)}
</SidebarMenuButton>
{onRenameRun && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
aria-label="Chat options"
onClick={(e) => e.stopPropagation()}
className="absolute right-1.5 top-1/2 flex size-5 -translate-y-1/2 items-center justify-center rounded text-muted-foreground opacity-0 transition-opacity hover:text-foreground group-hover/menu-item:opacity-100 data-[state=open]:opacity-100"
>
<MoreVertical className="size-4" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent side="right" align="start">
<DropdownMenuItem onClick={() => toggleChatPin(chat.id)}>
<Pin className="mr-2 size-3.5" />
{pinnedChatIds.includes(chat.id) ? 'Unpin' : 'Pin'}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setRenameDraft(chat.title || '')
setRenamingChatId(chat.id)
}}
>
<Pencil className="mr-2 size-3.5" />
Rename
</DropdownMenuItem>
{onDeleteRun && (
<DropdownMenuItem
className="text-destructive focus:text-destructive"
onClick={() => setDeleteChatTarget({ id: chat.id, title: chat.title || '(Untitled chat)' })}
>
<Trash2 className="mr-2 size-3.5" />
Delete
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
)}
</>
)}
</SidebarMenuItem>
))}
{onOpenChatHistory && (
@ -986,6 +1114,28 @@ export function SidebarContentPanel({
)}
</SidebarGroupContent>
</SidebarGroup>
<AlertDialog open={!!deleteChatTarget} onOpenChange={(open) => { if (!open) setDeleteChatTarget(null) }}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete chat?</AlertDialogTitle>
<AlertDialogDescription>
&ldquo;{deleteChatTarget?.title}&rdquo; and its full history will be permanently deleted.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
className="bg-destructive text-white hover:bg-destructive/90"
onClick={() => {
if (deleteChatTarget) onDeleteRun?.(deleteChatTarget.id)
setDeleteChatTarget(null)
}}
>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</SidebarContent>
{/* 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"
>
<Icon className="size-4" />
</button>

View file

@ -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<ToolkitPreview[]>(cachedToolkitPreviews ?? [])
const [toolkitLogosLoaded, setToolkitLogosLoaded] = useState(cachedToolkitLogosLoaded)
const [connectionsSettingsOpen, setConnectionsSettingsOpen] = useState(false)
@ -101,11 +101,8 @@ export function ToolConnectionsCard({ className }: { className?: string }) {
<>
<div className={cn('rounded-xl border border-border bg-card p-4', className)}>
<div className="flex items-start gap-3">
<div className="flex size-8 shrink-0 items-center justify-center rounded-lg border border-border bg-muted text-muted-foreground">
<Plug className="size-[14px]" />
</div>
<div className="min-w-0 flex-1">
<div className="text-[13.5px] leading-snug">
<div className={cn('leading-snug', compact ? 'text-[12.5px]' : 'text-[13.5px]')}>
<span className="text-muted-foreground">Bring context from and take action in the apps you already use.</span>
</div>
<div className="mt-3 flex min-h-5 flex-wrap items-center gap-1.5">
@ -119,7 +116,10 @@ export function ToolConnectionsCard({ className }: { className?: string }) {
<button
type="button"
onClick={() => setConnectionsSettingsOpen(true)}
className="ml-1 flex h-5 shrink-0 items-center gap-1 rounded-md px-1 text-[12px] font-medium text-primary hover:underline"
className={cn(
'ml-1 flex h-5 shrink-0 items-center gap-1 rounded-md px-1 font-medium text-primary hover:underline',
compact ? 'text-[11.5px]' : 'text-[12px]',
)}
>
Connections
<ArrowRight className="size-3" />

View file

@ -36,7 +36,7 @@ function DialogOverlay({
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/40 backdrop-blur-sm",
className
)}
{...props}

View file

@ -462,7 +462,7 @@ function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
<ul
data-slot="sidebar-menu"
data-sidebar="menu"
className={cn("flex w-full min-w-0 flex-col gap-0", className)}
className={cn("flex w-full min-w-0 flex-col gap-0.5", className)}
{...props}
/>
)

View file

@ -8,7 +8,6 @@ import {
Folder as FolderIcon,
FolderOpen,
FolderPlus,
Home,
Loader2,
MessageSquare,
Pencil,
@ -365,19 +364,18 @@ export function WorkspaceView({ tree, initialPath, actions, onNavigate, onOpenNo
}, [newName, onCreateWorkspace, resetAddDialog])
return (
<div className="flex h-full flex-col overflow-hidden">
<div className="flex shrink-0 items-center justify-between gap-3 border-b border-border px-6 py-4">
<div className="flex h-full flex-col overflow-hidden bg-[#f8f8f9] dark:bg-[#0b0b0d]">
<div className="mx-auto flex w-full max-w-[1120px] shrink-0 items-center justify-between gap-3 pl-[22px] pr-[30px] pt-[30px] pb-4">
<div className="flex min-w-0 items-center gap-1 text-sm">
<button
type="button"
onClick={() => onNavigate(WORKSPACE_ROOT)}
className={cn(
'inline-flex items-center gap-1.5 rounded-md px-2 py-1 transition-colors',
isRoot ? 'text-foreground' : 'text-muted-foreground hover:text-foreground hover:bg-accent',
'inline-flex items-center rounded-md px-2 py-1 transition-colors',
isRoot ? 'text-[#0d0e11] dark:text-[#f4f5f7]' : 'text-muted-foreground hover:text-foreground hover:bg-accent',
)}
>
<Home className="size-4" />
<span className="font-medium">Workspace</span>
<span className="text-[24px] font-[650] tracking-[-0.02em]">Workspace</span>
</button>
{breadcrumbs.map((crumb, idx) => {
const isLast = idx === breadcrumbs.length - 1
@ -482,12 +480,13 @@ export function WorkspaceView({ tree, initialPath, actions, onNavigate, onOpenNo
<div className="flex flex-1 overflow-hidden">
<div
className="relative flex-1 overflow-y-auto px-6 py-6"
className="relative flex-1 overflow-y-auto"
onDragEnter={handleDragEnter}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
<div className="mx-auto h-full w-full max-w-[1120px] px-[30px] py-6">
{items.length === 0 ? (
<div className="flex h-full flex-col items-center justify-center gap-3 text-center text-muted-foreground">
<FolderIcon className="size-10 opacity-50" />
@ -594,6 +593,7 @@ export function WorkspaceView({ tree, initialPath, actions, onNavigate, onOpenNo
})}
</div>
)}
</div>
{dropEnabled && isDraggingOver && (
<div className="pointer-events-none absolute inset-3 z-10 flex flex-col items-center justify-center gap-2 rounded-xl border-2 border-dashed border-primary/60 bg-primary/5 text-primary">