mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-31 06:57:12 +02:00
feat: scaffold electron orchestrator foundation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user