diff --git a/BACKEND_UI_CHANGES.md b/BACKEND_UI_CHANGES.md new file mode 100644 index 0000000..37631ef --- /dev/null +++ b/BACKEND_UI_CHANGES.md @@ -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 | diff --git a/src/main/windows/createMainWindow.ts b/src/main/windows/createMainWindow.ts index 95677a7..5b89ce3 100644 --- a/src/main/windows/createMainWindow.ts +++ b/src/main/windows/createMainWindow.ts @@ -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, diff --git a/src/renderer/components/AppShell.tsx b/src/renderer/components/AppShell.tsx index ed95f1a..9ea2eaa 100644 --- a/src/renderer/components/AppShell.tsx +++ b/src/renderer/components/AppShell.tsx @@ -9,6 +9,9 @@ interface AppShellProps { export function AppShell({ sidebar, content, overlay }: AppShellProps) { return (
+ {/* Full-width drag region matching the title bar overlay height */} +
+ diff --git a/src/renderer/components/ChatPane.tsx b/src/renderer/components/ChatPane.tsx index aa50211..d6ccae5 100644 --- a/src/renderer/components/ChatPane.tsx +++ b/src/renderer/components/ChatPane.tsx @@ -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 ( +
+ + + +
+ ); +} + interface ChatPaneProps { project: ProjectRecord; pattern: PatternDefinition; @@ -17,14 +27,16 @@ export function ChatPane({ project, pattern, session, onSend }: ChatPaneProps) { const transcriptRef = useRef(null); const textareaRef = useRef(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 (
- {/* Header */} -
+ {/* Header — extra top padding clears the title bar overlay zone */} +

{session.title}

@@ -52,10 +64,7 @@ export function ChatPane({ project, pattern, session, onSend }: ChatPaneProps) {

{session.status === 'running' && ( -
- - Running -
+ )} {session.status === 'error' && (
@@ -107,19 +116,34 @@ export function ChatPane({ project, pattern, session, onSend }: ChatPaneProps) {
{message.content} + {message.pending && message.content && ( + + )}
- {message.pending && ( -
- - Generating... -
- )} + {message.pending && !message.content && }
); })}
+ + {/* Activity indicator — shown while the agent is thinking (before streaming starts) */} + {isThinking && ( +
+
+
+ +
+
+
+ {pattern.agents[0]?.name ?? 'Agent'} +
+ +
+
+
+ )} )} diff --git a/src/renderer/components/Sidebar.tsx b/src/renderer/components/Sidebar.tsx index 9f5c7c0..40e61c5 100644 --- a/src/renderer/components/Sidebar.tsx +++ b/src/renderer/components/Sidebar.tsx @@ -98,8 +98,8 @@ export function Sidebar({ }: SidebarProps) { return (
- {/* Header */} -
+ {/* Header — extra top padding clears the title bar overlay zone */} +
K diff --git a/src/renderer/styles.css b/src/renderer/styles.css index c1f5fe5..a14d6d7 100644 --- a/src/renderer/styles.css +++ b/src/renderer/styles.css @@ -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;