+ )}
{shouldShowInlineArchivingStatus ? (
+
+
+
+ >
+ );
+}
+
+function RegisterAction(props: { action: CommandAction }) {
+ const { registerSource } = useCommandRegistry();
+
+ React.useEffect(() => registerSource(() => [props.action]), [props.action, registerSource]);
+
+ return null;
+}
+
+function renderPalette(action: CommandAction) {
+ return render(
+
+
+
+
+
+ );
+}
+
+describe("CommandPalette inline goal prompts", () => {
+ let cleanupDom: (() => void) | null = null;
+
+ beforeEach(() => {
+ cleanupDom = installDom();
+ });
+
+ afterEach(() => {
+ cleanup();
+ cleanupDom?.();
+ cleanupDom = null;
+ });
+
+ test("Goal: Set objective traps focus and restores it on dismissal", async () => {
+ const action: CommandAction = {
+ id: "goal:set-objective",
+ title: "Goal: Set objective",
+ section: "Goals",
+ run: () => undefined,
+ prompt: {
+ fields: [
+ {
+ type: "text",
+ name: "objective",
+ label: "Goal objective",
+ placeholder: "Describe the goal…",
+ },
+ ],
+ onSubmit: mock(),
+ },
+ };
+ const view = renderPalette(action);
+
+ const opener = view.getByRole("button", { name: "Open palette" });
+ opener.focus();
+ fireEvent.click(opener);
+ fireEvent.click(await view.findByText("Goal: Set objective"));
+
+ const objectiveInput = await view.findByRole("combobox", { name: "Goal objective" });
+ await waitFor(() => expect(document.activeElement).toBe(objectiveInput));
+
+ fireEvent.keyDown(objectiveInput, { key: "Tab" });
+ fireEvent.keyDown(objectiveInput, { key: "Tab" });
+ fireEvent.keyDown(objectiveInput, { key: "Tab", shiftKey: true });
+ expect(objectiveInput.closest('[cmdk-root=""]')?.contains(document.activeElement)).toBe(true);
+
+ fireEvent.keyDown(objectiveInput, { key: "Escape" });
+ await waitFor(() => expect(view.queryByText("Goal: Set objective")).toBeNull());
+ expect(document.activeElement).toBe(opener);
+ });
+
+ test("Goal: Mark complete traps focus and restores it on dismissal", async () => {
+ const action: CommandAction = {
+ id: "goal:mark-complete",
+ title: "Goal: Mark complete",
+ section: "Goals",
+ run: () => undefined,
+ prompt: {
+ fields: [
+ {
+ type: "text",
+ name: "summary",
+ label: "Completion summary",
+ placeholder: "Summarize the completed goal…",
+ },
+ ],
+ onSubmit: mock(),
+ },
+ };
+ const view = renderPalette(action);
+
+ const opener = view.getByRole("button", { name: "Open palette" });
+ opener.focus();
+ fireEvent.click(opener);
+ fireEvent.click(await view.findByText("Goal: Mark complete"));
+
+ const summaryInput = await view.findByRole("combobox", { name: "Completion summary" });
+ await waitFor(() => expect(document.activeElement).toBe(summaryInput));
+
+ fireEvent.keyDown(summaryInput, { key: "Tab" });
+ fireEvent.keyDown(summaryInput, { key: "Tab", shiftKey: true });
+ expect(summaryInput.closest('[cmdk-root=""]')?.contains(document.activeElement)).toBe(true);
+
+ fireEvent.keyDown(summaryInput, { key: "Escape" });
+ await waitFor(() => expect(view.queryByText("Goal: Mark complete")).toBeNull());
+ expect(document.activeElement).toBe(opener);
+ });
+});
diff --git a/src/browser/components/CommandPalette/CommandPalette.tsx b/src/browser/components/CommandPalette/CommandPalette.tsx
index 1dcc382f17..0c64d51778 100644
--- a/src/browser/components/CommandPalette/CommandPalette.tsx
+++ b/src/browser/components/CommandPalette/CommandPalette.tsx
@@ -25,6 +25,18 @@ interface CommandPaletteProps {
type PromptDef = NonNullable
>;
type PromptField = PromptDef["fields"][number];
+function getCommandInputPlaceholder(field: PromptField | null): string {
+ if (!field) {
+ return "Switch workspaces or type > for all commands, / for slash commands…";
+ }
+
+ if (field.type === "text") {
+ return field.placeholder ?? "Type value…";
+ }
+
+ return field.placeholder ?? "Search options…";
+}
+
interface PromptPaletteItem {
id: string;
title: string;
@@ -56,6 +68,8 @@ export const CommandPalette: React.FC = ({ getSlashContext
const [agentSkills, setAgentSkills] = useState([]);
const agentSkillsCacheRef = useRef