mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-03 10:28:43 +02:00
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:
@@ -0,0 +1,93 @@
|
||||
# Backend UI Changes
|
||||
|
||||
This document describes changes to the .NET sidecar / backend protocol that would enable richer agent-activity reporting in the chat UI. These are **not yet implemented** — the current UI infers activity state from existing events. A separate agent should implement these changes.
|
||||
|
||||
## Context
|
||||
|
||||
The chat UI now shows a "Thinking…" indicator while the agent processes a request (before streaming starts) and a blinking cursor while the response streams in. These states are inferred from the existing `status` and `message-delta` session events.
|
||||
|
||||
To display more granular activity (e.g. "Using tool X…", "Agent Y is thinking…", "Handing off to Agent Z…"), the sidecar protocol needs a new event kind.
|
||||
|
||||
## Proposed protocol addition
|
||||
|
||||
### New event kind: `agent-activity`
|
||||
|
||||
Add a new value `'agent-activity'` to `SessionEventKind` in `src/shared/domain/event.ts`:
|
||||
|
||||
```typescript
|
||||
export type SessionEventKind =
|
||||
| 'status'
|
||||
| 'message-delta'
|
||||
| 'message-complete'
|
||||
| 'agent-activity' // ← new
|
||||
| 'error';
|
||||
```
|
||||
|
||||
### New fields on `SessionEventRecord`
|
||||
|
||||
```typescript
|
||||
export interface SessionEventRecord {
|
||||
sessionId: string;
|
||||
kind: SessionEventKind;
|
||||
occurredAt: string;
|
||||
|
||||
// Existing fields…
|
||||
status?: 'idle' | 'running' | 'error';
|
||||
messageId?: string;
|
||||
authorName?: string;
|
||||
contentDelta?: string;
|
||||
error?: string;
|
||||
|
||||
// New fields for 'agent-activity' events
|
||||
activityType?: 'thinking' | 'tool-calling' | 'handoff' | 'completed';
|
||||
agentName?: string;
|
||||
toolName?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### Sidecar event mapping
|
||||
|
||||
The .NET sidecar should emit `agent-activity` events at these points:
|
||||
|
||||
| MAF lifecycle point | `activityType` | `agentName` | `toolName` |
|
||||
|---|---|---|---|
|
||||
| Agent begins processing a turn | `thinking` | agent name | — |
|
||||
| Agent invokes a tool | `tool-calling` | agent name | tool name |
|
||||
| Handoff orchestration transfers control | `handoff` | target agent name | — |
|
||||
| Agent finishes its contribution | `completed` | agent name | — |
|
||||
|
||||
### .NET sidecar changes
|
||||
|
||||
In the sidecar's turn-execution pipeline, emit a new JSON event type alongside the existing `turn-delta` and `turn-complete`:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "agent-activity",
|
||||
"requestId": "…",
|
||||
"sessionId": "…",
|
||||
"activityType": "tool-calling",
|
||||
"agentName": "Code Reviewer",
|
||||
"toolName": "read_file"
|
||||
}
|
||||
```
|
||||
|
||||
The Electron main process (`KopayaAppService`) should map this to a `SessionEventRecord` with `kind: 'agent-activity'` and forward it to the renderer via the existing `sessions:event` channel.
|
||||
|
||||
### Renderer consumption (already prepared)
|
||||
|
||||
Once these events are available, the `ChatPane` activity indicator can be enhanced to show contextual messages like:
|
||||
|
||||
- "Code Reviewer is thinking…"
|
||||
- "Code Reviewer is using read_file…"
|
||||
- "Handing off to Summarizer…"
|
||||
|
||||
The `ThinkingDots` component and activity indicator section in `ChatPane.tsx` are designed to be extended with this data.
|
||||
|
||||
## Files to change
|
||||
|
||||
| Layer | File | Change |
|
||||
|---|---|---|
|
||||
| Shared | `src/shared/domain/event.ts` | Add `'agent-activity'` to `SessionEventKind`, add optional `activityType` / `agentName` / `toolName` fields |
|
||||
| Main | `src/main/sidecar/sidecar.ts` | Parse `agent-activity` events from sidecar JSON output |
|
||||
| Main | `src/main/KopayaAppService.ts` | Map parsed activity events to `SessionEventRecord` and emit via `session-event` |
|
||||
| Sidecar | `sidecar/src/Kopaya.AgentHost/…` | Emit `agent-activity` JSON events during MAF turn execution |
|
||||
@@ -1,14 +1,22 @@
|
||||
import { BrowserWindow, shell } from 'electron';
|
||||
import { BrowserWindow, Menu, shell } from 'electron';
|
||||
import { join } from 'node:path';
|
||||
|
||||
export function createMainWindow(): BrowserWindow {
|
||||
Menu.setApplicationMenu(null);
|
||||
|
||||
const window = new BrowserWindow({
|
||||
width: 1440,
|
||||
height: 960,
|
||||
minWidth: 1120,
|
||||
minHeight: 720,
|
||||
title: 'kopaya',
|
||||
backgroundColor: '#0f172a',
|
||||
backgroundColor: '#09090b',
|
||||
titleBarStyle: 'hidden',
|
||||
titleBarOverlay: {
|
||||
color: '#09090b',
|
||||
symbolColor: '#a1a1aa',
|
||||
height: 40,
|
||||
},
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
contextIsolation: true,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user