feat(workspace,settings): model menu chip in the composer, compact settings panels

The workspace home's composer takes ONE Model chip (ModelMenu: model list
plus Effort) and moves AI Router / Permission / Local-New worktree into the
row under it; Settings drops the General-page cost card and export row,
PricingSettings and the empty states are compacted for settings panels.
This commit is contained in:
2026-10-02 17:49:05 +07:00
parent da1b17778c
commit fadecaddd0
23 changed files with 925 additions and 640 deletions
+3 -8
View File
@@ -24,7 +24,7 @@
*/
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Plus, Pencil, Trash2, Router as RouterIcon, Lock } from "lucide-react";
import { Plus, Pencil, Trash2, Lock } from "lucide-react";
import { api, type AiRouter, type AiRouterInput, type AiRouterModels } from "../lib/api";
const TIERS: (keyof AiRouterModels)[] = ["opus", "sonnet", "haiku", "smallFast", "subagent"];
@@ -181,13 +181,8 @@ export function AiRouterConfig() {
return (
<div className="space-y-4">
<div className="flex items-start justify-between gap-4">
<div>
<h2 className="flex items-center gap-2 text-sm font-semibold text-fg-primary">
<RouterIcon className="h-4 w-4 text-fg-muted" />
{t("aiRouter.title")}
</h2>
<p className="mt-1 max-w-2xl text-xs text-fg-muted">{t("aiRouter.description")}</p>
</div>
{/* The page header above already names the section. */}
<p className="max-w-2xl text-xs text-fg-muted">{t("aiRouter.description")}</p>
<button
type="button"
onClick={startAdd}
+2 -2
View File
@@ -47,14 +47,14 @@ function DefaultModel({ toolId, saved }: { toolId: string; saved: string }) {
<div className="space-y-2">
<p className="text-xs text-fg-muted">{t("toolTab.defaultModelHint")}</p>
<div className="flex flex-wrap items-start gap-2">
<div className="min-w-[16rem] flex-1">
<div className="w-72 max-w-full">
<ModelPicker value={value} onChange={setValue} tool={toolId} />
</div>
<button
type="button"
onClick={save}
disabled={value === saved}
className="btn-ghost text-xs disabled:opacity-50"
className="btn-primary text-xs disabled:opacity-50"
>
{t("toolTab.saveDefaultModel")}
</button>
+3 -8
View File
@@ -10,7 +10,7 @@
*/
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { KeyRound, Pencil, Plus, Trash2 } from "lucide-react";
import { Pencil, Plus, Trash2 } from "lucide-react";
import { api, type GitCredential } from "../lib/api";
const INPUT =
@@ -69,13 +69,8 @@ export function GitCredentialsConfig() {
return (
<div className="space-y-4">
<div className="flex items-start justify-between gap-4">
<div>
<h2 className="flex items-center gap-2 text-sm font-semibold text-fg-primary">
<KeyRound className="h-4 w-4 text-fg-muted" />
{t("gitCreds.title")}
</h2>
<p className="mt-1 max-w-2xl text-xs text-fg-muted">{t("gitCreds.description")}</p>
</div>
{/* The page header above already names the section. */}
<p className="max-w-2xl text-xs text-fg-muted">{t("gitCreds.description")}</p>
<button
type="button"
onClick={() => setForm({ url: "", username: "", token: "" })}
+50 -50
View File
@@ -392,22 +392,6 @@ export function Settings({
return (
<div className="animate-fade-in space-y-8">
{section === "general" && (
<div className="flex items-center justify-end gap-2">
<a
href={api.settings.exportData()}
download
className="inline-flex items-center gap-1.5 text-xs px-3 py-1.5 rounded-md border border-border text-fg-secondary hover:text-fg-primary hover:border-border-light transition-colors"
>
<FileDown className="w-3.5 h-3.5" />
{t("exportData")}
</a>
<button onClick={load} className="btn-ghost">
<RefreshCw className="w-4 h-4" /> {t("common:refresh")}
</button>
</div>
)}
{error && (
<div className="px-4 py-2.5 bg-status-danger/10 border border-status-danger/20 rounded-lg text-sm text-status-danger">
{error}
@@ -433,35 +417,6 @@ export function Settings({
/>
</SettingsSection>
{/* Cost summary card */}
<div className="card p-6">
<div className="flex items-center justify-between flex-wrap gap-4">
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-xl bg-status-success/10 border border-status-success/20 flex items-center justify-center">
<DollarSign className="w-6 h-6 text-status-success" />
</div>
<div>
<p className="text-sm text-fg-muted">{t("common:cost.totalEstimatedCost")}</p>
<p className="text-2xl font-semibold text-fg-primary">
<Tip
raw={
totalCost !== null
? `$${totalCost.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
: undefined
}
>
{totalCost !== null ? fmtCost(animatedTotalCost) : "$-.--"}
</Tip>
</p>
</div>
</div>
<div className="text-right text-xs text-fg-muted">
<p>{t("acrossSessions")}</p>
<p>{t("basedOnUsage")}</p>
</div>
</div>
</div>
{/* ─── LANES ─── */}
<section id="lanes" className="scroll-mt-24">
<h3 className="text-sm font-medium text-fg-secondary flex items-center gap-2 mb-1">
@@ -752,13 +707,58 @@ export function Settings({
<>
{/* ─── DATA MANAGEMENT ─── */}
<section id="data" className="scroll-mt-24">
<h3 className="text-sm font-medium text-fg-secondary flex items-center gap-2 mb-1">
<Database className="w-4 h-4 text-fg-muted" />
{t("data.title")}
</h3>
<p className="text-xs text-fg-muted mb-4">{t("data.description")}</p>
<div className="mb-4 flex flex-wrap items-start justify-between gap-2">
<div>
<h3 className="text-sm font-medium text-fg-secondary flex items-center gap-2 mb-1">
<Database className="w-4 h-4 text-fg-muted" />
{t("data.title")}
</h3>
<p className="text-xs text-fg-muted">{t("data.description")}</p>
</div>
<div className="flex items-center gap-2">
<a
href={api.settings.exportData()}
download
className="inline-flex items-center gap-1.5 text-xs px-3 py-1.5 rounded-md border border-border text-fg-secondary hover:text-fg-primary hover:border-border-light transition-colors"
>
<FileDown className="w-3.5 h-3.5" />
{t("exportData")}
</a>
<button onClick={load} className="btn-ghost">
<RefreshCw className="w-4 h-4" /> {t("common:refresh")}
</button>
</div>
</div>
<div className="space-y-4">
{/* Cost summary card */}
<div className="card p-6">
<div className="flex items-center justify-between flex-wrap gap-4">
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-xl bg-status-success/10 border border-status-success/20 flex items-center justify-center">
<DollarSign className="w-6 h-6 text-status-success" />
</div>
<div>
<p className="text-sm text-fg-muted">{t("common:cost.totalEstimatedCost")}</p>
<p className="text-2xl font-semibold text-fg-primary">
<Tip
raw={
totalCost !== null
? `$${totalCost.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
: undefined
}
>
{totalCost !== null ? fmtCost(animatedTotalCost) : "$-.--"}
</Tip>
</p>
</div>
</div>
<div className="text-right text-xs text-fg-muted">
<p>{t("acrossSessions")}</p>
<p>{t("basedOnUsage")}</p>
</div>
</div>
</div>
<div className="card p-5 space-y-4">
<div className="flex flex-wrap items-center justify-between gap-2">
<p className="text-xs text-fg-muted uppercase tracking-wider font-semibold flex-shrink-0">
+17 -12
View File
@@ -52,8 +52,6 @@ interface Section {
title: string;
description: string;
Icon: ComponentType<{ className?: string }>;
/** Full content width (config editors and tables), else `max-w-3xl`. */
wide?: boolean;
render: () => ReactNode;
}
@@ -119,7 +117,6 @@ export function SettingsHub() {
id: "agents",
group: "agents",
Icon: Bot,
wide: true,
...s("agents"),
render: () => <AiToolsSettings />,
},
@@ -129,7 +126,6 @@ export function SettingsHub() {
id: "agents/claude-code",
group: "agents" as const,
Icon: Boxes,
wide: true,
...s("claudeCode"),
render: () => <CcConfig />,
},
@@ -141,7 +137,6 @@ export function SettingsHub() {
id: "agents/codex",
group: "agents" as const,
Icon: SquareTerminal,
wide: true,
...s("codex"),
render: () => <CodexConfig />,
},
@@ -157,14 +152,12 @@ export function SettingsHub() {
Icon: x.id === "antigravity" ? Orbit : Puzzle,
title: x.label,
description: t("settingsNav.section.tool.description", { tool: x.label }),
wide: true,
render: () => <AiToolConfig key={x.id} toolId={x.id} />,
})),
{
id: "ai-router",
group: "routing",
Icon: Router,
wide: true,
...s("aiRouter"),
render: () => <AiRouterConfig />,
},
@@ -172,13 +165,25 @@ export function SettingsHub() {
id: "pricing",
group: "routing",
Icon: Coins,
wide: true,
...s("pricing"),
render: () => (
<div className="space-y-6">
<PricingSettings vendor="claude" allowReset id="pricing-claude" />
<PricingSettings vendor="openai" id="pricing-openai" />
<PricingSettings vendor="google" id="pricing-google" />
<PricingSettings
vendor="claude"
allowReset
id="pricing-claude"
heading={t("pricing.vendor.claude")}
/>
<PricingSettings
vendor="openai"
id="pricing-openai"
heading={t("pricing.vendor.openai")}
/>
<PricingSettings
vendor="google"
id="pricing-google"
heading={t("pricing.vendor.google")}
/>
</div>
),
},
@@ -304,7 +309,7 @@ export function SettingsHub() {
})}
</nav>
<div className={`min-w-0 flex-1 ${active.wide ? "" : "max-w-3xl"}`}>
<div className="min-w-0 flex-1">
<select
aria-label={t("settingsNav.label")}
value={active.id}
@@ -642,10 +642,10 @@ exports[`screen snapshots > Activity feed 1`] = `
</div>
</div>
<div
class="flex flex-col items-center justify-center py-20 text-center"
class="flex flex-col items-center justify-center text-center py-20"
>
<div
class="w-14 h-14 rounded-2xl bg-surface-4 flex items-center justify-center mb-5"
class="rounded-2xl bg-surface-4 flex items-center justify-center w-14 h-14 mb-5"
>
<svg
class="lucide lucide-activity w-6 h-6 text-fg-muted"
@@ -2787,82 +2787,11 @@ exports[`screen snapshots > Claude Config 1`] = `
</button>
</div>
</div>
<div
class="card overflow-x-auto mt-4"
<p
class="card mt-4 px-4 py-6 text-center text-xs text-fg-muted"
>
<table
class="w-full min-w-[1000px]"
>
<thead>
<tr
class="border-b border-border text-left"
>
<th
class="px-4 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider"
>
Pattern
</th>
<th
class="px-4 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider"
>
Model
</th>
<th
class="px-4 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider text-right"
>
Input
</th>
<th
class="px-4 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider text-right"
>
Output
</th>
<th
class="px-4 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider text-right"
>
Cache Read
</th>
<th
class="px-4 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider text-right"
>
Cache Write (5m)
</th>
<th
class="px-4 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider text-right"
>
Cache Write (1h)
</th>
<th
class="px-4 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider text-right"
>
Fast Input
</th>
<th
class="px-4 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider text-right"
>
Fast Output
</th>
<th
class="w-24 px-4 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider"
>
Actions
</th>
</tr>
</thead>
<tbody
class="divide-y divide-border"
>
<tr>
<td
class="px-4 py-6 text-center text-xs text-fg-muted"
colspan="10"
>
No Claude model prices. Reset Defaults restores them.
</td>
</tr>
</tbody>
</table>
</div>
No Claude model prices. Reset Defaults restores them.
</p>
</section>
</div>
</div>
@@ -6833,10 +6762,10 @@ exports[`screen snapshots > Sessions 1`] = `
</div>
</div>
<div
class="flex flex-col items-center justify-center py-20 text-center"
class="flex flex-col items-center justify-center text-center py-20"
>
<div
class="w-14 h-14 rounded-2xl bg-surface-4 flex items-center justify-center mb-5"
class="rounded-2xl bg-surface-4 flex items-center justify-center w-14 h-14 mb-5"
>
<svg
class="lucide lucide-folder-open w-6 h-6 text-fg-muted"
@@ -6875,73 +6804,6 @@ exports[`screen snapshots > Settings 1`] = `
<div
class="animate-fade-in space-y-8"
>
<div
class="flex items-center justify-end gap-2"
>
<a
class="inline-flex items-center gap-1.5 text-xs px-3 py-1.5 rounded-md border border-border text-fg-secondary hover:text-fg-primary hover:border-border-light transition-colors"
download=""
href="/api/settings/export"
>
<svg
class="lucide lucide-file-down w-3.5 h-3.5"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"
/>
<path
d="M14 2v4a2 2 0 0 0 2 2h4"
/>
<path
d="M12 18v-6"
/>
<path
d="m9 15 3 3 3-3"
/>
</svg>
Export Data
</a>
<button
class="btn-ghost"
>
<svg
class="lucide lucide-refresh-cw w-4 h-4"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"
/>
<path
d="M21 3v5h-5"
/>
<path
d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"
/>
<path
d="M8 16H3v5"
/>
</svg>
Refresh
</button>
</div>
<section
class="card p-5"
>
@@ -6999,70 +6861,6 @@ exports[`screen snapshots > Settings 1`] = `
</div>
</div>
</section>
<div
class="card p-6"
>
<div
class="flex items-center justify-between flex-wrap gap-4"
>
<div
class="flex items-center gap-4"
>
<div
class="w-12 h-12 rounded-xl bg-status-success/10 border border-status-success/20 flex items-center justify-center"
>
<svg
class="lucide lucide-dollar-sign w-6 h-6 text-status-success"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<line
x1="12"
x2="12"
y1="2"
y2="22"
/>
<path
d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"
/>
</svg>
</div>
<div>
<p
class="text-sm text-fg-muted"
>
Total Estimated Cost
</p>
<p
class="text-2xl font-semibold text-fg-primary"
>
<span
class="relative inline-block cursor-default"
>
$0.00
</span>
</p>
</div>
</div>
<div
class="text-right text-xs text-fg-muted"
>
<p>
Across all tracked sessions
</p>
<p>
Based on per-model token usage
</p>
</div>
</div>
</div>
<section
class="scroll-mt-24"
id="lanes"
@@ -8125,44 +7923,181 @@ exports[`screen snapshots > Settings 1`] = `
class="scroll-mt-24"
id="data"
>
<h3
class="text-sm font-medium text-fg-secondary flex items-center gap-2 mb-1"
<div
class="mb-4 flex flex-wrap items-start justify-between gap-2"
>
<svg
class="lucide lucide-database w-4 h-4 text-fg-muted"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
<div>
<h3
class="text-sm font-medium text-fg-secondary flex items-center gap-2 mb-1"
>
<svg
class="lucide lucide-database w-4 h-4 text-fg-muted"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<ellipse
cx="12"
cy="5"
rx="9"
ry="3"
/>
<path
d="M3 5V19A9 3 0 0 0 21 19V5"
/>
<path
d="M3 12A9 3 0 0 0 21 12"
/>
</svg>
Data Management
</h3>
<p
class="text-xs text-fg-muted"
>
Database info, import/export, and cleanup.
</p>
</div>
<div
class="flex items-center gap-2"
>
<ellipse
cx="12"
cy="5"
rx="9"
ry="3"
/>
<path
d="M3 5V19A9 3 0 0 0 21 19V5"
/>
<path
d="M3 12A9 3 0 0 0 21 12"
/>
</svg>
Data Management
</h3>
<p
class="text-xs text-fg-muted mb-4"
>
Database info, import/export, and cleanup.
</p>
<a
class="inline-flex items-center gap-1.5 text-xs px-3 py-1.5 rounded-md border border-border text-fg-secondary hover:text-fg-primary hover:border-border-light transition-colors"
download=""
href="/api/settings/export"
>
<svg
class="lucide lucide-file-down w-3.5 h-3.5"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"
/>
<path
d="M14 2v4a2 2 0 0 0 2 2h4"
/>
<path
d="M12 18v-6"
/>
<path
d="m9 15 3 3 3-3"
/>
</svg>
Export Data
</a>
<button
class="btn-ghost"
>
<svg
class="lucide lucide-refresh-cw w-4 h-4"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"
/>
<path
d="M21 3v5h-5"
/>
<path
d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"
/>
<path
d="M8 16H3v5"
/>
</svg>
Refresh
</button>
</div>
</div>
<div
class="space-y-4"
>
<div
class="card p-6"
>
<div
class="flex items-center justify-between flex-wrap gap-4"
>
<div
class="flex items-center gap-4"
>
<div
class="w-12 h-12 rounded-xl bg-status-success/10 border border-status-success/20 flex items-center justify-center"
>
<svg
class="lucide lucide-dollar-sign w-6 h-6 text-status-success"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<line
x1="12"
x2="12"
y1="2"
y2="22"
/>
<path
d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"
/>
</svg>
</div>
<div>
<p
class="text-sm text-fg-muted"
>
Total Estimated Cost
</p>
<p
class="text-2xl font-semibold text-fg-primary"
>
<span
class="relative inline-block cursor-default"
>
$0.00
</span>
</p>
</div>
</div>
<div
class="text-right text-xs text-fg-muted"
>
<p>
Across all tracked sessions
</p>
<p>
Based on per-model token usage
</p>
</div>
</div>
</div>
<div
class="card p-5 space-y-4"
>
@@ -9123,10 +9058,10 @@ exports[`screen snapshots > Settings — notifications 1`] = `
</button>
</div>
<div
class="flex flex-col items-center justify-center py-20 text-center"
class="flex flex-col items-center justify-center text-center py-8"
>
<div
class="w-14 h-14 rounded-2xl bg-surface-4 flex items-center justify-center mb-5"
class="rounded-2xl bg-surface-4 flex items-center justify-center w-10 h-10 mb-3"
>
<svg
class="lucide lucide-list-checks w-6 h-6 text-fg-muted"
@@ -9160,10 +9095,10 @@ exports[`screen snapshots > Settings — notifications 1`] = `
<h3
class="text-base font-medium text-fg-secondary mb-2"
>
No alert rules yet. Add one to start monitoring for stuck agents, error bursts, idle sessions, or token spikes.
No alert rules yet
</h3>
<p
class="text-sm text-fg-muted max-w-md mb-6"
class="text-sm text-fg-muted max-w-md "
>
Add a rule to start alerting on errors, inactivity, stuck agents, or token thresholds.
</p>