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
591 changes: 186 additions & 405 deletions dashboard/src/v2/SchedulerPage.tsx

Large diffs are not rendered by default.

119 changes: 69 additions & 50 deletions dashboard/src/v2/components/settings/PersistentSkillStorageManager.tsx

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { DEFAULT_DASHBOARD_SETTINGS } from "../../../../lib/settings";
import { dashboardSettingsToProjectSettings } from "../../../lib/settings-view-models";
import type { ProjectSettings, SystemSettings, TechstackCatalogEntrySettings } from "../../../../types";
import { SettingsSmartFindSearch } from "../../../SettingsPage";
import { DashboardI18nProvider } from "../../../i18n";

const defaultInnerHeight = window.innerHeight;
const interactionStyle = { transitionDuration: "200ms", transitionTimingFunction: "ease" };
Expand Down Expand Up @@ -1331,7 +1332,7 @@ describe("SettingsControls Accessibility", () => {

it("SettingsContentPanels can suppress the active panel strip for a shared command/status bar", () => {
render(
<SettingsContentPanels
<DashboardI18nProvider storage={null}><SettingsContentPanels
showActivePanelStatus={false}
state={{
activeCategory: "general",
Expand All @@ -1342,7 +1343,7 @@ describe("SettingsControls Accessibility", () => {
loading: false,
resettingProject: false,
} as any}
/>
/></DashboardI18nProvider>
);

expect(screen.queryByText("Active panel")).not.toBeInTheDocument();
Expand All @@ -1352,7 +1353,7 @@ describe("SettingsControls Accessibility", () => {
it("SettingsContentPanels routes the Techstacks category to the catalog panel", () => {
const projectSettings = createProjectSettings();
render(
<SettingsContentPanels
<DashboardI18nProvider storage={null}><SettingsContentPanels
state={{
activeCategory: "techstacks",
activeScope: "system",
Expand All @@ -1365,7 +1366,7 @@ describe("SettingsControls Accessibility", () => {
systemSettings: createSystemSettings(projectSettings),
updateSystem: () => {},
} as any}
/>
/></DashboardI18nProvider>
);

expect(screen.getByText("Techstacks Catalog")).toBeInTheDocument();
Expand All @@ -1375,14 +1376,14 @@ describe("SettingsControls Accessibility", () => {
it("SettingsTechstacksPanel protects the built-in Code UX Stack from removal", () => {
const projectSettings = createProjectSettings();
render(
<SettingsTechstacksPanel
<DashboardI18nProvider storage={null}><SettingsTechstacksPanel
state={{
activeScope: "system",
activeSaving: false,
systemSettings: createSystemSettings(projectSettings),
updateSystem: () => {},
} as any}
/>
/></DashboardI18nProvider>
);

expect(screen.getByText("Built-in stack protected")).toBeInTheDocument();
Expand Down Expand Up @@ -1417,7 +1418,7 @@ describe("SettingsControls Accessibility", () => {
);
};

render(<Harness />);
render(<DashboardI18nProvider storage={null}><Harness /></DashboardI18nProvider>);

await user.click(screen.getByRole("radio", { name: /Unassigned/ }));

Expand Down Expand Up @@ -1451,7 +1452,7 @@ describe("SettingsControls Accessibility", () => {
);
};

render(<Harness />);
render(<DashboardI18nProvider storage={null}><Harness /></DashboardI18nProvider>);

await user.click(screen.getByRole("radio", { name: /Custom Web/ }));
await user.click(screen.getByRole("radio", { name: /Web app/ }));
Expand All @@ -1462,6 +1463,45 @@ describe("SettingsControls Accessibility", () => {
});
});

it("SettingsTechstacksPanel localizes German controls without translating saved values", async () => {
const user = userEvent.setup();
let latestProjectSettings = createProjectSettings({
selectedTechstackId: null,
applicationKind: null,
});
const systemSettings = createSystemSettings(latestProjectSettings);
const Harness = () => {
const [projectSettings, setProjectSettings] = useState(latestProjectSettings);
latestProjectSettings = projectSettings;
return (
<SettingsTechstacksPanel
state={{
activeScope: "project",
activeSaving: false,
projectSources: { "techstack.selectedTechstackId": "project" },
selectedProject: { id: "project-1", name: "Test project" },
projectSettings,
systemSettings,
updateEditableSettings: (recipe: (current: ProjectSettings) => ProjectSettings) => setProjectSettings(recipe),
getFieldReset: () => undefined,
} as any}
/>
);
};

render(<DashboardI18nProvider initialLocale="de" storage={null}><Harness /></DashboardI18nProvider>);
expect(screen.getByText("Projekt-Technologie-Stack")).toBeInTheDocument();
expect(screen.getByText("Projektüberschreibung")).toBeInTheDocument();
expect(screen.getByRole("radio", { name: /Custom Web/ })).toBeInTheDocument();
await user.click(screen.getByRole("radio", { name: /Custom Web/ }));
await user.click(screen.getByRole("radio", { name: /Web-App/ }));

expect(latestProjectSettings.techstack).toEqual({
selectedTechstackId: "custom-web",
applicationKind: "web",
});
});

it("SettingsPage keeps category context, scope controls, and actions in one unified sticky command surface", () => {
const source = readFileSync("dashboard/src/v2/SettingsPage.tsx", "utf8");
const commandStatusBarSource = source.match(
Expand Down
61 changes: 48 additions & 13 deletions dashboard/src/v2/components/settings/panels/QAPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,40 @@ const withAgentPresetIds = (trigger: QaTriggerSettings, nextIds: string[]): QaTr
type ReflectionLoopSettings = ProjectSettings["agents"]["selfReflection"]["planning"];
type ReflectionCriterion = ReflectionLoopSettings["criteria"][number];

export interface SelfReflectionControlsCopy {
enableAria: (title: string) => string;
optedIn: string;
offByDefault: string;
maxImprovementAttempts: string;
maxImprovementAttemptsAria: (title: string) => string;
criteriaRows: string;
addCriterion: string;
emptyCriteria: string;
label: string;
ratingPrompt: string;
threshold: string;
criterionFallback: (number: number) => string;
thresholdAria: (label: string) => string;
removeCriterionAria: (label: string) => string;
}

const DEFAULT_SELF_REFLECTION_COPY: SelfReflectionControlsCopy = {
enableAria: (title) => `Enable ${title}`,
optedIn: "Opted in",
offByDefault: "Off by default",
maxImprovementAttempts: "Max improvement attempts",
maxImprovementAttemptsAria: (title) => `${title} max improvement attempts`,
criteriaRows: "Criteria rows",
addCriterion: "Add criterion",
emptyCriteria: "No rating criteria are configured. Add a row before enabling self-reflection.",
label: "Label",
ratingPrompt: "Rating prompt",
threshold: "Threshold",
criterionFallback: (number) => `Criterion ${number}`,
thresholdAria: (label) => `${label} threshold`,
removeCriterionAria: (label) => `Remove ${label}`,
};

const clampThreshold = (value: number): number => (
Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : 0
);
Expand All @@ -48,7 +82,8 @@ export const SelfReflectionControls: FunctionComponent<{
getBadge: (path: string) => string | undefined;
basePath: string;
last?: boolean;
}> = ({ title, description, settings, update, getBadge, basePath, last }) => {
copy?: SelfReflectionControlsCopy;
}> = ({ title, description, settings, update, getBadge, basePath, last, copy = DEFAULT_SELF_REFLECTION_COPY }) => {
const updateCriterion = (criterionId: string, patch: Partial<ReflectionCriterion>): void => {
update({
...settings,
Expand Down Expand Up @@ -85,7 +120,7 @@ export const SelfReflectionControls: FunctionComponent<{
<div className="flex w-full min-w-0 flex-col gap-4">
<div className="flex flex-wrap items-center gap-3">
<Toggle
aria-label={`Enable ${title}`}
aria-label={copy.enableAria(title)}
value={settings.enabled}
onChange={(value) => update({ ...settings, enabled: value })}
/>
Expand All @@ -97,16 +132,16 @@ export const SelfReflectionControls: FunctionComponent<{
: "border-black/[0.06] bg-black/[0.03] text-slate-500 dark:border-white/[0.06] dark:bg-white/[0.03] dark:text-slate-400"
}`}
>
{settings.enabled ? "Opted in" : "Off by default"}
{settings.enabled ? copy.optedIn : copy.offByDefault}
</span>
</div>

<label className="flex flex-col gap-2">
<span className="text-[10px] font-bold uppercase tracking-[0.16em] text-slate-500 dark:text-slate-400">
Max improvement attempts
{copy.maxImprovementAttempts}
</span>
<NumberInput
aria-label={`${title} max improvement attempts`}
aria-label={copy.maxImprovementAttemptsAria(title)}
value={settings.maxImprovementAttempts}
min={0}
max={5}
Expand All @@ -120,21 +155,21 @@ export const SelfReflectionControls: FunctionComponent<{
<div className="flex flex-col gap-3">
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="text-[10px] font-bold uppercase tracking-[0.16em] text-slate-400">
Criteria rows
{copy.criteriaRows}
</div>
<button
type="button"
onClick={addCriterion}
className="inline-flex items-center gap-2 rounded-full border border-signal-500/25 bg-signal-500/[0.08] px-3 py-1.5 text-[10px] font-bold uppercase tracking-[0.14em] text-signal-700 transition-colors hover:bg-signal-500/[0.14] focus:outline-none focus-visible:ring-2 focus-visible:ring-signal-500/30 dark:text-signal-200"
>
<Plus className="h-3.5 w-3.5" strokeWidth={2.4} />
Add criterion
{copy.addCriterion}
</button>
</div>

{settings.criteria.length === 0 ? (
<div className="rounded-[1rem] border border-dashed border-black/[0.06] bg-black/[0.02] px-4 py-3 text-xs leading-relaxed text-slate-500 dark:border-white/[0.06] dark:bg-white/[0.02] dark:text-slate-400">
No rating criteria are configured. Add a row before enabling self-reflection.
{copy.emptyCriteria}
</div>
) : (
<div className="grid gap-3">
Expand All @@ -149,7 +184,7 @@ export const SelfReflectionControls: FunctionComponent<{
>
<label className="flex min-w-0 flex-col gap-1.5" htmlFor={labelId}>
<span className="text-[10px] font-bold uppercase tracking-[0.14em] text-slate-500 dark:text-slate-400">
Label
{copy.label}
</span>
<input
id={labelId}
Expand All @@ -161,7 +196,7 @@ export const SelfReflectionControls: FunctionComponent<{
</label>
<label className="flex min-w-0 flex-col gap-1.5" htmlFor={promptId}>
<span className="text-[10px] font-bold uppercase tracking-[0.14em] text-slate-500 dark:text-slate-400">
Rating prompt
{copy.ratingPrompt}
</span>
<textarea
id={promptId}
Expand All @@ -173,7 +208,7 @@ export const SelfReflectionControls: FunctionComponent<{
</label>
<label className="flex min-w-0 flex-col gap-1.5" htmlFor={thresholdId}>
<span className="text-[10px] font-bold uppercase tracking-[0.14em] text-slate-500 dark:text-slate-400">
Threshold
{copy.threshold}
</span>
<input
id={thresholdId}
Expand All @@ -182,7 +217,7 @@ export const SelfReflectionControls: FunctionComponent<{
max={1}
step={0.05}
value={criterion.threshold}
aria-label={`${criterion.label || `Criterion ${index + 1}`} threshold`}
aria-label={copy.thresholdAria(criterion.label || copy.criterionFallback(index + 1))}
onInput={(event) => updateCriterion(criterion.id, { threshold: clampThreshold(Number(event.currentTarget.value)) })}
className="rounded-xl border border-black/[0.08] bg-white/80 px-3 py-2 font-mono text-sm text-slate-800 outline-none focus:border-signal-500/40 focus:ring-2 focus:ring-signal-500/20 dark:border-white/[0.08] dark:bg-void-900/60 dark:text-slate-100"
/>
Expand All @@ -191,7 +226,7 @@ export const SelfReflectionControls: FunctionComponent<{
<button
type="button"
onClick={() => removeCriterion(criterion.id)}
aria-label={`Remove ${criterion.label || `criterion ${index + 1}`}`}
aria-label={copy.removeCriterionAria(criterion.label || copy.criterionFallback(index + 1))}
className="inline-flex h-10 w-10 items-center justify-center rounded-xl border border-status-red/20 bg-status-red/[0.06] text-status-red transition-colors hover:bg-status-red/[0.12] focus:outline-none focus-visible:ring-2 focus-visible:ring-status-red/30"
>
<Trash2 className="h-4 w-4" strokeWidth={2.4} />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { FunctionComponent } from "preact";
import { FileText } from "lucide-preact";
import type { SettingsPageState } from "../../../hooks/use-settings-page-state.js";
import { useDashboardI18n } from "../../../i18n/index.js";
import { settingsAgentsGuidanceMessages } from "../../../i18n/messages/settings-agents-guidance.js";
import { Row, Toggle } from "../SettingsFormFields.js";
import { SectionCard, getBadge, getFieldBadge } from "./SharedPanelComponents.js";

Expand All @@ -9,41 +11,47 @@ export interface SettingsAgentAuthoringPanelProps {
}

export const SettingsAgentAuthoringPanel: FunctionComponent<SettingsAgentAuthoringPanelProps> = ({ state }) => {
const { translate } = useDashboardI18n();
const { activeScope, editableSettings, projectSources, updateEditableSettings } = state;
if (!editableSettings) return null;

const mirrorEnabled = editableSettings.agents.saveToProjectDirectory;

return (
<SectionCard
title="Project Markdown Mirror"
title={translate(settingsAgentsGuidanceMessages, "mirrorTitle")}
watermark="AGT"
badge={getBadge(activeScope, projectSources, "agents")}
badge={getBadge(activeScope, projectSources, "agents")
? translate(settingsAgentsGuidanceMessages, "projectOverride")
: undefined}
helpId="project-markdown-mirror"
icon={<FileText strokeWidth={2.4} />}
accent="sky"
summary="Keep dashboard-authored project agents reviewable in the repository when file-based collaboration matters."
configureLabel="Configure file sync"
summary={translate(settingsAgentsGuidanceMessages, "mirrorSummary")}
configureLabel={translate(settingsAgentsGuidanceMessages, "mirrorConfigure")}
highlights={[
{ label: "Markdown mirror", value: mirrorEnabled ? "Enabled" : "Off", tone: mirrorEnabled ? "active" : "neutral" },
{ label: "Directory", value: ".code-ux/agents" },
{ label: "Scope", value: "Project agents" },
{ label: translate(settingsAgentsGuidanceMessages, "mirrorHighlight"), value: translate(settingsAgentsGuidanceMessages, mirrorEnabled ? "enabled" : "off"), tone: mirrorEnabled ? "active" : "neutral" },
{ label: translate(settingsAgentsGuidanceMessages, "mirrorDirectory"), value: ".code-ux/agents" },
{ label: translate(settingsAgentsGuidanceMessages, "mirrorScope"), value: translate(settingsAgentsGuidanceMessages, "mirrorProjectAgents") },
]}
>
<div className="grid min-w-0 gap-3 lg:grid-cols-[minmax(0,1fr)_minmax(12rem,0.45fr)]">
<div className="min-w-0 rounded-[1.2rem] border border-black/[0.06] bg-black/[0.02] px-4 py-3 text-xs font-medium leading-relaxed text-slate-500 dark:border-white/[0.06] dark:bg-white/[0.025] dark:text-slate-400">
Mirror project-authored agents into `.code-ux/agents` when instructions should move through normal repository review. The dashboard keeps built-in and home agents separate from this project mirror.
{translate(settingsAgentsGuidanceMessages, "mirrorExplanation")}
</div>
<div role="status" className="min-w-0 rounded-[1.2rem] border border-signal-500/18 bg-signal-500/[0.07] px-4 py-3 text-xs font-semibold leading-relaxed text-signal-700 dark:border-signal-400/18 dark:bg-signal-400/[0.08] dark:text-signal-200">
{mirrorEnabled ? "Mirror enabled for dashboard-authored project agents." : "Mirror disabled; project agents stay database-backed only."}
{translate(settingsAgentsGuidanceMessages, mirrorEnabled ? "mirrorEnabledStatus" : "mirrorDisabledStatus")}
</div>
</div>
<Row
label="Save agent markdown to project directory"
description="When enabled, dashboard edits write a companion markdown file under `.code-ux/agents` for the selected project. Default and home agent files are never modified."
badge={getFieldBadge(activeScope, projectSources, "agents.saveToProjectDirectory")}
label={translate(settingsAgentsGuidanceMessages, "mirrorSaveLabel")}
description={translate(settingsAgentsGuidanceMessages, "mirrorSaveDescription")}
badge={getFieldBadge(activeScope, projectSources, "agents.saveToProjectDirectory")
? translate(settingsAgentsGuidanceMessages, "projectOverride")
: undefined}
>
<Toggle
aria-label="Toggle setting"
aria-label={translate(settingsAgentsGuidanceMessages, "mirrorToggleAria")}
value={mirrorEnabled}
onChange={() => updateEditableSettings((current) => ({
...current,
Expand All @@ -54,7 +62,7 @@ export const SettingsAgentAuthoringPanel: FunctionComponent<SettingsAgentAuthori
}))}
/>
</Row>
<Row label="Mirror directory" description="Dashboard-authored markdown companions live alongside other project-local Code UX files." last>
<Row label={translate(settingsAgentsGuidanceMessages, "mirrorDirectory")} description={translate(settingsAgentsGuidanceMessages, "mirrorDirectoryDescription")} last>
<div className="min-w-0 break-all rounded-xl bg-black/[0.04] px-3 py-2 font-mono text-sm text-slate-600 dark:bg-white/[0.04] dark:text-slate-300">
.code-ux/agents
</div>
Expand Down
Loading
Loading