Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
337 changes: 151 additions & 186 deletions apps/desktop/src/App.tsx

Large diffs are not rendered by default.

147 changes: 65 additions & 82 deletions apps/desktop/src/components/ModelPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
getModelSearchText,
getModelStatusLabel,
getProviderCategory,
getProviderRuntimeLabel,
getProviderSearchText,
isFavoriteModel,
isProviderRuntimePending,
Expand Down Expand Up @@ -84,22 +83,17 @@ export function ModelPicker({ value, compact = false, onChange }: ModelPickerPro
if (!normalizedSearch) {
return models;
}

return models.filter((model) => getModelSearchText(activeProvider, model).includes(normalizedSearch));
}, [activeProvider, normalizedSearch]);

const favoriteModels = useMemo(
() =>
visibleModels.filter((model) =>
isFavoriteModel(activeProvider, model.id, config.favoriteModels),
),
() => visibleModels.filter((model) => isFavoriteModel(activeProvider, model.id, config.favoriteModels)),
[activeProvider, config.favoriteModels, visibleModels],
);

const nonFavoriteModels = useMemo(
() =>
visibleModels.filter(
(model) => !isFavoriteModel(activeProvider, model.id, config.favoriteModels),
),
const remainingModels = useMemo(
() => visibleModels.filter((model) => !isFavoriteModel(activeProvider, model.id, config.favoriteModels)),
[activeProvider, config.favoriteModels, visibleModels],
);

Expand Down Expand Up @@ -128,49 +122,32 @@ export function ModelPicker({ value, compact = false, onChange }: ModelPickerPro
onClick={() => setOpen((current) => !current)}
className={[
"inline-flex items-center gap-3 rounded-2xl border border-[color:var(--border)] bg-[var(--panel-muted)] text-left text-[var(--text-primary)] transition hover:border-[color:var(--border-strong)] hover:bg-[var(--panel-elevated)]",
compact ? "min-h-11 px-3 py-2.5" : "min-h-12 px-4 py-3",
compact ? "min-h-10 px-3 py-2" : "min-h-10 px-3.5 py-2",
].join(" ")}
>
<span className="inline-flex h-8 w-8 items-center justify-center rounded-xl bg-[var(--accent-soft)] text-[var(--accent)]">
<span className="inline-flex h-7 w-7 items-center justify-center rounded-xl border border-[color:var(--border)] bg-[var(--panel-base)] text-[var(--accent)]">
<Sparkles className="h-4 w-4" />
</span>
<span className="min-w-0">
<span className="block text-[10px] uppercase tracking-[0.24em] text-[var(--text-muted)]">
<span className="block text-[10px] uppercase tracking-[0.22em] text-[var(--text-muted)]">
{config.selectedProvider}
</span>
<span className="block truncate text-sm font-medium">{triggerLabel}</span>
</span>
<span className="ml-auto inline-flex items-center gap-2">
{isProviderRuntimePending(config.selectedProvider) && (
<span className="hidden rounded-full border border-[color:var(--border)] px-2 py-0.5 text-[10px] text-[var(--text-muted)] sm:inline-flex">
runtime pending
</span>
)}
<ChevronDown className="h-4 w-4 text-[var(--text-muted)]" />
<span className="block max-w-[11rem] truncate text-sm font-medium">{triggerLabel}</span>
</span>
{isProviderRuntimePending(config.selectedProvider) ? (
<span className="hidden rounded-full border border-[color:var(--border)] px-2 py-0.5 text-[10px] text-[var(--text-muted)] lg:inline-flex">
pending
</span>
) : null}
<ChevronDown className="ml-auto h-4 w-4 text-[var(--text-muted)]" />
</button>

{open && (
<div className="absolute left-0 top-[calc(100%+12px)] z-40 w-[min(46rem,calc(100vw-2rem))] overflow-hidden rounded-[24px] border border-[color:var(--border)] bg-[var(--panel-overlay)] shadow-[0_24px_80px_rgba(15,23,42,0.28)] backdrop-blur-xl">
<div className="border-b border-[color:var(--border)] px-4 py-3">
<div className="flex items-center gap-3 rounded-2xl border border-[color:var(--border)] bg-[var(--panel-muted)] px-3 py-2.5">
<Search className="h-4 w-4 text-[var(--text-muted)]" />
<input
autoFocus
value={search}
onChange={(event) => setSearch(event.target.value)}
placeholder="Search providers or models"
className="w-full bg-transparent text-sm text-[var(--text-primary)] outline-none placeholder:text-[var(--text-muted)]"
/>
</div>
</div>

<div className="grid max-h-[28rem] grid-cols-1 overflow-hidden md:grid-cols-[220px_minmax(0,1fr)]">
<div className="border-b border-[color:var(--border)] md:border-b-0 md:border-r">
<div className="px-4 py-3 text-[10px] uppercase tracking-[0.24em] text-[var(--text-muted)]">
Providers
</div>
<div className="max-h-[28rem] overflow-y-auto px-2 pb-2">
{open ? (
<div className="absolute bottom-[calc(100%+12px)] left-0 z-50 w-[min(44rem,calc(100vw-1.5rem))] overflow-hidden rounded-[24px] border border-[color:var(--border)] bg-[var(--panel-overlay)] shadow-[0_36px_120px_rgba(0,0,0,0.48)] backdrop-blur-xl">
<div className="grid max-h-[30rem] grid-cols-[200px_minmax(0,1fr)] overflow-hidden max-sm:grid-cols-[160px_minmax(0,1fr)]">
<div className="border-r border-[color:var(--border)] bg-transparent px-2 py-2.5">
<div className="px-2 pb-2 text-[10px] uppercase tracking-[0.24em] text-[var(--text-muted)]">Providers</div>
<div className="max-h-[30rem] space-y-1 overflow-y-auto pr-1">
{visibleProviders.map((provider) => {
const definition = PROVIDER_DEFINITIONS[provider];
const selected = provider === activeProvider;
Expand All @@ -182,78 +159,86 @@ export function ModelPicker({ value, compact = false, onChange }: ModelPickerPro
onFocus={() => setHoveredProvider(provider)}
onClick={() => setHoveredProvider(provider)}
className={[
"mb-1 flex w-full flex-col rounded-2xl border px-3 py-3 text-left transition",
"flex w-full flex-col rounded-2xl border px-3 py-3 text-left transition",
selected
? "border-[color:var(--accent)] bg-[var(--accent-soft)]"
: "border-transparent hover:border-[color:var(--border)] hover:bg-[var(--panel-muted)]",
? "border-[color:var(--border-strong)] bg-[var(--panel-selected)]"
: "border-transparent bg-transparent hover:border-[color:var(--border)] hover:bg-[var(--panel-muted)]",
].join(" ")}
>
<span className="flex items-center justify-between gap-3">
<span className="flex items-center justify-between gap-2">
<span className="font-medium text-[var(--text-primary)]">{definition.label}</span>
<span className="rounded-full border border-[color:var(--border)] px-2 py-0.5 text-[10px] text-[var(--text-muted)]">
{definition.runtimeLabel}
</span>
{config.selectedProvider === provider ? (
<Check className="h-4 w-4 text-[var(--accent)]" />
) : null}
</span>
<span className="mt-1 text-xs text-[var(--text-muted)]">{getProviderCategory(provider)}</span>
<span className="mt-2 inline-flex w-fit rounded-full border border-[color:var(--border)] px-2 py-0.5 text-[10px] text-[var(--text-muted)]">
{definition.runtimeLabel}
</span>
</button>
);
})}
</div>
</div>

<div className="min-h-0">
<div className="flex items-center justify-between gap-3 border-b border-[color:var(--border)] px-4 py-3">
<div>
<div className="text-sm font-medium text-[var(--text-primary)]">{activeProvider}</div>
<div className="text-xs text-[var(--text-muted)]">
{PROVIDER_DEFINITIONS[activeProvider].description}
<div className="min-h-0 bg-[var(--panel-card)]/95">
<div className="border-b border-[color:var(--border)] px-4 py-3">
<div className="flex items-start justify-between gap-3">
<div>
<div className="text-sm font-medium text-[var(--text-primary)]">{activeProvider}</div>
<div className="mt-1 text-xs leading-5 text-[var(--text-muted)]">
{PROVIDER_DEFINITIONS[activeProvider].description}
</div>
</div>
<span className="rounded-full border border-[color:var(--border)] px-2 py-0.5 text-[10px] text-[var(--text-muted)]">
{PROVIDER_DEFINITIONS[activeProvider].runtimeLabel}
</span>
</div>

<div className="mt-3 flex items-center gap-3 rounded-2xl border border-[color:var(--border)] bg-[var(--panel-muted)] px-3 py-2.5">
<Search className="h-4 w-4 text-[var(--text-muted)]" />
<input
autoFocus
value={search}
onChange={(event) => setSearch(event.target.value)}
placeholder="Search models"
className="w-full bg-transparent text-sm text-[var(--text-primary)] outline-none placeholder:text-[var(--text-muted)]"
/>
</div>
<span className="rounded-full border border-[color:var(--border)] px-2.5 py-1 text-[10px] uppercase tracking-[0.18em] text-[var(--text-muted)]">
{getProviderRuntimeLabel(activeProvider)}
</span>
</div>

<div className="max-h-[28rem] overflow-y-auto p-3">
{favoriteModels.length > 0 && (
<div className="mb-4">
<div className="mb-2 text-[10px] uppercase tracking-[0.24em] text-[var(--text-muted)]">
Favorites
</div>
<div className="max-h-[30rem] space-y-4 overflow-y-auto p-3">
{favoriteModels.length > 0 ? (
<div>
<div className="mb-2 text-[10px] uppercase tracking-[0.24em] text-[var(--text-muted)]">Favorites</div>
<div className="space-y-2">
{favoriteModels.map((model) => (
<ModelRow
key={model.id}
provider={activeProvider}
model={model}
selected={
config.selectedProvider === activeProvider &&
config.selectedModel === model.id
}
selected={config.selectedProvider === activeProvider && config.selectedModel === model.id}
favorited
onSelect={handleSelectModel}
onToggleFavorite={handleToggleFavorite}
/>
))}
</div>
</div>
)}
) : null}

<div>
<div className="mb-2 text-[10px] uppercase tracking-[0.24em] text-[var(--text-muted)]">
{favoriteModels.length > 0 ? "All models" : "Models"}
</div>
<div className="space-y-2">
{nonFavoriteModels.length > 0 ? (
nonFavoriteModels.map((model) => (
{remainingModels.length > 0 ? (
remainingModels.map((model) => (
<ModelRow
key={model.id}
provider={activeProvider}
model={model}
selected={
config.selectedProvider === activeProvider &&
config.selectedModel === model.id
}
selected={config.selectedProvider === activeProvider && config.selectedModel === model.id}
favorited={isFavoriteModel(activeProvider, model.id, config.favoriteModels)}
onSelect={handleSelectModel}
onToggleFavorite={handleToggleFavorite}
Expand All @@ -270,7 +255,7 @@ export function ModelPicker({ value, compact = false, onChange }: ModelPickerPro
</div>
</div>
</div>
)}
) : null}
</div>
);
}
Expand All @@ -295,7 +280,7 @@ function ModelRow({
className={[
"group flex items-start gap-3 rounded-2xl border px-3 py-3 transition",
selected
? "border-[color:var(--accent)] bg-[var(--accent-soft)]"
? "border-[color:var(--border-strong)] bg-[var(--panel-selected)]"
: "border-[color:var(--border)] bg-[var(--panel-muted)] hover:border-[color:var(--border-strong)] hover:bg-[var(--panel-elevated)]",
].join(" ")}
>
Expand All @@ -304,13 +289,11 @@ function ModelRow({
onClick={() => onSelect(provider, model)}
className="flex min-w-0 flex-1 items-start gap-3 text-left"
>
<span className="mt-0.5 inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-xl border border-[color:var(--border)] bg-[var(--panel-base)] text-[var(--text-primary)]">
<span className="mt-0.5 inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-xl border border-[color:var(--border)] bg-[var(--panel-base)] text-sm text-[var(--text-primary)]">
{selected ? <Check className="h-4 w-4 text-[var(--accent)]" /> : provider.slice(0, 1)}
</span>
<span className="min-w-0 flex-1">
<span className="block truncate text-sm font-medium text-[var(--text-primary)]">
{model.label}
</span>
<span className="block truncate text-sm font-medium text-[var(--text-primary)]">{model.label}</span>
<span className="mt-1 block text-xs text-[var(--text-muted)]">{model.id}</span>
<span className="mt-2 inline-flex rounded-full border border-[color:var(--border)] px-2 py-0.5 text-[10px] text-[var(--text-muted)]">
{getModelStatusLabel(provider, model.id)}
Expand Down
12 changes: 6 additions & 6 deletions apps/desktop/src/components/TaskThreadView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,13 @@ export function TaskThreadView({

return (
<div className="flex h-full min-h-0 flex-col overflow-hidden">
<div className="border-b border-[color:var(--border)] px-5 py-5 sm:px-6">
<div className="border-b border-[color:var(--border)] px-5 py-4 sm:px-6">
<div className="flex flex-wrap items-start justify-between gap-4">
<div className="min-w-0">
<div className="text-[10px] uppercase tracking-[0.32em] text-[var(--text-muted)]">
<div className="text-[10px] uppercase tracking-[0.28em] text-[var(--text-muted)]">
Task thread
</div>
<h2 className="mt-2 truncate text-xl font-semibold tracking-tight text-[var(--text-primary)]">
<h2 className="mt-2 truncate text-[1.15rem] font-semibold tracking-[-0.03em] text-[var(--text-primary)]">
{task.title || "Untitled thread"}
</h2>
<div className="mt-3 flex flex-wrap items-center gap-2 text-xs text-[var(--text-muted)]">
Expand Down Expand Up @@ -115,7 +115,7 @@ export function TaskThreadView({
)}

<div className="min-h-0 flex-1 overflow-y-auto px-5 py-5 sm:px-6">
<div className="space-y-4">
<div className="mx-auto w-full max-w-[1080px] space-y-4">
<StreamCard eyebrow="Prompt" title="What should Codra build?" icon={<Wand2 className="h-4 w-4" />}>
<p className="whitespace-pre-wrap text-[15px] leading-7 text-[var(--text-primary)]">
{task.userPrompt}
Expand Down Expand Up @@ -434,15 +434,15 @@ function StreamCard({
return (
<section
className={[
"rounded-[24px] border p-4 sm:p-5",
"rounded-[22px] border p-4 sm:p-5",
tone === "danger"
? "border-[color:var(--danger-border)] bg-[var(--danger-card)]"
: "border-[color:var(--border)] bg-[var(--panel-card)]",
].join(" ")}
>
<div className="mb-3 flex items-center justify-between gap-3">
<div>
<div className="text-[10px] uppercase tracking-[0.32em] text-[var(--text-muted)]">{eyebrow}</div>
<div className="text-[10px] uppercase tracking-[0.24em] text-[var(--text-muted)]">{eyebrow}</div>
<div className="mt-1 flex items-center gap-2 text-base font-medium tracking-[-0.02em] text-[var(--text-primary)]">
{icon}
{title}
Expand Down
Loading
Loading