feat: scaffold electron orchestrator foundation

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Copilot CLI
2026-03-21 09:27:28 +01:00
co-authored by Copilot
parent 1ed3d3f652
commit 9e509593d6
46 changed files with 3870 additions and 13 deletions
+226
View File
@@ -0,0 +1,226 @@
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 { Sidebar } from '@renderer/components/Sidebar';
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'),
name: 'New Pattern',
description: 'Reusable orchestration pattern.',
mode: 'single',
availability: 'available',
maxIterations: 1,
agents: [
{
id: createId('agent'),
name: 'Primary Agent',
description: 'General-purpose project assistant.',
instructions: 'You are a helpful coding assistant working inside the selected project.',
model: 'gpt-5.4',
reasoningEffort: 'high',
},
],
createdAt: timestamp,
updatedAt: timestamp,
};
}
function EmptyDetail() {
const patterns = createBuiltinPatterns(nowIso());
return (
<div className="flex h-screen items-center justify-center px-10">
<div className="max-w-3xl rounded-3xl border border-slate-800 bg-slate-900/70 p-10">
<div className="text-xs uppercase tracking-[0.22em] text-slate-400">Workspace Overview</div>
<h2 className="mt-3 text-3xl font-semibold text-white">Chat-first orchestration across projects</h2>
<p className="mt-4 text-sm leading-7 text-slate-300">
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.
</p>
<div className="mt-8 grid gap-4 md:grid-cols-2">
{patterns.map((pattern) => (
<div
className="rounded-2xl border border-slate-800 bg-slate-950/70 p-4"
key={pattern.id}
>
<div className="flex items-center justify-between gap-4">
<h3 className="text-sm font-semibold text-slate-100">{pattern.name}</h3>
<span
className={`rounded-full px-2 py-0.5 text-[11px] uppercase tracking-wide ${
pattern.availability === 'unavailable'
? 'bg-amber-500/15 text-amber-200'
: 'bg-emerald-500/10 text-emerald-200'
}`}
>
{pattern.mode}
</span>
</div>
<p className="mt-2 text-sm text-slate-400">{pattern.description}</p>
</div>
))}
</div>
</div>
</div>
);
}
export default function App() {
const api = getElectronApi();
const [workspace, setWorkspace] = useState<WorkspaceState>();
const [draftPattern, setDraftPattern] = useState<PatternDefinition | null>(null);
const [error, setError] = useState<string>();
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));
}
});
const offWorkspace = api.onWorkspaceUpdated((nextWorkspace) => {
setWorkspace(nextWorkspace);
setError(undefined);
});
return () => {
disposed = true;
offWorkspace();
};
}, [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]);
const selectedSession = useMemo(
() => workspace?.sessions.find((session) => session.id === workspace.selectedSessionId),
[workspace?.selectedSessionId, workspace?.sessions],
);
const selectedPattern = useMemo(
() =>
draftPattern ??
workspace?.patterns.find((pattern) => pattern.id === workspace.selectedPatternId),
[draftPattern, workspace?.patterns, workspace?.selectedPatternId],
);
const selectedProject = useMemo(
() => workspace?.projects.find((project) => project.id === workspace.selectedProjectId),
[workspace?.projects, workspace?.selectedProjectId],
);
if (!workspace) {
return (
<div className="flex min-h-screen items-center justify-center bg-slate-950 text-slate-100">
Loading workspace
</div>
);
}
const patternForSession = selectedSession
? workspace.patterns.find((pattern) => pattern.id === selectedSession.patternId)
: undefined;
const projectForSession = selectedSession
? workspace.projects.find((project) => project.id === selectedSession.projectId)
: undefined;
return (
<AppShell
content={
error ? (
<div className="flex h-screen items-center justify-center px-10">
<div className="max-w-lg rounded-3xl border border-rose-500/40 bg-rose-500/10 p-8 text-rose-100">
<div className="text-xs uppercase tracking-[0.2em] text-rose-200">Error</div>
<h2 className="mt-3 text-2xl font-semibold">Something went wrong</h2>
<p className="mt-3 text-sm leading-7">{error}</p>
</div>
</div>
) : selectedSession && patternForSession && projectForSession ? (
<ChatPane
onSend={(content) => api.sendSessionMessage({ sessionId: selectedSession.id, content })}
pattern={patternForSession}
project={projectForSession}
session={selectedSession}
/>
) : selectedPattern ? (
<PatternEditor
isBuiltin={selectedPattern.id.startsWith('pattern-')}
onChange={setDraftPattern}
onDelete={
selectedPattern.id.startsWith('pattern-')
? undefined
: async () => {
await api.deletePattern(selectedPattern.id);
}
}
onSave={async () => {
await api.savePattern({ pattern: draftPattern ?? selectedPattern });
}}
pattern={draftPattern ?? selectedPattern}
/>
) : (
<EmptyDetail />
)
}
sidebar={
<Sidebar
onAddProject={() => {
void api.addProject();
}}
onCreateSession={() => {
if (!workspace.selectedProjectId || !workspace.selectedPatternId) {
return;
}
void api.createSession({
projectId: workspace.selectedProjectId,
patternId: workspace.selectedPatternId,
});
}}
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}
/>
}
/>
);
}
+15
View File
@@ -0,0 +1,15 @@
import type { ReactNode } from 'react';
interface AppShellProps {
sidebar: ReactNode;
content: ReactNode;
}
export function AppShell({ sidebar, content }: AppShellProps) {
return (
<div className="flex min-h-screen bg-slate-950 text-slate-100">
<aside className="w-[360px] shrink-0 border-r border-slate-800 bg-slate-900/90">{sidebar}</aside>
<main className="min-w-0 flex-1">{content}</main>
</div>
);
}
+140
View File
@@ -0,0 +1,140 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import type { PatternDefinition } from '@shared/domain/pattern';
import type { ProjectRecord } from '@shared/domain/project';
import type { SessionRecord } from '@shared/domain/session';
interface ChatPaneProps {
project: ProjectRecord;
pattern: PatternDefinition;
session: SessionRecord;
onSend: (content: string) => Promise<void>;
}
export function ChatPane({ project, pattern, session, onSend }: ChatPaneProps) {
const [input, setInput] = useState('');
const transcriptRef = useRef<HTMLDivElement>(null);
useEffect(() => {
transcriptRef.current?.scrollTo({
top: transcriptRef.current.scrollHeight,
behavior: 'smooth',
});
}, [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],
);
return (
<div className="flex h-screen flex-col">
<header className="border-b border-slate-800 px-8 py-6">
<div className="flex items-start justify-between gap-6">
<div>
<div className="text-xs uppercase tracking-[0.22em] text-slate-400">{project.name}</div>
<h2 className="mt-2 text-2xl font-semibold text-white">{session.title}</h2>
<p className="mt-2 text-sm text-slate-400">
Pattern: <span className="font-medium text-slate-200">{pattern.name}</span> Mode:{' '}
<span className="font-medium text-slate-200">{pattern.mode}</span>
</p>
<p className="mt-1 text-sm text-slate-500">{sessionStats}</p>
</div>
<div
className={`rounded-full px-3 py-1.5 text-xs font-medium uppercase tracking-wide ${
session.status === 'error'
? 'bg-rose-500/15 text-rose-200'
: session.status === 'running'
? 'bg-sky-500/15 text-sky-200'
: 'bg-slate-800 text-slate-200'
}`}
>
{session.status}
</div>
</div>
</header>
<div
className="flex-1 overflow-y-auto px-8 py-6"
ref={transcriptRef}
>
{session.messages.length === 0 ? (
<div className="rounded-3xl border border-dashed border-slate-800 bg-slate-900/60 px-6 py-8 text-sm text-slate-400">
Start the conversation to launch this orchestration against <span className="font-medium text-slate-200">{project.path}</span>.
</div>
) : (
<div className="mx-auto flex max-w-4xl flex-col gap-4">
{session.messages.map((message) => {
const isUser = message.role === 'user';
return (
<div
className={`flex ${isUser ? 'justify-end' : 'justify-start'}`}
key={message.id}
>
<div
className={`max-w-3xl rounded-3xl px-5 py-4 shadow-sm ${
isUser
? 'bg-sky-500 text-slate-950'
: 'border border-slate-800 bg-slate-900/85 text-slate-100'
}`}
>
<div className={`text-xs font-semibold uppercase tracking-wide ${isUser ? 'text-slate-800' : 'text-slate-400'}`}>
{message.authorName}
</div>
<div className="mt-2 whitespace-pre-wrap text-sm leading-6">{message.content}</div>
{message.pending ? (
<div className="mt-3 text-xs text-slate-400">Streaming</div>
) : null}
</div>
</div>
);
})}
</div>
)}
</div>
<div className="border-t border-slate-800 px-8 py-5">
{session.lastError ? (
<div className="mb-4 rounded-xl border border-rose-500/40 bg-rose-500/10 px-4 py-3 text-sm text-rose-200">
{session.lastError}
</div>
) : null}
<form
className="mx-auto flex max-w-4xl flex-col gap-3"
onSubmit={async (event) => {
event.preventDefault();
if (!input.trim()) {
return;
}
const nextInput = input;
setInput('');
await onSend(nextInput);
}}
>
<textarea
className="min-h-28 w-full rounded-3xl border border-slate-700 bg-slate-900 px-5 py-4 text-sm text-slate-100 shadow-inner outline-none transition focus:border-sky-500"
disabled={isBusy}
onChange={(event) => setInput(event.target.value)}
placeholder="Ask the selected orchestration to reason about the current project..."
value={input}
/>
<div className="flex items-center justify-between gap-4">
<p className="text-xs text-slate-500">
The .NET sidecar replays the saved transcript so sessions can resume after app restart.
</p>
<button
className="rounded-full bg-sky-500 px-5 py-2.5 text-sm font-semibold text-slate-950 hover:bg-sky-400 disabled:cursor-not-allowed disabled:opacity-60"
disabled={isBusy || !input.trim()}
type="submit"
>
{isBusy ? 'Running…' : 'Send'}
</button>
</div>
</form>
</div>
</div>
);
}
+250
View File
@@ -0,0 +1,250 @@
import { validatePatternDefinition, type OrchestrationMode, type PatternDefinition } from '@shared/domain/pattern';
interface PatternEditorProps {
pattern: PatternDefinition;
isBuiltin: boolean;
onChange: (pattern: PatternDefinition) => void;
onDelete?: () => void;
onSave: () => void;
}
const modes: OrchestrationMode[] = ['single', 'sequential', 'concurrent', 'handoff', 'group-chat', 'magentic'];
export function PatternEditor({ pattern, isBuiltin, onChange, onDelete, onSave }: PatternEditorProps) {
const issues = validatePatternDefinition(pattern);
return (
<div className="flex h-full flex-col">
<div className="border-b border-slate-800 px-8 py-6">
<div className="flex items-start justify-between gap-4">
<div>
<div className="text-xs uppercase tracking-[0.22em] text-slate-400">Pattern</div>
<h2 className="mt-2 text-2xl font-semibold text-white">{pattern.name || 'Untitled pattern'}</h2>
<p className="mt-2 max-w-3xl text-sm text-slate-400">
Define a reusable orchestration blueprint that can be launched against any project in the
workspace.
</p>
</div>
<div className="flex gap-3">
{!isBuiltin && onDelete ? (
<button
className="rounded-lg border border-rose-500/40 px-4 py-2 text-sm font-medium text-rose-200 hover:bg-rose-500/10"
onClick={onDelete}
type="button"
>
Delete
</button>
) : null}
<button
className="rounded-lg bg-sky-500 px-4 py-2 text-sm font-medium text-slate-950 hover:bg-sky-400"
onClick={onSave}
type="button"
>
Save Pattern
</button>
</div>
</div>
</div>
<div className="grid flex-1 grid-cols-[minmax(0,1fr)_320px] gap-0 overflow-hidden">
<div className="overflow-y-auto px-8 py-6">
<div className="space-y-8">
<section className="grid gap-4 md:grid-cols-2">
<label className="space-y-2">
<span className="text-sm font-medium text-slate-200">Name</span>
<input
className="w-full rounded-lg border border-slate-700 bg-slate-900 px-3 py-2 text-sm text-slate-100"
onChange={(event) => onChange({ ...pattern, name: event.target.value })}
value={pattern.name}
/>
</label>
<label className="space-y-2">
<span className="text-sm font-medium text-slate-200">Mode</span>
<select
className="w-full rounded-lg border border-slate-700 bg-slate-900 px-3 py-2 text-sm text-slate-100"
onChange={(event) =>
onChange({
...pattern,
mode: event.target.value as OrchestrationMode,
})
}
value={pattern.mode}
>
{modes.map((mode) => (
<option
key={mode}
value={mode}
>
{mode}
</option>
))}
</select>
</label>
<label className="space-y-2 md:col-span-2">
<span className="text-sm font-medium text-slate-200">Description</span>
<textarea
className="min-h-24 w-full rounded-lg border border-slate-700 bg-slate-900 px-3 py-2 text-sm text-slate-100"
onChange={(event) => onChange({ ...pattern, description: event.target.value })}
value={pattern.description}
/>
</label>
</section>
<section>
<div className="mb-4 flex items-center justify-between gap-4">
<div>
<h3 className="text-lg font-semibold text-white">Agents</h3>
<p className="mt-1 text-sm text-slate-400">
Configure the participating Copilot-backed agents and their model selections.
</p>
</div>
<button
className="rounded-lg border border-slate-700 px-3 py-2 text-sm font-medium text-slate-100 hover:bg-slate-800"
onClick={() =>
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"
>
Add Agent
</button>
</div>
<div className="space-y-4">
{pattern.agents.map((agent, index) => (
<div
className="rounded-2xl border border-slate-800 bg-slate-900/70 p-4"
key={agent.id}
>
<div className="mb-4 flex items-center justify-between gap-4">
<div className="text-sm font-medium text-slate-200">Agent {index + 1}</div>
{pattern.agents.length > 1 ? (
<button
className="text-sm text-rose-200 hover:text-rose-100"
onClick={() =>
onChange({
...pattern,
agents: pattern.agents.filter((current) => current.id !== agent.id),
})
}
type="button"
>
Remove
</button>
) : null}
</div>
<div className="grid gap-4 md:grid-cols-2">
<label className="space-y-2">
<span className="text-sm font-medium text-slate-300">Name</span>
<input
className="w-full rounded-lg border border-slate-700 bg-slate-950 px-3 py-2 text-sm text-slate-100"
onChange={(event) =>
onChange({
...pattern,
agents: pattern.agents.map((current) =>
current.id === agent.id ? { ...current, name: event.target.value } : current,
),
})
}
value={agent.name}
/>
</label>
<label className="space-y-2">
<span className="text-sm font-medium text-slate-300">Model</span>
<input
className="w-full rounded-lg border border-slate-700 bg-slate-950 px-3 py-2 text-sm text-slate-100"
onChange={(event) =>
onChange({
...pattern,
agents: pattern.agents.map((current) =>
current.id === agent.id ? { ...current, model: event.target.value } : current,
),
})
}
value={agent.model}
/>
</label>
<label className="space-y-2 md:col-span-2">
<span className="text-sm font-medium text-slate-300">Description</span>
<input
className="w-full rounded-lg border border-slate-700 bg-slate-950 px-3 py-2 text-sm text-slate-100"
onChange={(event) =>
onChange({
...pattern,
agents: pattern.agents.map((current) =>
current.id === agent.id ? { ...current, description: event.target.value } : current,
),
})
}
value={agent.description}
/>
</label>
<label className="space-y-2 md:col-span-2">
<span className="text-sm font-medium text-slate-300">Instructions</span>
<textarea
className="min-h-28 w-full rounded-lg border border-slate-700 bg-slate-950 px-3 py-2 text-sm text-slate-100"
onChange={(event) =>
onChange({
...pattern,
agents: pattern.agents.map((current) =>
current.id === agent.id ? { ...current, instructions: event.target.value } : current,
),
})
}
value={agent.instructions}
/>
</label>
</div>
</div>
))}
</div>
</section>
</div>
</div>
<aside className="border-l border-slate-800 bg-slate-900/70 px-6 py-6">
<h3 className="text-sm font-semibold uppercase tracking-[0.2em] text-slate-400">Validation</h3>
<div className="mt-4 space-y-3">
{issues.length === 0 ? (
<div className="rounded-xl border border-emerald-500/30 bg-emerald-500/10 px-4 py-3 text-sm text-emerald-200">
This pattern is ready to launch.
</div>
) : (
issues.map((issue, index) => (
<div
className={`rounded-xl px-4 py-3 text-sm ${
issue.level === 'error'
? 'border border-rose-500/40 bg-rose-500/10 text-rose-200'
: 'border border-amber-500/40 bg-amber-500/10 text-amber-200'
}`}
key={`${issue.field ?? 'issue'}-${index}`}
>
<div className="font-medium">{issue.level.toUpperCase()}</div>
<div className="mt-1">{issue.message}</div>
</div>
))
)}
{isBuiltin ? (
<div className="rounded-xl border border-slate-700 bg-slate-900 px-4 py-3 text-sm text-slate-400">
Built-in patterns can be edited and saved, but they cannot be deleted from the global library.
</div>
) : null}
</div>
</aside>
</div>
</div>
);
}
+188
View File
@@ -0,0 +1,188 @@
import type { PatternDefinition } from '@shared/domain/pattern';
import type { ProjectRecord } from '@shared/domain/project';
import type { SessionRecord } from '@shared/domain/session';
import type { WorkspaceState } from '@shared/domain/workspace';
interface SidebarProps {
workspace: WorkspaceState;
onAddProject: () => void;
onCreateSession: () => void;
onNewPattern: () => void;
onProjectSelect: (projectId?: string) => void;
onPatternSelect: (patternId?: string) => void;
onSessionSelect: (sessionId?: string) => 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 modeBadge(pattern: PatternDefinition) {
if (pattern.availability === 'unavailable') {
return 'bg-amber-500/15 text-amber-200';
}
return 'bg-emerald-500/10 text-emerald-200';
}
function sessionCountLabel(project: ProjectRecord, sessions: SessionRecord[]) {
const count = sessions.filter((session) => session.projectId === project.id).length;
return `${count} session${count === 1 ? '' : 's'}`;
}
export function Sidebar({
workspace,
onAddProject,
onCreateSession,
onNewPattern,
onProjectSelect,
onPatternSelect,
onSessionSelect,
}: SidebarProps) {
return (
<div className="flex h-screen flex-col">
<div className="border-b border-slate-800 px-5 py-4">
<div className="flex items-start justify-between gap-4">
<div>
<div className="text-xs uppercase tracking-[0.22em] text-slate-400">kopaya</div>
<h1 className="mt-1 text-xl font-semibold text-white">Agent Orchestrator</h1>
<p className="mt-1 text-sm text-slate-400">
React + Electron frontend with a bundled .NET sidecar.
</p>
</div>
<button
className="rounded-lg border border-slate-700 px-3 py-2 text-sm font-medium text-slate-200 hover:bg-slate-800"
onClick={onAddProject}
type="button"
>
Add Project
</button>
</div>
</div>
<div className="flex-1 space-y-6 overflow-y-auto px-4 py-4">
<section>
<div className="mb-3 flex items-center justify-between px-1">
<div>
<h2 className="text-sm font-semibold text-slate-200">Patterns</h2>
<p className="text-xs text-slate-500">Global orchestration library</p>
</div>
<button
className="rounded-md border border-slate-700 px-2.5 py-1.5 text-xs font-medium text-slate-200 hover:bg-slate-800"
onClick={onNewPattern}
type="button"
>
New Pattern
</button>
</div>
<div className="space-y-2">
{workspace.patterns.map((pattern) => (
<button
className={itemClasses(workspace.selectedPatternId === pattern.id && !workspace.selectedSessionId)}
key={pattern.id}
onClick={() => onPatternSelect(pattern.id)}
type="button"
>
<div className="flex items-start justify-between gap-3">
<div>
<div className="font-medium text-slate-100">{pattern.name}</div>
<div className="mt-1 text-xs text-slate-400">{pattern.description}</div>
</div>
<span className={`rounded-full px-2 py-0.5 text-[11px] uppercase tracking-wide ${modeBadge(pattern)}`}>
{pattern.mode}
</span>
</div>
</button>
))}
</div>
</section>
<section>
<div className="mb-3 flex items-center justify-between px-1">
<div>
<h2 className="text-sm font-semibold text-slate-200">Projects</h2>
<p className="text-xs text-slate-500">Workspace folders and their sessions</p>
</div>
<button
className="rounded-md border border-slate-700 px-2.5 py-1.5 text-xs font-medium text-slate-200 hover:bg-slate-800"
disabled={!workspace.selectedProjectId || !workspace.selectedPatternId}
onClick={onCreateSession}
type="button"
>
New Session
</button>
</div>
<div className="space-y-3">
{workspace.projects.map((project) => {
const sessions = workspace.sessions.filter((session) => session.projectId === project.id);
return (
<div
className="rounded-xl border border-slate-800 bg-slate-900/60 p-3"
key={project.id}
>
<button
className={itemClasses(workspace.selectedProjectId === project.id && !workspace.selectedSessionId)}
onClick={() => onProjectSelect(project.id)}
type="button"
>
<div className="font-medium text-slate-100">{project.name}</div>
<div className="mt-1 text-xs text-slate-400">{project.path}</div>
<div className="mt-2 text-[11px] uppercase tracking-wide text-slate-500">
{sessionCountLabel(project, sessions)}
</div>
</button>
{sessions.length > 0 ? (
<div className="mt-3 space-y-2 border-t border-slate-800 pt-3">
{sessions.map((session) => (
<button
className={itemClasses(workspace.selectedSessionId === session.id)}
key={session.id}
onClick={() => onSessionSelect(session.id)}
type="button"
>
<div className="flex items-center justify-between gap-3">
<div className="min-w-0">
<div className="truncate font-medium text-slate-100">{session.title}</div>
<div className="mt-1 text-xs text-slate-400">
{session.messages.length} message{session.messages.length === 1 ? '' : 's'}
</div>
</div>
<span
className={`rounded-full px-2 py-0.5 text-[11px] uppercase tracking-wide ${
session.status === 'error'
? 'bg-rose-500/15 text-rose-200'
: session.status === 'running'
? 'bg-sky-500/15 text-sky-200'
: 'bg-slate-700 text-slate-200'
}`}
>
{session.status}
</span>
</div>
</button>
))}
</div>
) : (
<div className="mt-3 rounded-lg border border-dashed border-slate-800 px-3 py-2 text-xs text-slate-500">
No sessions yet. Select a pattern, then start one.
</div>
)}
</div>
);
})}
{workspace.projects.length === 0 ? (
<div className="rounded-xl border border-dashed border-slate-800 bg-slate-900/50 px-4 py-5 text-sm text-slate-400">
Add one or more project folders to begin orchestrating sessions.
</div>
) : null}
</div>
</section>
</div>
</div>
);
}
+9
View File
@@ -0,0 +1,9 @@
import type { ElectronApi } from '@shared/contracts/ipc';
declare global {
interface Window {
kopayaApi: ElectronApi;
}
}
export {};
+18
View File
@@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>kopaya</title>
</head>
<body class="bg-slate-950 text-slate-100">
<div id="root"></div>
<script
type="module"
src="./main.tsx"
></script>
</body>
</html>
+7
View File
@@ -0,0 +1,7 @@
export function getElectronApi() {
if (!window.kopayaApi) {
throw new Error('The Electron preload API is unavailable.');
}
return window.kopayaApi;
}
+16
View File
@@ -0,0 +1,16 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from '@renderer/App';
import '@renderer/styles.css';
const container = document.getElementById('root');
if (!container) {
throw new Error('Could not find the root element.');
}
createRoot(container).render(
<StrictMode>
<App />
</StrictMode>,
);
+29
View File
@@ -0,0 +1,29 @@
@import "tailwindcss";
:root {
color-scheme: dark;
font-family:
Inter,
ui-sans-serif,
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
sans-serif;
}
body {
margin: 0;
min-height: 100vh;
}
#root {
min-height: 100vh;
}
button,
input,
select,
textarea {
font: inherit;
}