From 32f8de64853d9a98c9896dd5ed8aac922ff90951 Mon Sep 17 00:00:00 2001 From: Copilot CLI Date: Sat, 21 Mar 2026 11:22:51 +0100 Subject: [PATCH] feat: redesign UI for modern look and clean UX - Refined dark theme with zinc palette and indigo accents - Clean sidebar showing only project/session tree - Moved pattern management to full-screen settings panel - New session modal with project + pattern picker - Modern chat with avatar icons, inline send button, Enter-to-send - Welcome pane when no session is selected - Added lucide-react for consistent iconography - Custom scrollbar styling and auto-resize textarea - Removed clutter: no sidebar pattern list, no verbose headers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bun.lock | 3 + package.json | 1 + src/renderer/App.tsx | 256 +++++------- src/renderer/components/AppShell.tsx | 12 +- src/renderer/components/ChatPane.tsx | 211 +++++----- src/renderer/components/NewSessionModal.tsx | 102 +++++ src/renderer/components/PatternEditor.tsx | 424 ++++++++++---------- src/renderer/components/SettingsPanel.tsx | 119 ++++++ src/renderer/components/Sidebar.tsx | 278 +++++++------ src/renderer/components/WelcomePane.tsx | 63 +++ src/renderer/styles.css | 40 ++ 11 files changed, 911 insertions(+), 598 deletions(-) create mode 100644 src/renderer/components/NewSessionModal.tsx create mode 100644 src/renderer/components/SettingsPanel.tsx create mode 100644 src/renderer/components/WelcomePane.tsx diff --git a/bun.lock b/bun.lock index d2dd391..546c7fa 100644 --- a/bun.lock +++ b/bun.lock @@ -6,6 +6,7 @@ "name": "kopaya", "dependencies": { "keytar": "^7.9.0", + "lucide-react": "^0.577.0", "react": "^19.2.4", "react-dom": "^19.2.4", }, @@ -404,6 +405,8 @@ "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + "lucide-react": ["lucide-react@0.577.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A=="], + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], "matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="], diff --git a/package.json b/package.json index 3f0a36f..42467b0 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ }, "dependencies": { "keytar": "^7.9.0", + "lucide-react": "^0.577.0", "react": "^19.2.4", "react-dom": "^19.2.4" } diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 62c1b48..0919573 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -2,24 +2,21 @@ import { useEffect, useMemo, useState } from 'react'; import { AppShell } from '@renderer/components/AppShell'; import { ChatPane } from '@renderer/components/ChatPane'; -import { PatternEditor } from '@renderer/components/PatternEditor'; +import { NewSessionModal } from '@renderer/components/NewSessionModal'; +import { SettingsPanel } from '@renderer/components/SettingsPanel'; import { Sidebar } from '@renderer/components/Sidebar'; +import { WelcomePane } from '@renderer/components/WelcomePane'; import { getElectronApi } from '@renderer/lib/electronApi'; import type { PatternDefinition } from '@shared/domain/pattern'; -import { createBuiltinPatterns } from '@shared/domain/pattern'; import type { WorkspaceState } from '@shared/domain/workspace'; import { createId, nowIso } from '@shared/utils/ids'; -function clonePattern(pattern: PatternDefinition): PatternDefinition { - return structuredClone(pattern); -} - function createDraftPattern(): PatternDefinition { const timestamp = nowIso(); return { - id: createId('pattern'), + id: createId('custom-pattern'), name: 'New Pattern', - description: 'Reusable orchestration pattern.', + description: '', mode: 'single', availability: 'available', maxIterations: 1, @@ -27,7 +24,7 @@ function createDraftPattern(): PatternDefinition { { id: createId('agent'), name: 'Primary Agent', - description: 'General-purpose project assistant.', + description: 'General-purpose assistant.', instructions: 'You are a helpful coding assistant working inside the selected project.', model: 'gpt-5.4', reasoningEffort: 'high', @@ -38,68 +35,25 @@ function createDraftPattern(): PatternDefinition { }; } -function EmptyDetail() { - const patterns = createBuiltinPatterns(nowIso()); - return ( -
-
-
Workspace Overview
-

Chat-first orchestration across projects

-

- Select a pattern to edit it, add one or more projects on the left, and start sessions that bind a - project folder to a reusable orchestration blueprint. -

-
- {patterns.map((pattern) => ( -
-
-

{pattern.name}

- - {pattern.mode} - -
-

{pattern.description}

-
- ))} -
-
-
- ); -} - export default function App() { const api = getElectronApi(); const [workspace, setWorkspace] = useState(); - const [draftPattern, setDraftPattern] = useState(null); const [error, setError] = useState(); + const [showSettings, setShowSettings] = useState(false); + const [showNewSession, setShowNewSession] = useState(false); + + // Load workspace on mount useEffect(() => { let disposed = false; void api .loadWorkspace() - .then((nextWorkspace) => { - if (!disposed) { - setWorkspace(nextWorkspace); - } - }) - .catch((nextError) => { - if (!disposed) { - setError(nextError instanceof Error ? nextError.message : String(nextError)); - } - }); + .then((ws) => !disposed && setWorkspace(ws)) + .catch((e) => !disposed && setError(e instanceof Error ? e.message : String(e))); - const offWorkspace = api.onWorkspaceUpdated((nextWorkspace) => { - setWorkspace(nextWorkspace); + const offWorkspace = api.onWorkspaceUpdated((ws) => { + setWorkspace(ws); setError(undefined); }); @@ -109,118 +63,114 @@ export default function App() { }; }, [api]); - useEffect(() => { - if (!workspace?.selectedPatternId) { - setDraftPattern(null); - return; - } - - const selectedPattern = workspace.patterns.find((pattern) => pattern.id === workspace.selectedPatternId); - setDraftPattern(selectedPattern ? clonePattern(selectedPattern) : null); - }, [workspace?.lastUpdatedAt, workspace?.selectedPatternId, workspace?.patterns]); - + // Derived state const selectedSession = useMemo( - () => workspace?.sessions.find((session) => session.id === workspace.selectedSessionId), + () => workspace?.sessions.find((s) => s.id === workspace.selectedSessionId), [workspace?.selectedSessionId, workspace?.sessions], ); - const selectedPattern = useMemo( + const patternForSession = useMemo( () => - draftPattern ?? - workspace?.patterns.find((pattern) => pattern.id === workspace.selectedPatternId), - [draftPattern, workspace?.patterns, workspace?.selectedPatternId], + selectedSession + ? workspace?.patterns.find((p) => p.id === selectedSession.patternId) + : undefined, + [selectedSession, workspace?.patterns], ); - const selectedProject = useMemo( - () => workspace?.projects.find((project) => project.id === workspace.selectedProjectId), - [workspace?.projects, workspace?.selectedProjectId], + const projectForSession = useMemo( + () => + selectedSession + ? workspace?.projects.find((p) => p.id === selectedSession.projectId) + : undefined, + [selectedSession, workspace?.projects], ); + // Loading state if (!workspace) { return ( -
- Loading workspace… +
+
Loading workspace…
); } - const patternForSession = selectedSession - ? workspace.patterns.find((pattern) => pattern.id === selectedSession.patternId) - : undefined; - const projectForSession = selectedSession - ? workspace.projects.find((project) => project.id === selectedSession.projectId) - : undefined; + // Determine main content + let content: React.ReactNode; + if (error) { + content = ( +
+
+

Something went wrong

+

{error}

+
+
+ ); + } else if (selectedSession && patternForSession && projectForSession) { + content = ( + api.sendSessionMessage({ sessionId: selectedSession.id, content: c })} + pattern={patternForSession} + project={projectForSession} + session={selectedSession} + /> + ); + } else { + content = ( + 0} + onAddProject={() => void api.addProject()} + onNewSession={() => setShowNewSession(true)} + onOpenSettings={() => setShowSettings(true)} + /> + ); + } + + // Settings overlay + const overlay = showSettings ? ( + setShowSettings(false)} + onDeletePattern={async (id) => { + await api.deletePattern(id); + }} + onNewPattern={createDraftPattern} + onSavePattern={async (pattern) => { + await api.savePattern({ pattern }); + }} + patterns={workspace.patterns} + /> + ) : null; return ( - -
-
Error
-

Something went wrong

-

{error}

-
-
- ) : selectedSession && patternForSession && projectForSession ? ( - api.sendSessionMessage({ sessionId: selectedSession.id, content })} - pattern={patternForSession} - project={projectForSession} - session={selectedSession} - /> - ) : selectedPattern ? ( - { - await api.deletePattern(selectedPattern.id); - } - } - onSave={async () => { - await api.savePattern({ pattern: draftPattern ?? selectedPattern }); + <> + void api.addProject()} + onNewSession={() => setShowNewSession(true)} + onOpenSettings={() => setShowSettings(true)} + onProjectSelect={(projectId) => { + void api.selectProject(projectId); }} - pattern={draftPattern ?? selectedPattern} + onSessionSelect={(sessionId) => { + void api.selectSession(sessionId); + }} + workspace={workspace} /> - ) : ( - - ) - } - sidebar={ - { - void api.addProject(); - }} - onCreateSession={() => { - if (!workspace.selectedProjectId || !workspace.selectedPatternId) { - return; - } + } + /> - void api.createSession({ - projectId: workspace.selectedProjectId, - patternId: workspace.selectedPatternId, - }); + {showNewSession && ( + setShowNewSession(false)} + onCreate={(projectId, patternId) => { + setShowNewSession(false); + void api.createSession({ projectId, patternId }); }} - onNewPattern={() => { - setDraftPattern(createDraftPattern()); - void api.selectPattern(undefined); - void api.selectSession(undefined); - }} - onPatternSelect={(patternId) => { - void api.selectPattern(patternId); - void api.selectSession(undefined); - }} - onProjectSelect={(projectId) => { - void api.selectProject(projectId); - void api.selectSession(undefined); - }} - onSessionSelect={(sessionId) => { - void api.selectSession(sessionId); - }} - workspace={workspace} + patterns={workspace.patterns} + projects={workspace.projects} /> - } - /> + )} + ); } diff --git a/src/renderer/components/AppShell.tsx b/src/renderer/components/AppShell.tsx index 90cebac..ed95f1a 100644 --- a/src/renderer/components/AppShell.tsx +++ b/src/renderer/components/AppShell.tsx @@ -3,13 +3,17 @@ import type { ReactNode } from 'react'; interface AppShellProps { sidebar: ReactNode; content: ReactNode; + overlay?: ReactNode; } -export function AppShell({ sidebar, content }: AppShellProps) { +export function AppShell({ sidebar, content, overlay }: AppShellProps) { return ( -
- -
{content}
+
+ +
{content}
+ {overlay}
); } diff --git a/src/renderer/components/ChatPane.tsx b/src/renderer/components/ChatPane.tsx index 61f5f1f..aa50211 100644 --- a/src/renderer/components/ChatPane.tsx +++ b/src/renderer/components/ChatPane.tsx @@ -1,4 +1,5 @@ -import { useEffect, useMemo, useRef, useState } from 'react'; +import { type KeyboardEvent, useEffect, useRef, useState } from 'react'; +import { AlertCircle, ArrowUp, Bot, Loader2, User } from 'lucide-react'; import type { PatternDefinition } from '@shared/domain/pattern'; import type { ProjectRecord } from '@shared/domain/project'; @@ -14,6 +15,7 @@ interface ChatPaneProps { export function ChatPane({ project, pattern, session, onSend }: ChatPaneProps) { const [input, setInput] = useState(''); const transcriptRef = useRef(null); + const textareaRef = useRef(null); useEffect(() => { transcriptRef.current?.scrollTo({ @@ -23,117 +25,144 @@ export function ChatPane({ project, pattern, session, onSend }: ChatPaneProps) { }, [session.messages.length]); const isBusy = session.status === 'running'; - const sessionStats = useMemo( - () => `${session.messages.length} messages • ${pattern.agents.length} agent${pattern.agents.length === 1 ? '' : 's'}`, - [pattern.agents.length, session.messages.length], - ); + + async function handleSubmit() { + const text = input.trim(); + if (!text || isBusy) return; + setInput(''); + await onSend(text); + } + + function handleKeyDown(e: KeyboardEvent) { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + void handleSubmit(); + } + } return ( -
-
-
-
-
{project.name}
-

{session.title}

-

- Pattern: {pattern.name} • Mode:{' '} - {pattern.mode} -

-

{sessionStats}

-
-
- {session.status} -
+
+ {/* Header */} +
+
+

{session.title}

+

+ {project.name} · {pattern.name} · {pattern.mode} +

+
+
+ {session.status === 'running' && ( +
+ + Running +
+ )} + {session.status === 'error' && ( +
+ + Error +
+ )} + {session.status === 'idle' && session.messages.length > 0 && ( + + {session.messages.length} message{session.messages.length === 1 ? '' : 's'} + + )}
-
+ {/* Messages */} +
{session.messages.length === 0 ? ( -
- Start the conversation to launch this orchestration against {project.path}. +
+ +

+ Send a message to start the conversation +

+

+ Using {pattern.name} in{' '} + {project.name} +

) : ( -
- {session.messages.map((message) => { - const isUser = message.role === 'user'; - return ( -
-
-
- {message.authorName} +
+
+ {session.messages.map((message) => { + const isUser = message.role === 'user'; + return ( +
+
+
+ {isUser ? : } +
+
+
+ {message.authorName} +
+
+ {message.content} +
+ {message.pending && ( +
+ + Generating... +
+ )} +
-
{message.content}
- {message.pending ? ( -
Streaming…
- ) : null}
-
- ); - })} + ); + })} +
)}
-
- {session.lastError ? ( -
- {session.lastError} + {/* Input area */} +
+ {session.lastError && ( +
+ + {session.lastError}
- ) : null} + )} -
{ - event.preventDefault(); - if (!input.trim()) { - return; - } - - const nextInput = input; - setInput(''); - await onSend(nextInput); - }} - > -