feat: move instructions to AI settings (#26624)

This commit is contained in:
Danielle Maywood
2026-06-24 12:29:58 +01:00
committed by GitHub
parent b11cd07d97
commit 3133a8b9c6
15 changed files with 817 additions and 602 deletions
+1 -2
View File
@@ -186,8 +186,7 @@ deployment. Use this to encode organizational conventions:
- Required review processes before merging.
- Any guardrails specific to your environment.
Configure the system prompt from **Agents** > **Settings** >
**Manage Agents** > **Instructions**
Configure the system prompt from **AI Settings** > **Instructions**
or via the API at `PUT /api/experimental/chats/config/system-prompt`.
See [Platform Controls](./platform-controls/index.md) for details.
@@ -52,9 +52,7 @@ Administrators can set a system prompt that applies to all agent sessions. This
is useful for establishing organizational conventions: coding standards,
commit message formats, preferred libraries, or repository-specific context.
This setting is available under **Agents** > **Settings** >
**Manage Agents** > **Instructions** and is only accessible to
administrators. Developers do not see or interact with it.
This setting is available under **AI Settings** > **Instructions** and is only accessible to administrators. Developers do not see or interact with it.
### Plan mode instructions
@@ -63,8 +61,7 @@ enters plan mode. These instructions supplement the built-in planning behavior
and are useful for organization-specific planning requirements such as required
plan sections, approval checkpoints, or review workflows.
This setting is available under **Agents** > **Settings** >
**Manage Agents** > **Instructions**. Developers do not edit it directly.
This setting is available under **AI Settings** > **Instructions**. Developers do not edit it directly.
The same value is exposed over the experimental chat configuration API:
@@ -35,6 +35,11 @@ const AISettingsSidebarView: FC<AISettingsSidebarViewProps> = ({
{permissions.editDeploymentConfig && (
<SidebarNavItem href="/ai/settings/models">Models</SidebarNavItem>
)}
{permissions.editDeploymentConfig && (
<SidebarNavItem href="/ai/settings/instructions">
Instructions
</SidebarNavItem>
)}
{permissions.editDeploymentConfig && (
<SidebarNavItem href="/ai/settings/lifecycle">
Lifecycle
@@ -8,9 +8,10 @@ import {
} from "#/api/queries/chats";
import { useAuthenticated } from "#/hooks/useAuthenticated";
import { RequirePermission } from "#/modules/permissions/RequirePermission";
import { AgentSettingsInstructionsPageView } from "./AgentSettingsInstructionsPageView";
import { pageTitle } from "#/utils/page";
import { InstructionsPageView } from "./InstructionsPageView";
const AgentSettingsInstructionsPage: FC = () => {
const InstructionsPage: FC = () => {
const { permissions } = useAuthenticated();
const queryClient = useQueryClient();
@@ -31,16 +32,22 @@ const AgentSettingsInstructionsPage: FC = () => {
return (
<RequirePermission isFeatureVisible={permissions.editDeploymentConfig}>
<AgentSettingsInstructionsPageView
<title>{pageTitle("Instructions", "AI Settings")}</title>
<InstructionsPageView
systemPromptData={systemPromptQuery.data}
planModeInstructionsData={planModeInstructionsQuery.data}
onSaveSystemPrompt={saveSystemPromptMutation.mutate}
isSavingSystemPrompt={saveSystemPromptMutation.isPending}
isSaveSystemPromptError={saveSystemPromptMutation.isError}
onSavePlanModeInstructions={savePlanModeInstructionsMutation.mutate}
isSavingPlanModeInstructions={
onSaveSystemPrompt={saveSystemPromptMutation.mutateAsync}
onSavePlanModeInstructions={
savePlanModeInstructionsMutation.mutateAsync
}
onResetSystemPromptSave={saveSystemPromptMutation.reset}
onResetPlanModeInstructionsSave={savePlanModeInstructionsMutation.reset}
isSaving={
saveSystemPromptMutation.isPending ||
savePlanModeInstructionsMutation.isPending
}
isSaveSystemPromptError={saveSystemPromptMutation.isError}
isSavePlanModeInstructionsError={
savePlanModeInstructionsMutation.isError
}
@@ -49,4 +56,4 @@ const AgentSettingsInstructionsPage: FC = () => {
);
};
export default AgentSettingsInstructionsPage;
export default InstructionsPage;
@@ -0,0 +1,485 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { type FC, useState } from "react";
import { expect, fn, userEvent, waitFor, within } from "storybook/test";
import {
InstructionsPageView,
type InstructionsPageViewProps,
} from "./InstructionsPageView";
const mockDefaultSystemPrompt = "You are Coder, an AI coding assistant.";
const saveOrder: string[] = [];
const baseArgs: InstructionsPageViewProps = {
systemPromptData: {
system_prompt: "Always explain tradeoffs before proposing a change.",
include_default_system_prompt: true,
default_system_prompt: mockDefaultSystemPrompt,
},
planModeInstructionsData: {
plan_mode_instructions:
"Use a numbered checklist for implementation plans.",
},
onSaveSystemPrompt: fn(async () => undefined),
onSavePlanModeInstructions: fn(async () => undefined),
onResetSystemPromptSave: fn(),
onResetPlanModeInstructionsSave: fn(),
isSaving: false,
isSaveSystemPromptError: false,
isSavePlanModeInstructionsError: false,
};
const meta = {
title: "pages/AISettingsPage/InstructionsPage/InstructionsPageView",
component: InstructionsPageView,
args: baseArgs,
} satisfies Meta<typeof InstructionsPageView>;
export default meta;
type Story = StoryObj<typeof InstructionsPageView>;
export const Default: Story = {};
export const AdminWithDefaultToggleOn: Story = {
args: {
systemPromptData: {
system_prompt: "Always use TypeScript for code examples.",
include_default_system_prompt: true,
default_system_prompt: mockDefaultSystemPrompt,
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const body = within(canvasElement.ownerDocument.body);
const toggle = await canvas.findByRole("switch", {
name: "Include Coder Agents default system prompt",
});
expect(toggle).toBeChecked();
const promptInput = await canvas.findByDisplayValue(
"Always use TypeScript for code examples.",
);
expect(promptInput).toBeInTheDocument();
expect(
canvas.getByText("Additional system instructions"),
).toBeInTheDocument();
await userEvent.click(canvas.getByRole("button", { name: "View prompt" }));
expect(await body.findByText("Default System Prompt")).toBeInTheDocument();
expect(body.getByText(mockDefaultSystemPrompt)).toBeInTheDocument();
await userEvent.keyboard("{Escape}");
await waitFor(() => {
expect(body.queryByText("Default System Prompt")).not.toBeInTheDocument();
});
await userEvent.click(toggle);
const saveButton = canvas.getByRole("button", {
name: "Save",
});
await waitFor(() => {
expect(saveButton).toBeEnabled();
});
},
};
export const AdminWithDefaultToggleOff: Story = {
args: {
systemPromptData: {
system_prompt: "You are a custom assistant.",
include_default_system_prompt: false,
default_system_prompt: mockDefaultSystemPrompt,
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const toggle = await canvas.findByRole("switch", {
name: "Include Coder Agents default system prompt",
});
expect(toggle).not.toBeChecked();
expect(
await canvas.findByDisplayValue("You are a custom assistant."),
).toBeInTheDocument();
expect(
canvas.getByText("Additional system instructions"),
).toBeInTheDocument();
},
};
export const InvisibleUnicodeWarningSystemPrompt: Story = {
args: {
systemPromptData: {
system_prompt:
"Normal prompt text\u200b\u200b\u200b\u200bhidden instruction",
include_default_system_prompt: true,
default_system_prompt: mockDefaultSystemPrompt,
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByText("Additional system instructions");
const alert = await canvas.findByText(/invisible Unicode/);
expect(alert).toBeInTheDocument();
expect(alert.textContent).toContain("4");
},
};
export const NoWarningForCleanPrompt: Story = {
args: {
systemPromptData: {
system_prompt: "You are a helpful coding assistant.",
include_default_system_prompt: true,
default_system_prompt: mockDefaultSystemPrompt,
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByText("Additional system instructions");
await canvas.findByDisplayValue("You are a helpful coding assistant.");
expect(canvas.queryByText(/invisible Unicode/)).toBeNull();
},
};
export const SavesSystemPrompt: Story = {
args: {
systemPromptData: {
system_prompt: "",
include_default_system_prompt: false,
default_system_prompt: mockDefaultSystemPrompt,
},
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const textarea = await canvas.findByLabelText(
"Additional system instructions",
);
await userEvent.type(textarea, "Always explain tradeoffs first.");
await userEvent.click(
canvas.getByRole("switch", {
name: "Include Coder Agents default system prompt",
}),
);
const saveButton = canvas.getByRole("button", {
name: "Save",
});
await waitFor(() => {
expect(saveButton).toBeEnabled();
});
await userEvent.click(saveButton);
await waitFor(() => {
expect(args.onSaveSystemPrompt).toHaveBeenCalledWith({
system_prompt: "Always explain tradeoffs first.",
include_default_system_prompt: true,
});
});
expect(args.onSavePlanModeInstructions).not.toHaveBeenCalled();
},
};
export const SavesPlanModeInstructions: Story = {
args: {
planModeInstructionsData: { plan_mode_instructions: "" },
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const textarea = await canvas.findByLabelText(
"Additional plan mode instructions",
);
await userEvent.clear(textarea);
await userEvent.type(textarea, "Always produce a concise plan first.");
const saveButton = canvas.getByRole("button", {
name: "Save",
});
await waitFor(() => {
expect(saveButton).toBeEnabled();
});
await userEvent.click(saveButton);
await waitFor(() => {
expect(args.onSavePlanModeInstructions).toHaveBeenCalledWith({
plan_mode_instructions: "Always produce a concise plan first.",
});
});
expect(args.onSaveSystemPrompt).not.toHaveBeenCalled();
},
};
export const SystemPromptSaveErrorThenCancel: Story = {
args: {
systemPromptData: {
system_prompt: "Baseline system prompt.",
include_default_system_prompt: false,
default_system_prompt: mockDefaultSystemPrompt,
},
onResetSystemPromptSave: fn(),
onResetPlanModeInstructionsSave: fn(),
isSaveSystemPromptError: true,
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
expect(
canvas.getByText("Failed to save system prompt."),
).toBeInTheDocument();
const textarea = await canvas.findByLabelText(
"Additional system instructions",
);
await userEvent.type(textarea, " edited");
const cancelButton = canvas.getByRole("button", {
name: "Cancel",
});
await waitFor(() => {
expect(cancelButton).toBeEnabled();
});
await userEvent.click(cancelButton);
expect(args.onResetSystemPromptSave).toHaveBeenCalledTimes(1);
expect(args.onResetPlanModeInstructionsSave).toHaveBeenCalledTimes(1);
await waitFor(() => {
expect(
canvas.getByDisplayValue("Baseline system prompt."),
).toBeInTheDocument();
});
},
};
export const CleanSystemPromptErrorCancelDismisses: Story = {
args: {
onResetSystemPromptSave: fn(),
onResetPlanModeInstructionsSave: fn(),
isSaveSystemPromptError: true,
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
expect(
canvas.getByText("Failed to save system prompt."),
).toBeInTheDocument();
const cancelButton = canvas.getByRole("button", {
name: "Cancel",
});
const saveButton = canvas.getByRole("button", {
name: "Save",
});
await waitFor(() => {
expect(cancelButton).toBeEnabled();
});
expect(saveButton).toBeDisabled();
await userEvent.click(cancelButton);
expect(args.onResetSystemPromptSave).toHaveBeenCalledTimes(1);
expect(args.onResetPlanModeInstructionsSave).toHaveBeenCalledTimes(1);
},
};
export const CleanPlanModeInstructionsErrorCancelDismisses: Story = {
args: {
onResetSystemPromptSave: fn(),
onResetPlanModeInstructionsSave: fn(),
isSavePlanModeInstructionsError: true,
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
expect(
canvas.getByText("Failed to save plan mode instructions."),
).toBeInTheDocument();
const cancelButton = canvas.getByRole("button", {
name: "Cancel",
});
const saveButton = canvas.getByRole("button", {
name: "Save",
});
await waitFor(() => {
expect(cancelButton).toBeEnabled();
});
expect(saveButton).toBeDisabled();
await userEvent.click(cancelButton);
expect(args.onResetSystemPromptSave).toHaveBeenCalledTimes(1);
expect(args.onResetPlanModeInstructionsSave).toHaveBeenCalledTimes(1);
},
};
export const CleanBothErrorsCancelDismisses: Story = {
args: {
onResetSystemPromptSave: fn(),
onResetPlanModeInstructionsSave: fn(),
isSaveSystemPromptError: true,
isSavePlanModeInstructionsError: true,
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
expect(
canvas.getByText("Failed to save system prompt."),
).toBeInTheDocument();
expect(
canvas.getByText("Failed to save plan mode instructions."),
).toBeInTheDocument();
const cancelButton = canvas.getByRole("button", {
name: "Cancel",
});
const saveButton = canvas.getByRole("button", {
name: "Save",
});
await waitFor(() => {
expect(cancelButton).toBeEnabled();
});
expect(saveButton).toBeDisabled();
await userEvent.click(cancelButton);
expect(args.onResetSystemPromptSave).toHaveBeenCalledTimes(1);
expect(args.onResetPlanModeInstructionsSave).toHaveBeenCalledTimes(1);
},
};
export const PlanModeInstructionsSaveError: Story = {
args: {
isSavePlanModeInstructionsError: true,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(
canvas.getByText("Failed to save plan mode instructions."),
).toBeInTheDocument();
},
};
export const SavesBothSections: Story = {
args: {
systemPromptData: {
system_prompt: "",
include_default_system_prompt: false,
default_system_prompt: mockDefaultSystemPrompt,
},
planModeInstructionsData: { plan_mode_instructions: "" },
onSaveSystemPrompt: fn(async () => {
saveOrder.push("system");
}),
onSavePlanModeInstructions: fn(async () => {
saveOrder.push("plan");
}),
},
play: async ({ canvasElement, args }) => {
saveOrder.length = 0;
const canvas = within(canvasElement);
const systemPromptTextarea = await canvas.findByLabelText(
"Additional system instructions",
);
const planModeTextarea = await canvas.findByLabelText(
"Additional plan mode instructions",
);
await userEvent.type(systemPromptTextarea, "New system instruction.");
await userEvent.type(planModeTextarea, "New plan mode guidance.");
const saveButton = canvas.getByRole("button", {
name: "Save",
});
await waitFor(() => {
expect(saveButton).toBeEnabled();
});
await userEvent.click(saveButton);
await waitFor(() => {
expect(args.onSaveSystemPrompt).toHaveBeenCalledWith({
system_prompt: "New system instruction.",
include_default_system_prompt: false,
});
});
await waitFor(() => {
expect(args.onSavePlanModeInstructions).toHaveBeenCalledWith({
plan_mode_instructions: "New plan mode guidance.",
});
});
expect(saveOrder).toEqual(["system", "plan"]);
},
};
const RefetchPromptWrapper: FC = () => {
const [systemPromptValue, setSystemPromptValue] = useState("Old");
return (
<>
<button
type="button"
onClick={() => setSystemPromptValue("New")}
className="sr-only"
>
Simulate system prompt refetch
</button>
<InstructionsPageView
systemPromptData={{
system_prompt: systemPromptValue,
include_default_system_prompt: false,
default_system_prompt: mockDefaultSystemPrompt,
}}
planModeInstructionsData={{
plan_mode_instructions: "Baseline plan mode guidance.",
}}
onSaveSystemPrompt={fn()}
onSavePlanModeInstructions={fn()}
onResetSystemPromptSave={fn()}
onResetPlanModeInstructionsSave={fn()}
isSaving={false}
isSaveSystemPromptError={false}
isSavePlanModeInstructionsError={true}
/>
</>
);
};
export const PartialSaveFailureCancelResyncsToServer: Story = {
render: () => <RefetchPromptWrapper />,
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByDisplayValue("Old");
await canvas.findByDisplayValue("Baseline plan mode guidance.");
expect(
canvas.getByText("Failed to save plan mode instructions."),
).toBeInTheDocument();
const planTextarea = await canvas.findByLabelText(
"Additional plan mode instructions",
);
await userEvent.type(planTextarea, " edited");
await userEvent.click(
canvas.getByRole("button", { name: "Simulate system prompt refetch" }),
);
const cancelButton = canvas.getByRole("button", {
name: "Cancel",
});
await waitFor(() => {
expect(cancelButton).toBeEnabled();
});
await userEvent.click(cancelButton);
await waitFor(() => {
expect(canvas.getByDisplayValue("New")).toBeInTheDocument();
expect(canvas.queryByDisplayValue("Old")).toBeNull();
});
},
};
@@ -0,0 +1,281 @@
import { useFormik } from "formik";
import type { FC } from "react";
import { useState } from "react";
import TextareaAutosize from "react-textarea-autosize";
import { toast } from "sonner";
import type * as TypesGen from "#/api/typesGenerated";
import { Alert, AlertDescription } from "#/components/Alert/Alert";
import { Button } from "#/components/Button/Button";
import {
SettingsHeader,
SettingsHeaderDescription,
SettingsHeaderTitle,
} from "#/components/SettingsHeader/SettingsHeader";
import { Spinner } from "#/components/Spinner/Spinner";
import { Switch } from "#/components/Switch/Switch";
import { TextPreviewDialog } from "#/pages/AgentsPage/components/TextPreviewDialog";
import { countInvisibleCharacters } from "#/utils/invisibleUnicode";
const TEXTAREA_MAX_ROWS = 9;
export interface InstructionsPageViewProps {
systemPromptData: TypesGen.ChatSystemPromptResponse | undefined;
planModeInstructionsData:
| TypesGen.ChatPlanModeInstructionsResponse
| undefined;
onSaveSystemPrompt: (
req: TypesGen.UpdateChatSystemPromptRequest,
) => Promise<void> | void;
onSavePlanModeInstructions: (
req: TypesGen.UpdateChatPlanModeInstructionsRequest,
) => Promise<void> | void;
onResetSystemPromptSave: () => void;
onResetPlanModeInstructionsSave: () => void;
isSaving: boolean;
isSaveSystemPromptError: boolean;
isSavePlanModeInstructionsError: boolean;
}
export const InstructionsPageView: FC<InstructionsPageViewProps> = ({
systemPromptData,
planModeInstructionsData,
...formProps
}) => {
const hasLoadedInstructions =
systemPromptData !== undefined && planModeInstructionsData !== undefined;
// Without this gate, Formik would initialize from empty query fallbacks and
// keep those values after query data loads.
if (!hasLoadedInstructions) {
return null;
}
return (
<InstructionsForm
systemPromptData={systemPromptData}
planModeInstructionsData={planModeInstructionsData}
{...formProps}
/>
);
};
interface InstructionsFormProps {
systemPromptData: TypesGen.ChatSystemPromptResponse;
planModeInstructionsData: TypesGen.ChatPlanModeInstructionsResponse;
onSaveSystemPrompt: (
req: TypesGen.UpdateChatSystemPromptRequest,
) => Promise<void> | void;
onSavePlanModeInstructions: (
req: TypesGen.UpdateChatPlanModeInstructionsRequest,
) => Promise<void> | void;
onResetSystemPromptSave: () => void;
onResetPlanModeInstructionsSave: () => void;
isSaving: boolean;
isSaveSystemPromptError: boolean;
isSavePlanModeInstructionsError: boolean;
}
const InstructionsForm: FC<InstructionsFormProps> = ({
systemPromptData,
planModeInstructionsData,
onSaveSystemPrompt,
onSavePlanModeInstructions,
onResetSystemPromptSave,
onResetPlanModeInstructionsSave,
isSaving,
isSaveSystemPromptError,
isSavePlanModeInstructionsError,
}) => {
const [showDefaultPromptPreview, setShowDefaultPromptPreview] =
useState(false);
const defaultSystemPrompt = systemPromptData.default_system_prompt ?? "";
const initialValues = {
system_prompt: systemPromptData.system_prompt ?? "",
include_default_system_prompt:
systemPromptData.include_default_system_prompt ?? false,
plan_mode_instructions:
planModeInstructionsData.plan_mode_instructions ?? "",
};
const form = useFormik({
initialValues,
onSubmit: async (values, { resetForm, setValues }) => {
onResetSystemPromptSave();
onResetPlanModeInstructionsSave();
try {
if (
values.system_prompt !== initialValues.system_prompt ||
values.include_default_system_prompt !==
initialValues.include_default_system_prompt
) {
await onSaveSystemPrompt({
system_prompt: values.system_prompt,
include_default_system_prompt: values.include_default_system_prompt,
});
}
if (
values.plan_mode_instructions !== initialValues.plan_mode_instructions
) {
await onSavePlanModeInstructions({
plan_mode_instructions: values.plan_mode_instructions,
});
}
} catch (error) {
await setValues(values, false);
throw error;
}
toast.success("Instructions saved successfully.");
resetForm({ values });
},
});
const systemInvisibleCharCount = countInvisibleCharacters(
form.values.system_prompt,
);
const planModeInvisibleCharCount = countInvisibleCharacters(
form.values.plan_mode_instructions,
);
const isDisabled = isSaving || form.isSubmitting;
return (
<div className="flex max-w-4xl flex-col gap-8">
<SettingsHeader>
<SettingsHeaderTitle>Instructions</SettingsHeaderTitle>
<SettingsHeaderDescription>
Control the system prompts and plan mode instructions used across the
deployment.
</SettingsHeaderDescription>
</SettingsHeader>
<form
className="flex flex-col rounded-lg border border-solid border-border p-6"
onSubmit={form.handleSubmit}
>
<div className="flex items-center gap-2 font-sans text-sm font-normal leading-6 text-content-primary">
<Switch
checked={form.values.include_default_system_prompt}
onCheckedChange={(checked) =>
form.setFieldValue("include_default_system_prompt", checked)
}
aria-label="Include Coder Agents default system prompt"
disabled={isDisabled}
/>
<div className="flex min-w-0 items-center gap-1.5">
<span>Include Coder Agents default system prompt.</span>
<Button
size="xs"
variant="subtle"
type="button"
onClick={() => setShowDefaultPromptPreview(true)}
className="min-w-0 px-0 font-sans text-sm font-normal leading-6 text-content-link hover:text-content-link"
>
View prompt
</Button>
</div>
</div>
<label
className="mt-4 mb-2 font-sans text-sm font-bold leading-6 text-content-primary"
htmlFor="system_prompt"
>
Additional system instructions
</label>
<TextareaAutosize
className="w-full resize-none overflow-y-auto rounded-lg border border-solid border-border bg-surface-primary px-4 py-3 font-sans text-sm font-normal leading-6 text-content-primary placeholder:text-content-secondary focus:outline-none focus:ring-2 focus:ring-content-link/30 [scrollbar-width:thin]"
id="system_prompt"
placeholder="Instructions appended to every agent session"
name="system_prompt"
value={form.values.system_prompt}
onChange={form.handleChange}
disabled={isDisabled}
minRows={1}
maxRows={TEXTAREA_MAX_ROWS}
/>
{systemInvisibleCharCount > 0 && (
<Alert severity="warning" className="mt-2">
<AlertDescription>
This text contains {systemInvisibleCharCount} invisible Unicode{" "}
{systemInvisibleCharCount !== 1 ? "characters" : "character"} that
could hide content. These will be stripped on save.
</AlertDescription>
</Alert>
)}
<label
className="mt-8 mb-2 font-sans text-sm font-bold leading-6 text-content-primary"
htmlFor="plan_mode_instructions"
>
Additional plan mode instructions
</label>
<TextareaAutosize
className="w-full resize-none overflow-y-auto rounded-lg border border-solid border-border bg-surface-primary px-4 py-3 font-sans text-sm font-normal leading-6 text-content-primary placeholder:text-content-secondary focus:outline-none focus:ring-2 focus:ring-content-link/30 [scrollbar-width:thin]"
id="plan_mode_instructions"
placeholder="Instructions applied when the agent enters plan mode"
name="plan_mode_instructions"
value={form.values.plan_mode_instructions}
onChange={form.handleChange}
disabled={isDisabled}
minRows={4}
maxRows={TEXTAREA_MAX_ROWS}
/>
{planModeInvisibleCharCount > 0 && (
<Alert severity="warning" className="mt-2">
<AlertDescription>
This text contains {planModeInvisibleCharCount} invisible Unicode{" "}
{planModeInvisibleCharCount !== 1 ? "characters" : "character"}{" "}
that could hide content. These will be stripped on save.
</AlertDescription>
</Alert>
)}
{isSaveSystemPromptError && (
<p className="m-0 mt-4 text-xs text-content-destructive">
Failed to save system prompt.
</p>
)}
{isSavePlanModeInstructionsError && (
<p className="m-0 mt-4 text-xs text-content-destructive">
Failed to save plan mode instructions.
</p>
)}
<div className="mt-8 flex justify-end gap-4">
<Button
variant="outline"
type="button"
onClick={() => {
// Save failures leave mutation errors outside Formik state, so
// both stores must reset before the clean form disables actions.
onResetSystemPromptSave();
onResetPlanModeInstructionsSave();
form.resetForm({ values: initialValues });
}}
disabled={
isDisabled ||
(!form.dirty &&
!isSaveSystemPromptError &&
!isSavePlanModeInstructionsError)
}
>
Cancel
</Button>
<Button type="submit" disabled={isDisabled || !form.dirty}>
{isSaving && <Spinner loading className="h-4 w-4" />}
Save
</Button>
</div>
</form>
{showDefaultPromptPreview && (
<TextPreviewDialog
content={defaultSystemPrompt}
fileName="Default System Prompt"
onClose={() => setShowDefaultPromptPreview(false)}
/>
)}
</div>
);
};
@@ -1,181 +0,0 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect, fn, userEvent, waitFor, within } from "storybook/test";
import {
AgentSettingsInstructionsPageView,
type AgentSettingsInstructionsPageViewProps,
} from "./AgentSettingsInstructionsPageView";
const mockDefaultSystemPrompt = "You are Coder, an AI coding assistant.";
const baseArgs: AgentSettingsInstructionsPageViewProps = {
systemPromptData: {
system_prompt: "Always explain tradeoffs before proposing a change.",
include_default_system_prompt: true,
default_system_prompt: mockDefaultSystemPrompt,
},
planModeInstructionsData: {
plan_mode_instructions:
"Use a numbered checklist for implementation plans.",
},
onSaveSystemPrompt: fn(),
isSavingSystemPrompt: false,
isSaveSystemPromptError: false,
onSavePlanModeInstructions: fn(),
isSavingPlanModeInstructions: false,
isSavePlanModeInstructionsError: false,
};
const meta = {
title: "pages/AgentsPage/AgentSettingsInstructionsPageView",
component: AgentSettingsInstructionsPageView,
args: baseArgs,
} satisfies Meta<typeof AgentSettingsInstructionsPageView>;
export default meta;
type Story = StoryObj<typeof AgentSettingsInstructionsPageView>;
export const Default: Story = {};
export const AdminWithDefaultToggleOn: Story = {
args: {
systemPromptData: {
system_prompt: "Always use TypeScript for code examples.",
include_default_system_prompt: true,
default_system_prompt: mockDefaultSystemPrompt,
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const body = within(canvasElement.ownerDocument.body);
const toggle = await canvas.findByRole("switch", {
name: "Include Coder Agents default system prompt",
});
expect(toggle).toBeChecked();
const promptInput = await canvas.findByDisplayValue(
"Always use TypeScript for code examples.",
);
expect(promptInput).toBeInTheDocument();
expect(
canvas.getByText(/built-in Coder Agents prompt is prepended/i),
).toBeInTheDocument();
await userEvent.click(canvas.getByRole("button", { name: "Preview" }));
expect(await body.findByText("Default System Prompt")).toBeInTheDocument();
expect(body.getByText(mockDefaultSystemPrompt)).toBeInTheDocument();
await userEvent.keyboard("{Escape}");
await waitFor(() => {
expect(body.queryByText("Default System Prompt")).not.toBeInTheDocument();
});
await userEvent.click(toggle);
const promptForm = promptInput.closest("form");
if (!(promptForm instanceof HTMLFormElement)) {
throw new Error("Expected system prompt textarea to live inside a form.");
}
const saveButton = within(promptForm).getByRole("button", {
name: "Save",
});
await waitFor(() => {
expect(saveButton).toBeEnabled();
});
},
};
export const AdminWithDefaultToggleOff: Story = {
args: {
systemPromptData: {
system_prompt: "You are a custom assistant.",
include_default_system_prompt: false,
default_system_prompt: mockDefaultSystemPrompt,
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const toggle = await canvas.findByRole("switch", {
name: "Include Coder Agents default system prompt",
});
expect(toggle).not.toBeChecked();
expect(
await canvas.findByDisplayValue("You are a custom assistant."),
).toBeInTheDocument();
expect(
canvas.getByText(/only the additional instructions below are used/i),
).toBeInTheDocument();
},
};
export const InvisibleUnicodeWarningSystemPrompt: Story = {
args: {
systemPromptData: {
system_prompt:
"Normal prompt text\u200b\u200b\u200b\u200bhidden instruction",
include_default_system_prompt: true,
default_system_prompt: mockDefaultSystemPrompt,
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByText("System instructions");
const alert = await canvas.findByText(/invisible Unicode/);
expect(alert).toBeInTheDocument();
expect(alert.textContent).toContain("4");
},
};
// The deleted combined story covered both prompt editors on one page. After
// the split, this story covers the system prompt half and the General page
// stories cover the personal instructions half.
export const NoWarningForCleanPrompt: Story = {
args: {
systemPromptData: {
system_prompt: "You are a helpful coding assistant.",
include_default_system_prompt: true,
default_system_prompt: mockDefaultSystemPrompt,
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByText("System instructions");
await canvas.findByDisplayValue("You are a helpful coding assistant.");
expect(canvas.queryByText(/invisible Unicode/)).toBeNull();
},
};
export const SavesPlanModeInstructions: Story = {
args: {
planModeInstructionsData: { plan_mode_instructions: "" },
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const textarea = await canvas.findByPlaceholderText(
"Additional instructions for planning mode",
);
await userEvent.clear(textarea);
await userEvent.type(textarea, "Always produce a concise plan first.");
const planModeForm = textarea.closest("form");
if (!(planModeForm instanceof HTMLFormElement)) {
throw new Error(
"Expected plan mode instructions textarea to live inside a form.",
);
}
const saveButton = within(planModeForm).getByRole("button", {
name: "Save",
});
await waitFor(() => {
expect(saveButton).toBeEnabled();
});
await userEvent.click(saveButton);
await waitFor(() => {
expect(args.onSavePlanModeInstructions).toHaveBeenCalledWith(
{ plan_mode_instructions: "Always produce a concise plan first." },
expect.anything(),
);
});
},
};
@@ -1,67 +0,0 @@
import type { FC } from "react";
import type { UseMutateFunction } from "react-query";
import type * as TypesGen from "#/api/typesGenerated";
import { PlanModeInstructionsSettings } from "./components/PlanModeInstructionsSettings";
import { SectionHeader } from "./components/SectionHeader";
import { SystemInstructionsSettings } from "./components/SystemInstructionsSettings";
export interface AgentSettingsInstructionsPageViewProps {
systemPromptData: TypesGen.ChatSystemPromptResponse | undefined;
planModeInstructionsData:
| TypesGen.ChatPlanModeInstructionsResponse
| undefined;
onSaveSystemPrompt: UseMutateFunction<
void,
Error,
TypesGen.UpdateChatSystemPromptRequest,
unknown
>;
isSavingSystemPrompt: boolean;
isSaveSystemPromptError: boolean;
onSavePlanModeInstructions: UseMutateFunction<
void,
Error,
TypesGen.UpdateChatPlanModeInstructionsRequest,
unknown
>;
isSavingPlanModeInstructions: boolean;
isSavePlanModeInstructionsError: boolean;
}
export const AgentSettingsInstructionsPageView: FC<
AgentSettingsInstructionsPageViewProps
> = ({
systemPromptData,
planModeInstructionsData,
onSaveSystemPrompt,
isSavingSystemPrompt,
isSaveSystemPromptError,
onSavePlanModeInstructions,
isSavingPlanModeInstructions,
isSavePlanModeInstructionsError,
}) => {
const isAnyPromptSaving =
isSavingSystemPrompt || isSavingPlanModeInstructions;
return (
<div className="flex flex-col gap-8">
<SectionHeader
label="Instructions"
description="Control the system prompts and plan mode instructions used across the deployment."
/>
<SystemInstructionsSettings
systemPromptData={systemPromptData}
onSaveSystemPrompt={onSaveSystemPrompt}
isSavingSystemPrompt={isSavingSystemPrompt}
isSaveSystemPromptError={isSaveSystemPromptError}
isAnyPromptSaving={isAnyPromptSaving}
/>
<PlanModeInstructionsSettings
planModeInstructionsData={planModeInstructionsData}
onSavePlanModeInstructions={onSavePlanModeInstructions}
isSavePlanModeInstructionsError={isSavePlanModeInstructionsError}
isAnyPromptSaving={isAnyPromptSaving}
/>
</div>
);
};
@@ -33,7 +33,6 @@ import { AgentSettingsAgentsPageView } from "./AgentSettingsAgentsPageView";
import AgentSettingsCompactionPage from "./AgentSettingsCompactionPage";
import AgentSettingsExperimentsPage from "./AgentSettingsExperimentsPage";
import AgentSettingsGeneralPage from "./AgentSettingsGeneralPage";
import AgentSettingsInstructionsPage from "./AgentSettingsInstructionsPage";
import AgentSettingsPage from "./AgentSettingsPage";
import AgentSettingsSpendPage from "./AgentSettingsSpendPage";
import { type AgentsOutletContext, AgentsPageView } from "./AgentsPageView";
@@ -207,7 +206,7 @@ const agentsRouting = {
{ path: "compaction", element: <AgentSettingsCompactionPage /> },
{
path: "instructions",
element: <AgentSettingsInstructionsPage />,
element: <Navigate to="/ai/settings/instructions" replace />,
},
{ path: "experiments", element: <AgentSettingsExperimentsPage /> },
{
@@ -201,9 +201,9 @@ export const SettingsPanel: FC<SettingsPanelProps> = ({
<SettingsNavItem
icon={ReceiptTextIcon}
label="Instructions"
active={settingsSection === "instructions"}
to="/agents/settings/instructions"
state={location.state}
active={false}
to="/ai/settings/instructions"
trailingIcon={ArrowUpRightIcon}
/>
<SettingsNavItem
icon={FlaskConicalIcon}
@@ -46,13 +46,6 @@ describe("sidebarViewFromPath", () => {
});
});
it("returns the lifecycle admin settings section", () => {
expect(sidebarViewFromPath("/agents/settings/lifecycle")).toEqual({
panel: "settings-admin",
section: "lifecycle",
});
});
it("normalizes the admin index route to an undefined section", () => {
expect(sidebarViewFromPath("/agents/settings/admin")).toEqual({
panel: "settings-admin",
@@ -60,11 +53,23 @@ describe("sidebarViewFromPath", () => {
});
});
it("returns the instructions admin settings section", () => {
it("falls through moved admin sections to the user settings panel", () => {
expect(sidebarViewFromPath("/agents/settings/instructions")).toEqual({
panel: "settings-admin",
panel: "settings",
section: "instructions",
});
expect(sidebarViewFromPath("/agents/settings/lifecycle")).toEqual({
panel: "settings",
section: "lifecycle",
});
expect(sidebarViewFromPath("/agents/settings/models")).toEqual({
panel: "settings",
section: "models",
});
expect(sidebarViewFromPath("/agents/settings/templates")).toEqual({
panel: "settings",
section: "templates",
});
});
it("falls through unknown settings slugs to the user settings panel", () => {
@@ -6,13 +6,9 @@ type SidebarView =
const ADMIN_SETTINGS_SECTIONS = new Set([
"agents",
"templates",
"models",
"mcp-servers",
"spend",
"instructions",
"experiments",
"lifecycle",
]);
/**
@@ -1,132 +0,0 @@
import { useFormik } from "formik";
import type { FC } from "react";
import { useState } from "react";
import TextareaAutosize from "react-textarea-autosize";
import type * as TypesGen from "#/api/typesGenerated";
import { Alert, AlertDescription } from "#/components/Alert/Alert";
import { Button } from "#/components/Button/Button";
import { cn } from "#/utils/cn";
import { countInvisibleCharacters } from "#/utils/invisibleUnicode";
interface MutationCallbacks {
onSuccess?: () => void;
onError?: () => void;
}
interface PlanModeInstructionsSettingsProps {
planModeInstructionsData:
| TypesGen.ChatPlanModeInstructionsResponse
| undefined;
onSavePlanModeInstructions: (
req: TypesGen.UpdateChatPlanModeInstructionsRequest,
options?: MutationCallbacks,
) => void;
isSavePlanModeInstructionsError: boolean;
isAnyPromptSaving: boolean;
}
export const PlanModeInstructionsSettings: FC<
PlanModeInstructionsSettingsProps
> = ({
planModeInstructionsData,
onSavePlanModeInstructions,
isSavePlanModeInstructionsError,
isAnyPromptSaving,
}) => {
const [
isPlanModeInstructionsOverflowing,
setIsPlanModeInstructionsOverflowing,
] = useState(false);
const hasLoadedPlanModeInstructions = planModeInstructionsData !== undefined;
const form = useFormik({
enableReinitialize: true,
initialValues: {
plan_mode_instructions:
planModeInstructionsData?.plan_mode_instructions ?? "",
},
onSubmit: (values, { resetForm }) => {
onSavePlanModeInstructions(values, {
onSuccess: () => {
resetForm();
},
});
},
});
const planModeInvisibleCharCount = countInvisibleCharacters(
form.values.plan_mode_instructions,
);
const isPlanModeInstructionsDisabled =
isAnyPromptSaving || !hasLoadedPlanModeInstructions;
return (
<form className="space-y-2" onSubmit={form.handleSubmit}>
<div className="flex items-center gap-2">
<h3 className="m-0 text-[13px] font-semibold text-content-primary">
Plan mode instructions
</h3>
</div>
<p className="!mt-0.5 m-0 text-xs text-content-secondary">
Custom instructions applied when the agent enters planning mode. These
supplement the built-in planning behavior.
</p>
<TextareaAutosize
className={cn(
"max-h-[240px] w-full resize-none rounded-lg border border-border bg-surface-primary px-4 py-3 font-sans text-[13px] leading-relaxed text-content-primary placeholder:text-content-secondary focus:outline-none focus:ring-2 focus:ring-content-link/30",
isPlanModeInstructionsOverflowing &&
"overflow-y-auto [scrollbar-width:thin]",
)}
placeholder="Additional instructions for planning mode"
name="plan_mode_instructions"
value={form.values.plan_mode_instructions}
onChange={form.handleChange}
onHeightChange={(height) =>
setIsPlanModeInstructionsOverflowing(height >= 240)
}
disabled={isPlanModeInstructionsDisabled}
minRows={4}
maxRows={12}
/>
{planModeInvisibleCharCount > 0 && (
<Alert severity="warning">
<AlertDescription>
This text contains {planModeInvisibleCharCount} invisible Unicode{" "}
{planModeInvisibleCharCount !== 1 ? "characters" : "character"} that
could hide content. These will be stripped on save.
</AlertDescription>
</Alert>
)}
<div className="flex justify-end gap-2">
<Button
size="sm"
variant="outline"
type="button"
onClick={() => form.setFieldValue("plan_mode_instructions", "")}
disabled={
isPlanModeInstructionsDisabled ||
!form.values.plan_mode_instructions
}
>
Clear
</Button>
<Button
size="sm"
type="submit"
disabled={
isPlanModeInstructionsDisabled ||
!(form.dirty && hasLoadedPlanModeInstructions)
}
>
Save
</Button>
</div>
{isSavePlanModeInstructionsError && (
<p className="m-0 text-xs text-content-destructive">
Failed to save plan mode instructions.
</p>
)}
</form>
);
};
@@ -1,184 +0,0 @@
import { useFormik } from "formik";
import type { FC } from "react";
import { useState } from "react";
import TextareaAutosize from "react-textarea-autosize";
import type * as TypesGen from "#/api/typesGenerated";
import { Alert, AlertDescription } from "#/components/Alert/Alert";
import { Button } from "#/components/Button/Button";
import { Spinner } from "#/components/Spinner/Spinner";
import { Switch } from "#/components/Switch/Switch";
import {
TemporarySavedState,
useTemporarySavedState,
} from "#/components/TemporarySavedState/TemporarySavedState";
import { cn } from "#/utils/cn";
import { countInvisibleCharacters } from "#/utils/invisibleUnicode";
import { TextPreviewDialog } from "./TextPreviewDialog";
interface MutationCallbacks {
onSuccess?: () => void;
onError?: () => void;
}
interface SystemInstructionsSettingsProps {
systemPromptData: TypesGen.ChatSystemPromptResponse | undefined;
onSaveSystemPrompt: (
req: TypesGen.UpdateChatSystemPromptRequest,
options?: MutationCallbacks,
) => void;
isSavingSystemPrompt: boolean;
isSaveSystemPromptError: boolean;
isAnyPromptSaving: boolean;
}
export const SystemInstructionsSettings: FC<
SystemInstructionsSettingsProps
> = ({
systemPromptData,
onSaveSystemPrompt,
isSavingSystemPrompt,
isSaveSystemPromptError,
isAnyPromptSaving,
}) => {
const [showDefaultPromptPreview, setShowDefaultPromptPreview] =
useState(false);
const [isSystemPromptOverflowing, setIsSystemPromptOverflowing] =
useState(false);
const { isSavedVisible, showSavedState } = useTemporarySavedState();
const hasLoadedSystemPrompt = systemPromptData !== undefined;
const defaultSystemPrompt = systemPromptData?.default_system_prompt ?? "";
const form = useFormik({
enableReinitialize: true,
initialValues: {
system_prompt: systemPromptData?.system_prompt ?? "",
include_default_system_prompt:
systemPromptData?.include_default_system_prompt ?? false,
},
onSubmit: (values, { resetForm }) => {
onSaveSystemPrompt(values, {
onSuccess: () => {
showSavedState();
resetForm();
},
});
},
});
const systemInvisibleCharCount = countInvisibleCharacters(
form.values.system_prompt,
);
const isSystemPromptDisabled = isAnyPromptSaving || !hasLoadedSystemPrompt;
return (
<>
<form className="flex flex-col gap-2" onSubmit={form.handleSubmit}>
<div className="flex items-center gap-2">
<h3 className="m-0 text-sm font-semibold text-content-primary">
System instructions
</h3>
</div>
<div className="flex items-center justify-between gap-4">
<div className="flex min-w-0 items-center gap-2 text-xs font-medium text-content-primary">
<span>Include Coder Agents default system prompt</span>
<Button
size="xs"
variant="subtle"
type="button"
onClick={() => setShowDefaultPromptPreview(true)}
disabled={!hasLoadedSystemPrompt}
className="min-w-0 px-0 text-content-link hover:text-content-link"
>
Preview
</Button>
</div>
<Switch
checked={form.values.include_default_system_prompt}
onCheckedChange={(checked) =>
form.setFieldValue("include_default_system_prompt", checked)
}
aria-label="Include Coder Agents default system prompt"
disabled={isSystemPromptDisabled}
/>
</div>
<p className="!mt-0.5 m-0 text-xs text-content-secondary">
{form.values.include_default_system_prompt
? "The built-in Coder Agents prompt is prepended. Additional instructions below are appended."
: "Only the additional instructions below are used. When empty, no deployment-wide system prompt is sent."}
</p>
<TextareaAutosize
className={cn(
"max-h-[240px] w-full resize-none rounded-lg border border-border bg-surface-primary px-4 py-3 font-sans text-sm leading-relaxed text-content-primary placeholder:text-content-secondary focus:outline-none focus:ring-2 focus:ring-content-link/30",
isSystemPromptOverflowing &&
"overflow-y-auto [scrollbar-width:thin]",
)}
placeholder="Additional instructions for all users"
name="system_prompt"
value={form.values.system_prompt}
onChange={form.handleChange}
onHeightChange={(height) =>
setIsSystemPromptOverflowing(height >= 240)
}
disabled={isSystemPromptDisabled}
minRows={1}
/>
{systemInvisibleCharCount > 0 && (
<Alert severity="warning">
<AlertDescription>
This text contains {systemInvisibleCharCount} invisible Unicode{" "}
{systemInvisibleCharCount !== 1 ? "characters" : "character"} that
could hide content. These will be stripped on save.
</AlertDescription>
</Alert>
)}
<div className="mt-2 flex min-h-6 justify-end gap-2">
{(form.dirty || isSavedVisible || isSavingSystemPrompt) &&
(isSavedVisible ? (
<TemporarySavedState />
) : (
<>
<Button
size="xs"
variant="outline"
type="button"
onClick={() => form.setFieldValue("system_prompt", "")}
disabled={
isSystemPromptDisabled || !form.values.system_prompt
}
>
Clear
</Button>
<Button
size="xs"
type="submit"
disabled={
isSystemPromptDisabled ||
!(form.dirty && hasLoadedSystemPrompt)
}
>
{isSavingSystemPrompt && (
<Spinner loading className="h-4 w-4" />
)}
Save
</Button>
</>
))}
</div>
{isSaveSystemPromptError && (
<p className="m-0 text-xs text-content-destructive">
Failed to save system prompt.
</p>
)}
</form>
{showDefaultPromptPreview && (
<TextPreviewDialog
content={defaultSystemPrompt}
fileName="Default System Prompt"
onClose={() => setShowDefaultPromptPreview(false)}
/>
)}
</>
);
};
+9 -4
View File
@@ -371,9 +371,7 @@ const AgentSettingsGeneralPage = lazy(
const AgentSettingsCompactionPage = lazy(
() => import("./pages/AgentsPage/AgentSettingsCompactionPage"),
);
const AgentSettingsInstructionsPage = lazy(
() => import("./pages/AgentsPage/AgentSettingsInstructionsPage"),
);
const AgentSettingsExperimentsPage = lazy(
() => import("./pages/AgentsPage/AgentSettingsExperimentsPage"),
);
@@ -448,6 +446,9 @@ const AISettingsGatewayKeysPage = lazy(
const AISettingsModelsPage = lazy(
() => import("./pages/AISettingsPage/ModelsPage/ModelsPage"),
);
const AISettingsInstructionsPage = lazy(
() => import("./pages/AISettingsPage/InstructionsPage/InstructionsPage"),
);
const AISettingsTemplatesPage = lazy(
() => import("./pages/AISettingsPage/TemplatesPage/TemplatesPage"),
);
@@ -755,6 +756,10 @@ export const router = createBrowserRouter(
/>
<Route index element={<AISettingsIndexPage />} />
<Route path="models" element={<AISettingsModelsPage />} />
<Route
path="instructions"
element={<AISettingsInstructionsPage />}
/>
<Route path="lifecycle" element={<AISettingsLifecyclePage />} />
<Route path="templates" element={<AISettingsTemplatesPage />} />
<Route path="models/add" element={<AISettingsAddModelPage />} />
@@ -829,7 +834,7 @@ export const router = createBrowserRouter(
/>
<Route
path="instructions"
element={<AgentSettingsInstructionsPage />}
element={<Navigate to="/ai/settings/instructions" replace />}
/>
<Route
path="experiments"