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…
+
);
}
- 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' && (
+
+ )}
+ {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 && (
+
- ) : null}
+ )}
-
);
diff --git a/src/renderer/components/NewSessionModal.tsx b/src/renderer/components/NewSessionModal.tsx
new file mode 100644
index 0000000..3c16e3e
--- /dev/null
+++ b/src/renderer/components/NewSessionModal.tsx
@@ -0,0 +1,102 @@
+import { useState } from 'react';
+import { X } from 'lucide-react';
+
+import type { PatternDefinition } from '@shared/domain/pattern';
+import type { ProjectRecord } from '@shared/domain/project';
+
+interface NewSessionModalProps {
+ projects: ProjectRecord[];
+ patterns: PatternDefinition[];
+ defaultProjectId?: string;
+ onClose: () => void;
+ onCreate: (projectId: string, patternId: string) => void;
+}
+
+export function NewSessionModal({
+ projects,
+ patterns,
+ defaultProjectId,
+ onClose,
+ onCreate,
+}: NewSessionModalProps) {
+ const availablePatterns = patterns.filter((p) => p.availability !== 'unavailable');
+ const [projectId, setProjectId] = useState(defaultProjectId ?? projects[0]?.id ?? '');
+ const [patternId, setPatternId] = useState(availablePatterns[0]?.id ?? '');
+
+ const canCreate = projectId && patternId;
+
+ return (
+
+
+ {/* Header */}
+
+
New Session
+
+
+
+
+
+ {/* Body */}
+
+
+ Project
+ setProjectId(e.target.value)}
+ value={projectId}
+ >
+ {projects.map((p) => (
+
+ {p.name}
+
+ ))}
+
+
+
+
+ Pattern
+ setPatternId(e.target.value)}
+ value={patternId}
+ >
+ {availablePatterns.map((p) => (
+
+ {p.name} ({p.mode})
+
+ ))}
+
+ {patternId && (
+
+ {availablePatterns.find((p) => p.id === patternId)?.description}
+
+ )}
+
+
+
+ {/* Footer */}
+
+
+ Cancel
+
+ canCreate && onCreate(projectId, patternId)}
+ type="button"
+ >
+ Start Session
+
+
+
+
+ );
+}
diff --git a/src/renderer/components/PatternEditor.tsx b/src/renderer/components/PatternEditor.tsx
index 2713f36..318f5e3 100644
--- a/src/renderer/components/PatternEditor.tsx
+++ b/src/renderer/components/PatternEditor.tsx
@@ -1,3 +1,5 @@
+import { AlertCircle, CheckCircle, ChevronLeft, Plus, Trash2 } from 'lucide-react';
+
import { validatePatternDefinition, type OrchestrationMode, type PatternDefinition } from '@shared/domain/pattern';
interface PatternEditorProps {
@@ -6,244 +8,254 @@ interface PatternEditorProps {
onChange: (pattern: PatternDefinition) => void;
onDelete?: () => void;
onSave: () => void;
+ onBack: () => void;
}
const modes: OrchestrationMode[] = ['single', 'sequential', 'concurrent', 'handoff', 'group-chat', 'magentic'];
-export function PatternEditor({ pattern, isBuiltin, onChange, onDelete, onSave }: PatternEditorProps) {
+function InputField({
+ label,
+ value,
+ onChange,
+ multiline,
+ placeholder,
+}: {
+ label: string;
+ value: string;
+ onChange: (value: string) => void;
+ multiline?: boolean;
+ placeholder?: string;
+}) {
+ const baseClasses =
+ 'w-full rounded-lg border border-zinc-700 bg-zinc-900 px-3 py-2 text-[13px] text-zinc-100 placeholder-zinc-600 outline-none transition focus:border-indigo-500/50';
+ return (
+
+ {label}
+ {multiline ? (
+ onChange(e.target.value)}
+ placeholder={placeholder}
+ value={value}
+ />
+ ) : (
+ onChange(e.target.value)}
+ placeholder={placeholder}
+ value={value}
+ />
+ )}
+
+ );
+}
+
+export function PatternEditor({ pattern, isBuiltin, onChange, onDelete, onSave, onBack }: PatternEditorProps) {
const issues = validatePatternDefinition(pattern);
+ function updateAgent(agentId: string, patch: Record
) {
+ onChange({
+ ...pattern,
+ agents: pattern.agents.map((a) => (a.id === agentId ? { ...a, ...patch } : a)),
+ });
+ }
+
return (
-
-
+ {/* Header */}
+
+
+
+
+
-
Pattern
-
{pattern.name || 'Untitled pattern'}
-
- Define a reusable orchestration blueprint that can be launched against any project in the
- workspace.
+
+ {pattern.name || 'Untitled pattern'}
+
+
+ {isBuiltin ? 'Built-in pattern' : 'Custom pattern'}
-
- {!isBuiltin && onDelete ? (
-
- Delete
-
- ) : null}
+
+
+ {!isBuiltin && onDelete && (
- Save Pattern
+
+ Delete
-
+ )}
+
+ Save
+
-
-
-
-
-
- Name
- onChange({ ...pattern, name: event.target.value })}
- value={pattern.name}
- />
-
-
- Mode
+ {/* Body */}
+
+
+ {/* Validation banner */}
+ {issues.length > 0 ? (
+
+ {issues.map((issue, i) => (
+
+ ))}
+
+ ) : (
+
+
+ Pattern is valid and ready to use
+
+ )}
+
+ {/* Basic fields */}
+
+
+ General
+
+
+ onChange({ ...pattern, name: v })}
+ placeholder="Pattern name"
+ value={pattern.name}
+ />
+
+ Mode
- onChange({
- ...pattern,
- mode: event.target.value as OrchestrationMode,
- })
+ className="w-full rounded-lg border border-zinc-700 bg-zinc-900 px-3 py-2 text-[13px] text-zinc-100 outline-none transition focus:border-indigo-500/50"
+ onChange={(e) =>
+ onChange({ ...pattern, mode: e.target.value as OrchestrationMode })
}
value={pattern.mode}
>
- {modes.map((mode) => (
-
- {mode}
+ {modes.map((m) => (
+
+ {m}
))}
-
- Description
- onChange({ ...pattern, description: event.target.value })}
- value={pattern.description}
- />
-
-
+
+ onChange({ ...pattern, description: v })}
+ placeholder="What this pattern does..."
+ value={pattern.description}
+ />
+
-
-
-
-
Agents
-
- Configure the participating Copilot-backed agents and their model selections.
-
-
-
- onChange({
- ...pattern,
- agents: [
- ...pattern.agents,
- {
- id: `agent-${crypto.randomUUID()}`,
- name: `Agent ${pattern.agents.length + 1}`,
- description: 'New participant',
- instructions: 'You are a helpful specialist in this orchestration.',
- model: 'gpt-5.4',
- },
- ],
- })
- }
- type="button"
+ {/* Agents */}
+
+
+
+ Agents ({pattern.agents.length})
+
+
+ onChange({
+ ...pattern,
+ agents: [
+ ...pattern.agents,
+ {
+ id: `agent-${crypto.randomUUID()}`,
+ name: `Agent ${pattern.agents.length + 1}`,
+ description: '',
+ instructions: '',
+ model: 'gpt-5.4',
+ },
+ ],
+ })
+ }
+ type="button"
+ >
+
+ Add
+
+
+
+
+ {pattern.agents.map((agent, index) => (
+
- Add Agent
-
-
-
-
- {pattern.agents.map((agent, index) => (
-
-
-
Agent {index + 1}
- {pattern.agents.length > 1 ? (
-
- onChange({
- ...pattern,
- agents: pattern.agents.filter((current) => current.id !== agent.id),
- })
- }
- type="button"
- >
- Remove
-
- ) : null}
+
+
+ Agent {index + 1}
+
+ {pattern.agents.length > 1 && (
+
+ onChange({
+ ...pattern,
+ agents: pattern.agents.filter((a) => a.id !== agent.id),
+ })
+ }
+ type="button"
+ >
+ Remove
+
+ )}
+
+
+
updateAgent(agent.id, { name: v })}
+ value={agent.name}
+ />
+ updateAgent(agent.id, { model: v })}
+ placeholder="e.g. gpt-5.4"
+ value={agent.model}
+ />
+
+ updateAgent(agent.id, { description: v })}
+ value={agent.description}
+ />
-
-
-
- Name
-
- onChange({
- ...pattern,
- agents: pattern.agents.map((current) =>
- current.id === agent.id ? { ...current, name: event.target.value } : current,
- ),
- })
- }
- value={agent.name}
- />
-
-
- Model
-
- onChange({
- ...pattern,
- agents: pattern.agents.map((current) =>
- current.id === agent.id ? { ...current, model: event.target.value } : current,
- ),
- })
- }
- value={agent.model}
- />
-
-
- Description
-
- onChange({
- ...pattern,
- agents: pattern.agents.map((current) =>
- current.id === agent.id ? { ...current, description: event.target.value } : current,
- ),
- })
- }
- value={agent.description}
- />
-
-
- Instructions
-
- onChange({
- ...pattern,
- agents: pattern.agents.map((current) =>
- current.id === agent.id ? { ...current, instructions: event.target.value } : current,
- ),
- })
- }
- value={agent.instructions}
- />
-
+
+ updateAgent(agent.id, { instructions: v })}
+ placeholder="System prompt for this agent..."
+ value={agent.instructions}
+ />
- ))}
-
-
-
-
-
-
- Validation
-
- {issues.length === 0 ? (
-
- This pattern is ready to launch.
-
- ) : (
- issues.map((issue, index) => (
-
-
{issue.level.toUpperCase()}
-
{issue.message}
- ))
- )}
-
- {isBuiltin ? (
-
- Built-in patterns can be edited and saved, but they cannot be deleted from the global library.
-
- ) : null}
-
-
+ ))}
+
+
+
);
diff --git a/src/renderer/components/SettingsPanel.tsx b/src/renderer/components/SettingsPanel.tsx
new file mode 100644
index 0000000..bbcc572
--- /dev/null
+++ b/src/renderer/components/SettingsPanel.tsx
@@ -0,0 +1,119 @@
+import { useState } from 'react';
+import { ChevronRight, Plus, X } from 'lucide-react';
+
+import type { PatternDefinition } from '@shared/domain/pattern';
+import { PatternEditor } from '@renderer/components/PatternEditor';
+
+interface SettingsPanelProps {
+ patterns: PatternDefinition[];
+ onClose: () => void;
+ onSavePattern: (pattern: PatternDefinition) => Promise
;
+ onDeletePattern: (patternId: string) => Promise;
+ onNewPattern: () => PatternDefinition;
+}
+
+function modeBadgeClasses(pattern: PatternDefinition) {
+ if (pattern.availability === 'unavailable') return 'bg-amber-500/10 text-amber-400';
+ return 'bg-zinc-800 text-zinc-400';
+}
+
+export function SettingsPanel({
+ patterns,
+ onClose,
+ onSavePattern,
+ onDeletePattern,
+ onNewPattern,
+}: SettingsPanelProps) {
+ const [editingPattern, setEditingPattern] = useState(null);
+
+ if (editingPattern) {
+ const isBuiltin = editingPattern.id.startsWith('pattern-');
+ return (
+
+
setEditingPattern(null)}
+ onChange={setEditingPattern}
+ onDelete={
+ isBuiltin
+ ? undefined
+ : async () => {
+ await onDeletePattern(editingPattern.id);
+ setEditingPattern(null);
+ }
+ }
+ onSave={async () => {
+ await onSavePattern(editingPattern);
+ setEditingPattern(null);
+ }}
+ pattern={editingPattern}
+ />
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
Settings
+
+
+
+
+
+ {/* Body */}
+
+
+
+
+
Orchestration Patterns
+
+ Define reusable agent configurations for your sessions
+
+
+
setEditingPattern(onNewPattern())}
+ type="button"
+ >
+
+ New Pattern
+
+
+
+
+ {patterns.map((pattern) => (
+
setEditingPattern(structuredClone(pattern))}
+ type="button"
+ >
+
+
+ {pattern.name}
+
+ {pattern.mode}
+
+
+
{pattern.description}
+
+
+
+ {pattern.agents.length} agent{pattern.agents.length === 1 ? '' : 's'}
+
+
+
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/src/renderer/components/Sidebar.tsx b/src/renderer/components/Sidebar.tsx
index b4829bf..9f5c7c0 100644
--- a/src/renderer/components/Sidebar.tsx
+++ b/src/renderer/components/Sidebar.tsx
@@ -1,4 +1,13 @@
-import type { PatternDefinition } from '@shared/domain/pattern';
+import { useState } from 'react';
+import {
+ ChevronDown,
+ ChevronRight,
+ FolderOpen,
+ MessageSquare,
+ Plus,
+ Settings,
+} from 'lucide-react';
+
import type { ProjectRecord } from '@shared/domain/project';
import type { SessionRecord } from '@shared/domain/session';
import type { WorkspaceState } from '@shared/domain/workspace';
@@ -6,183 +15,164 @@ import type { WorkspaceState } from '@shared/domain/workspace';
interface SidebarProps {
workspace: WorkspaceState;
onAddProject: () => void;
- onCreateSession: () => void;
- onNewPattern: () => void;
+ onNewSession: () => void;
onProjectSelect: (projectId?: string) => void;
- onPatternSelect: (patternId?: string) => void;
- onSessionSelect: (sessionId?: string) => void;
+ onSessionSelect: (sessionId: string) => void;
+ onOpenSettings: () => void;
}
-function itemClasses(active: boolean) {
- return active
- ? 'w-full rounded-lg border border-sky-500/60 bg-sky-500/10 px-3 py-2 text-left text-sm text-sky-200'
- : 'w-full rounded-lg border border-transparent px-3 py-2 text-left text-sm text-slate-300 transition hover:border-slate-700 hover:bg-slate-800/70';
+function statusDot(status: SessionRecord['status']) {
+ if (status === 'running') return 'bg-blue-400';
+ if (status === 'error') return 'bg-red-400';
+ return 'bg-zinc-600';
}
-function modeBadge(pattern: PatternDefinition) {
- if (pattern.availability === 'unavailable') {
- return 'bg-amber-500/15 text-amber-200';
- }
+function ProjectGroup({
+ project,
+ sessions,
+ selectedSessionId,
+ onSessionSelect,
+}: {
+ project: ProjectRecord;
+ sessions: SessionRecord[];
+ selectedSessionId?: string;
+ onSessionSelect: (sessionId: string) => void;
+}) {
+ const [expanded, setExpanded] = useState(true);
- return 'bg-emerald-500/10 text-emerald-200';
-}
+ return (
+
+
setExpanded(!expanded)}
+ type="button"
+ >
+ {expanded ? (
+
+ ) : (
+
+ )}
+
+ {project.name}
+ {sessions.length}
+
-function sessionCountLabel(project: ProjectRecord, sessions: SessionRecord[]) {
- const count = sessions.filter((session) => session.projectId === project.id).length;
- return `${count} session${count === 1 ? '' : 's'}`;
+ {expanded && (
+
+ {sessions.length === 0 ? (
+
No sessions yet
+ ) : (
+ sessions.map((session) => {
+ const isActive = selectedSessionId === session.id;
+ return (
+
onSessionSelect(session.id)}
+ type="button"
+ >
+
+ {session.title}
+
+
+ );
+ })
+ )}
+
+ )}
+
+ );
}
export function Sidebar({
workspace,
onAddProject,
- onCreateSession,
- onNewPattern,
+ onNewSession,
onProjectSelect,
- onPatternSelect,
onSessionSelect,
+ onOpenSettings,
}: SidebarProps) {
return (
-
-
-
-
-
kopaya
-
Agent Orchestrator
-
- React + Electron frontend with a bundled .NET sidecar.
-
+
+ {/* Header */}
+
-
-
-
+ {/* Project + Session Tree */}
+
+ {workspace.projects.length === 0 ? (
+
+
-
Patterns
-
Global orchestration library
+
No projects yet
+
+ Add a project folder to start orchestrating
+
- New Pattern
+ Add Project
-
- {workspace.patterns.map((pattern) => (
-
onPatternSelect(pattern.id)}
- type="button"
- >
-
-
-
{pattern.name}
-
{pattern.description}
-
-
- {pattern.mode}
-
-
-
+ ) : (
+
+ {workspace.projects.map((project) => (
+
s.projectId === project.id)}
+ />
))}
-
-
-
-
-
-
Projects
-
Workspace folders and their sessions
-
-
- New Session
-
-
-
-
- {workspace.projects.map((project) => {
- const sessions = workspace.sessions.filter((session) => session.projectId === project.id);
- return (
-
-
onProjectSelect(project.id)}
- type="button"
- >
- {project.name}
- {project.path}
-
- {sessionCountLabel(project, sessions)}
-
-
-
- {sessions.length > 0 ? (
-
- {sessions.map((session) => (
-
onSessionSelect(session.id)}
- type="button"
- >
-
-
-
{session.title}
-
- {session.messages.length} message{session.messages.length === 1 ? '' : 's'}
-
-
-
- {session.status}
-
-
-
- ))}
-
- ) : (
-
- No sessions yet. Select a pattern, then start one.
-
- )}
-
- );
- })}
-
- {workspace.projects.length === 0 ? (
-
- Add one or more project folders to begin orchestrating sessions.
-
- ) : null}
-
-
+ )}
+
+ {/* Footer */}
+ {workspace.projects.length > 0 && (
+
+ )}
);
}
diff --git a/src/renderer/components/WelcomePane.tsx b/src/renderer/components/WelcomePane.tsx
new file mode 100644
index 0000000..bb67e1d
--- /dev/null
+++ b/src/renderer/components/WelcomePane.tsx
@@ -0,0 +1,63 @@
+import { MessageSquare, Plus, Settings } from 'lucide-react';
+
+interface WelcomePaneProps {
+ hasProjects: boolean;
+ onNewSession: () => void;
+ onAddProject: () => void;
+ onOpenSettings: () => void;
+}
+
+export function WelcomePane({
+ hasProjects,
+ onNewSession,
+ onAddProject,
+ onOpenSettings,
+}: WelcomePaneProps) {
+ return (
+
+
+
+
+
+
+
+
Welcome to kopaya
+
+ Orchestrate AI agents across your projects. Start a session to begin a conversation
+ with one or more Copilot-backed agents.
+
+
+
+
+ {hasProjects ? (
+
+
+ New Session
+
+ ) : (
+
+
+ Add Your First Project
+
+ )}
+
+
+ Manage patterns
+
+
+
+
+ );
+}
diff --git a/src/renderer/styles.css b/src/renderer/styles.css
index b44620a..c1f5fe5 100644
--- a/src/renderer/styles.css
+++ b/src/renderer/styles.css
@@ -1,5 +1,17 @@
@import "tailwindcss";
+@theme {
+ --color-surface-0: #09090b;
+ --color-surface-1: #18181b;
+ --color-surface-2: #27272a;
+ --color-surface-3: #3f3f46;
+ --color-border: #27272a;
+ --color-border-subtle: #1e1e22;
+ --color-accent: #6366f1;
+ --color-accent-hover: #818cf8;
+ --color-accent-muted: rgba(99, 102, 241, 0.15);
+}
+
:root {
color-scheme: dark;
font-family:
@@ -10,11 +22,14 @@
BlinkMacSystemFont,
"Segoe UI",
sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
}
body {
margin: 0;
min-height: 100vh;
+ background: var(--color-surface-0);
}
#root {
@@ -27,3 +42,28 @@ select,
textarea {
font: inherit;
}
+
+/* Scrollbar styling */
+::-webkit-scrollbar {
+ width: 6px;
+}
+
+::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+::-webkit-scrollbar-thumb {
+ background: #3f3f46;
+ border-radius: 3px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: #52525b;
+}
+
+/* Auto-resize textarea helper */
+.auto-resize-textarea {
+ field-sizing: content;
+ min-height: 44px;
+ max-height: 200px;
+}