feat: blend title bar, remove menu, add agent activity indicators

- Configure hidden title bar with titleBarOverlay matching dark theme
- Remove native menu bar via Menu.setApplicationMenu(null)
- Fix backgroundColor mismatch (#0f172a -> #09090b)
- Add full-width drag region in AppShell for window dragging
- Adjust sidebar and chat headers with top padding for overlay zone
- Add ThinkingDots component with animated dot sequence
- Show activity indicator in chat when agent is processing
- Replace 'Generating...' spinner with inline blinking cursor
- Refine header status badge to subtle pulsing dot
- Add BACKEND_UI_CHANGES.md documenting future sidecar protocol additions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-03-21 12:43:44 +01:00
co-authored by Copilot
parent 37a460ed1d
commit b9e9662f6e
6 changed files with 182 additions and 19 deletions
+3
View File
@@ -9,6 +9,9 @@ interface AppShellProps {
export function AppShell({ sidebar, content, overlay }: AppShellProps) {
return (
<div className="relative flex h-screen bg-[var(--color-surface-0)] text-zinc-100">
{/* Full-width drag region matching the title bar overlay height */}
<div className="drag-region absolute inset-x-0 top-0 z-10 h-10" />
<aside className="flex w-72 shrink-0 flex-col border-r border-[var(--color-border)] bg-[var(--color-surface-1)]">
{sidebar}
</aside>
+39 -15
View File
@@ -5,6 +5,16 @@ import type { PatternDefinition } from '@shared/domain/pattern';
import type { ProjectRecord } from '@shared/domain/project';
import type { SessionRecord } from '@shared/domain/session';
function ThinkingDots() {
return (
<div className="flex items-center gap-1.5">
<span className="thinking-dot size-2 rounded-full bg-zinc-500" />
<span className="thinking-dot size-2 rounded-full bg-zinc-500" />
<span className="thinking-dot size-2 rounded-full bg-zinc-500" />
</div>
);
}
interface ChatPaneProps {
project: ProjectRecord;
pattern: PatternDefinition;
@@ -17,14 +27,16 @@ export function ChatPane({ project, pattern, session, onSend }: ChatPaneProps) {
const transcriptRef = useRef<HTMLDivElement>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const isBusy = session.status === 'running';
const hasPendingMessage = session.messages.some((m) => m.pending);
const isThinking = isBusy && !hasPendingMessage;
useEffect(() => {
transcriptRef.current?.scrollTo({
top: transcriptRef.current.scrollHeight,
behavior: 'smooth',
});
}, [session.messages.length]);
const isBusy = session.status === 'running';
}, [session.messages.length, isBusy]);
async function handleSubmit() {
const text = input.trim();
@@ -42,8 +54,8 @@ export function ChatPane({ project, pattern, session, onSend }: ChatPaneProps) {
return (
<div className="flex h-full flex-col">
{/* Header */}
<header className="flex items-center justify-between border-b border-[var(--color-border)] px-6 py-3">
{/* Header — extra top padding clears the title bar overlay zone */}
<header className="flex items-center justify-between border-b border-[var(--color-border)] px-6 pb-3 pt-12">
<div className="min-w-0">
<h2 className="truncate text-sm font-semibold text-zinc-100">{session.title}</h2>
<p className="mt-0.5 truncate text-[12px] text-zinc-500">
@@ -52,10 +64,7 @@ export function ChatPane({ project, pattern, session, onSend }: ChatPaneProps) {
</div>
<div className="flex items-center gap-2">
{session.status === 'running' && (
<div className="flex items-center gap-1.5 text-[12px] text-blue-400">
<Loader2 className="size-3.5 animate-spin" />
Running
</div>
<span className="size-2 animate-pulse rounded-full bg-blue-400" />
)}
{session.status === 'error' && (
<div className="flex items-center gap-1.5 text-[12px] text-red-400">
@@ -107,19 +116,34 @@ export function ChatPane({ project, pattern, session, onSend }: ChatPaneProps) {
</div>
<div className="whitespace-pre-wrap text-[14px] leading-relaxed text-zinc-200">
{message.content}
{message.pending && message.content && (
<span className="ml-0.5 inline-block h-[18px] w-[2px] animate-pulse rounded-sm bg-zinc-400 align-text-bottom" />
)}
</div>
{message.pending && (
<div className="mt-2 flex items-center gap-1.5 text-[12px] text-zinc-500">
<Loader2 className="size-3 animate-spin" />
Generating...
</div>
)}
{message.pending && !message.content && <ThinkingDots />}
</div>
</div>
</div>
);
})}
</div>
{/* Activity indicator — shown while the agent is thinking (before streaming starts) */}
{isThinking && (
<div className="py-3">
<div className="flex gap-3">
<div className="mt-0.5 flex size-7 shrink-0 items-center justify-center rounded-full bg-zinc-800 text-zinc-400">
<Bot className="size-3.5" />
</div>
<div className="min-w-0 flex-1">
<div className="mb-1.5 text-[12px] font-medium text-zinc-500">
{pattern.agents[0]?.name ?? 'Agent'}
</div>
<ThinkingDots />
</div>
</div>
</div>
)}
</div>
)}
</div>
+2 -2
View File
@@ -98,8 +98,8 @@ export function Sidebar({
}: SidebarProps) {
return (
<div className="flex h-full flex-col">
{/* Header */}
<div className="flex items-center justify-between border-b border-[var(--color-border)] px-4 py-3">
{/* Header — extra top padding clears the title bar overlay zone */}
<div className="flex items-center justify-between border-b border-[var(--color-border)] px-4 pb-3 pt-12">
<div className="flex items-center gap-2">
<div className="flex size-7 items-center justify-center rounded-lg bg-indigo-600 text-[11px] font-bold text-white">
K
+35
View File
@@ -61,6 +61,41 @@ textarea {
background: #52525b;
}
/* Window drag regions for custom title bar */
.drag-region {
-webkit-app-region: drag;
}
.no-drag {
-webkit-app-region: no-drag;
}
/* Thinking dots animation */
@keyframes thinking-dot {
0%,
60%,
100% {
opacity: 0.3;
transform: scale(0.8);
}
30% {
opacity: 1;
transform: scale(1);
}
}
.thinking-dot {
animation: thinking-dot 1.4s ease-in-out infinite;
}
.thinking-dot:nth-child(2) {
animation-delay: 0.2s;
}
.thinking-dot:nth-child(3) {
animation-delay: 0.4s;
}
/* Auto-resize textarea helper */
.auto-resize-textarea {
field-sizing: content;