feat(dashboard): enrich profiles dashboard and de-dupe channel env vars (#37872)

* feat(desktop): enrich profiles dashboard and de-dupe channel env vars

Add active-profile switching, role descriptions (manual + auto-generate
via the auxiliary LLM), per-profile model selection, and gateway-running
/ distribution badges to the GUI Profiles page. New profile creation
gains clone-all, optional description and model assignment.

Hide messaging-platform credentials (channel_managed) from the Keys/Env
page since the Channels page is the canonical surface for them, and
relabel the trimmed "messaging" category as "Gateway".

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(desktop): address review feedback on profiles/env changes

- ProfilesPage: scope the action-menu outside-click handler to the menu's
  own container via a ref so opening one card's menu no longer leaves
  others open.
- EnvPage: route the "Gateway" label and hint through i18n
  (t.common.gateway / gatewayHint) instead of hard-coded English, with an
  English fallback for untranslated locales.
- web_server: only report description_auto=true when auto-generation
  actually succeeded.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(desktop): address second-round review on profiles

- ProfilesPage: treat describe-auto success by null-checking the
  description and trust the response's description_auto flag instead of
  assuming true; disable the model-editor Save button unless the selected
  choice resolves to a real /api/model/options entry (avoids silent
  no-op saves).
- tests: cover the new profile endpoints (active get/set + 404,
  description round-trip + 404, model round-trip + 400 validation, and
  describe-auto success/failure contracts).

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(desktop): more profiles review fixes (toggles, races, tests)

- ProfilesPage: use the canonical `active` returned by setActiveProfile;
  make the SOUL/description/model action-menu items toggle their editor
  closed when already open; guard description save/auto-describe against
  stale responses via an activeDescRequest ref so a late reply can't
  clobber a different open editor.
- tests: assert /api/env channel_managed classification matches
  _channel_managed_env_keys().

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Austin Pickett
2026-06-03 10:37:36 -04:00
committed by GitHub
co-authored by Cursor
parent 214b7e070f
commit 7fb8a6b5c5
7 changed files with 1494 additions and 176 deletions
+25 -6
View File
@@ -513,12 +513,12 @@ export default function EnvPage() {
const categories = ["tool", "messaging", "setting"];
const CATEGORY_LABELS: Record<string, string> = {
tool: "Tools",
messaging: "Messaging",
messaging: t.common.gateway ?? "Gateway",
setting: "Settings",
};
for (const cat of categories) {
const hasEntries = Object.values(vars).some(
(info) => info.category === cat,
(info) => info.category === cat && !info.channel_managed,
);
if (hasEntries) {
items.push({ id: `section-${cat}`, label: CATEGORY_LABELS[cat] ?? cat });
@@ -526,7 +526,7 @@ export default function EnvPage() {
}
}
return items;
}, [vars]);
}, [vars, t]);
useLayoutEffect(() => {
if (!vars) {
@@ -681,21 +681,33 @@ export default function EnvPage() {
}))
.sort((a, b) => a.priority - b.priority);
// Non-provider categories — use translated labels
// Non-provider categories — use translated labels. Platform credentials
// (channel_managed) are configured on the Channels page, so the messaging
// category here is trimmed down to cross-cutting gateway / API / proxy
// settings and relabelled accordingly.
const CATEGORY_META_LABELS: Record<string, string> = {
tool: t.app.nav.keys,
messaging: t.common.messaging,
messaging: t.common.gateway ?? "Gateway",
setting: t.app.nav.config,
};
const CATEGORY_META_HINTS: Record<string, string | undefined> = {
messaging:
t.common.gatewayHint ??
"Messaging platforms, the API server and webhooks are configured on the Channels page. These are gateway-wide settings (proxy/relay mode and the global allowlist).",
};
const otherCategories = ["tool", "messaging", "setting"];
const nonProvider = otherCategories.map((cat) => {
const entries = Object.entries(vars).filter(
([, info]) => info.category === cat && (showAdvanced || !info.advanced),
([, info]) =>
info.category === cat &&
!info.channel_managed &&
(showAdvanced || !info.advanced),
);
const setEntries = entries.filter(([, info]) => info.is_set);
const unsetEntries = entries.filter(([, info]) => !info.is_set);
return {
label: CATEGORY_META_LABELS[cat] ?? cat,
hint: CATEGORY_META_HINTS[cat],
icon: CATEGORY_META_ICONS[cat] ?? KeyRound,
category: cat,
setEntries,
@@ -839,6 +851,7 @@ function EnvCategoryCard({
}: {
section: {
category: string;
hint?: string;
icon: React.ComponentType<{ className?: string }>;
label: string;
setEntries: [string, EnvVarInfo][];
@@ -899,6 +912,12 @@ function EnvCategoryCard({
{section.setEntries.length} {t.common.of} {section.totalEntries}{" "}
{t.common.configured}
</CardDescription>
{section.hint && (
<CardDescription className="text-text-tertiary">
{section.hint}
</CardDescription>
)}
</CardHeader>
{hasContent && (
File diff suppressed because it is too large Load Diff