Skip to content

Commit ea8cc37

Browse files
committed
fix: stay in editing pane for skill when editing name
1 parent 7d69e14 commit ea8cc37

5 files changed

Lines changed: 107 additions & 20 deletions

File tree

ui/goose2/src/features/skills/ui/SkillEditor.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,26 @@ import {
1111
DialogHeader,
1212
DialogTitle,
1313
} from "@/shared/ui/dialog";
14-
import { createSkill, updateSkill, type EditingSkill } from "../api/skills";
14+
import {
15+
createSkill,
16+
updateSkill,
17+
type EditingSkill,
18+
type SkillInfo,
19+
} from "../api/skills";
1520
import { formatSkillName, isValidSkillName } from "../lib/skillsHelpers";
1621
import { getRenamedSkillFileLocation } from "../lib/skillsPath";
1722

1823
interface SkillEditorProps {
1924
isOpen: boolean;
2025
onClose: () => void;
21-
onCreated?: () => void;
26+
onSaved?: (savedSkill?: SkillInfo) => void | Promise<void>;
2227
editingSkill?: EditingSkill;
2328
}
2429

2530
export function SkillEditor({
2631
isOpen,
2732
onClose,
28-
onCreated,
33+
onSaved,
2934
editingSkill,
3035
}: SkillEditorProps) {
3136
const { t } = useTranslation(["skills", "common"]);
@@ -74,8 +79,9 @@ export function SkillEditor({
7479
setSaving(true);
7580
setError(null);
7681
try {
82+
let savedSkill: SkillInfo | undefined;
7783
if (isEditing) {
78-
await updateSkill(
84+
savedSkill = await updateSkill(
7985
editingSkill.path,
8086
name,
8187
description.trim(),
@@ -87,7 +93,7 @@ export function SkillEditor({
8793
setName("");
8894
setDescription("");
8995
setInstructions("");
90-
onCreated?.();
96+
await onSaved?.(savedSkill);
9197
onClose();
9298
} catch (err) {
9399
setError(String(err));

ui/goose2/src/features/skills/ui/SkillsDialogs.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import type { EditingSkill, SkillInfo } from "../api/skills";
1616
interface SkillsDialogsProps {
1717
dialogOpen: boolean;
1818
onDialogClose: () => void;
19-
onCreated: () => void | Promise<void>;
19+
onSaved: (savedSkill?: SkillInfo) => void | Promise<void>;
2020
editingSkill?: EditingSkill;
2121
deletingSkill: SkillInfo | null;
2222
onDeletingSkillChange: (skill: SkillInfo | null) => void;
@@ -26,7 +26,7 @@ interface SkillsDialogsProps {
2626
export function SkillsDialogs({
2727
dialogOpen,
2828
onDialogClose,
29-
onCreated,
29+
onSaved,
3030
editingSkill,
3131
deletingSkill,
3232
onDeletingSkillChange,
@@ -39,7 +39,7 @@ export function SkillsDialogs({
3939
<SkillEditor
4040
isOpen={dialogOpen}
4141
onClose={onDialogClose}
42-
onCreated={onCreated}
42+
onSaved={onSaved}
4343
editingSkill={editingSkill}
4444
/>
4545

ui/goose2/src/features/skills/ui/SkillsView.tsx

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export function SkillsView({ onStartChatWithSkill }: SkillsViewProps) {
5555
const [expandedSectionIds, setExpandedSectionIds] = useState<string[]>([]);
5656
const loadRequestIdRef = useRef(0);
5757

58-
const loadSkills = useCallback(async () => {
58+
const loadSkills = useCallback(async (): Promise<SkillViewInfo[]> => {
5959
const requestId = loadRequestIdRef.current + 1;
6060
loadRequestIdRef.current = requestId;
6161
setLoading(true);
@@ -64,16 +64,19 @@ export function SkillsView({ onStartChatWithSkill }: SkillsViewProps) {
6464
const projectDirs = projects.flatMap((project) => project.workingDirs);
6565
const result = await listSkills(projectDirs);
6666
if (loadRequestIdRef.current !== requestId) {
67-
return;
67+
return [];
6868
}
69-
setSkills(
70-
withInferredSkillCategories(hydrateProjectNames(result, projects)),
69+
const nextSkills = withInferredSkillCategories(
70+
hydrateProjectNames(result, projects),
7171
);
72+
setSkills(nextSkills);
73+
return nextSkills;
7274
} catch {
7375
if (loadRequestIdRef.current === requestId) {
7476
setSkills([]);
7577
toast.error(t("view.loadError"));
7678
}
79+
return [];
7780
} finally {
7881
if (loadRequestIdRef.current === requestId) {
7982
setLoading(false);
@@ -190,14 +193,31 @@ export function SkillsView({ onStartChatWithSkill }: SkillsViewProps) {
190193
setDialogOpen(true);
191194
};
192195

196+
const handleSkillSaved = useCallback(
197+
async (savedSkill?: SkillInfo) => {
198+
const refreshedSkills = await loadSkills();
199+
if (
200+
savedSkill &&
201+
refreshedSkills.some((skill) => skill.id === savedSkill.id)
202+
) {
203+
setActiveSkillId(savedSkill.id);
204+
}
205+
},
206+
[loadSkills],
207+
);
208+
209+
const refreshSkills = useCallback(async () => {
210+
await loadSkills();
211+
}, [loadSkills]);
212+
193213
const {
194214
fileInputRef,
195215
isDragOver,
196216
dropHandlers,
197217
handleFileChange,
198218
openFilePicker,
199219
handleExport,
200-
} = useSkillImportExport(loadSkills);
220+
} = useSkillImportExport(refreshSkills);
201221

202222
const handleSelectSkill = (skill: SkillViewInfo) => {
203223
setActiveSkillId(skill.id);
@@ -207,7 +227,7 @@ export function SkillsView({ onStartChatWithSkill }: SkillsViewProps) {
207227
<SkillsDialogs
208228
dialogOpen={dialogOpen}
209229
onDialogClose={handleDialogClose}
210-
onCreated={loadSkills}
230+
onSaved={handleSkillSaved}
211231
editingSkill={editingSkill}
212232
deletingSkill={deletingSkill}
213233
onDeletingSkillChange={setDeletingSkill}

ui/goose2/src/features/skills/ui/__tests__/SkillEditor.test.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const { createSkill, updateSkill } = await import("../../api/skills");
2020
const defaultProps = {
2121
isOpen: true,
2222
onClose: vi.fn(),
23-
onCreated: vi.fn(),
23+
onSaved: vi.fn(),
2424
};
2525

2626
describe("SkillEditor", () => {
@@ -308,10 +308,10 @@ describe("SkillEditor", () => {
308308
);
309309
});
310310

311-
it("calls onCreated callback after successful save", async () => {
311+
it("calls onSaved callback after successful save", async () => {
312312
const user = userEvent.setup();
313-
const onCreated = vi.fn();
314-
render(<SkillEditor {...defaultProps} onCreated={onCreated} />);
313+
const onSaved = vi.fn();
314+
render(<SkillEditor {...defaultProps} onSaved={onSaved} />);
315315

316316
await user.type(screen.getByPlaceholderText("my-skill-name"), "my-skill");
317317
await user.type(
@@ -320,7 +320,7 @@ describe("SkillEditor", () => {
320320
);
321321
await user.click(screen.getByRole("button", { name: /create skill/i }));
322322

323-
expect(onCreated).toHaveBeenCalled();
323+
expect(onSaved).toHaveBeenCalled();
324324
});
325325

326326
it("clears fields after save", async () => {

ui/goose2/src/features/skills/ui/__tests__/SkillsView.test.tsx

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,18 @@ const mockSkills: SkillInfo[] = [
6262

6363
vi.mock("../../api/skills", () => ({
6464
listSkills: vi.fn().mockResolvedValue([]),
65+
createSkill: vi.fn().mockResolvedValue(undefined),
66+
updateSkill: vi.fn().mockResolvedValue({
67+
id: "global:/path/renamed-review",
68+
name: "renamed-review",
69+
description: "Reviews code",
70+
instructions: "Review the code...",
71+
path: "/path/renamed-review",
72+
fileLocation: "/path/renamed-review/SKILL.md",
73+
sourceKind: "global",
74+
sourceLabel: "Personal",
75+
projectLinks: [],
76+
}),
6577
deleteSkill: vi.fn().mockResolvedValue(undefined),
6678
exportSkill: vi
6779
.fn()
@@ -75,11 +87,12 @@ vi.mock("@/features/projects/stores/projectStore", () => ({
7587
) => selector({ projects: mockProjects }),
7688
}));
7789

78-
const { listSkills, deleteSkill } = (await import(
90+
const { listSkills, deleteSkill, updateSkill } = (await import(
7991
"../../api/skills"
8092
)) as unknown as {
8193
listSkills: ReturnType<typeof vi.fn>;
8294
deleteSkill: ReturnType<typeof vi.fn>;
95+
updateSkill: ReturnType<typeof vi.fn>;
8396
};
8497

8598
beforeEach(() => {
@@ -288,6 +301,54 @@ describe("SkillsView", () => {
288301
expect(screen.queryByText("code-review")).not.toBeInTheDocument();
289302
});
290303

304+
it("stays on the detail page after renaming a skill", async () => {
305+
const renamedSkill: SkillInfo = {
306+
...mockSkills[1],
307+
id: "global:/path/renamed-review",
308+
name: "renamed-review",
309+
path: "/path/renamed-review",
310+
fileLocation: "/path/renamed-review/SKILL.md",
311+
};
312+
listSkills
313+
.mockResolvedValueOnce(mockSkills)
314+
.mockResolvedValueOnce([mockSkills[0], renamedSkill, mockSkills[2]]);
315+
updateSkill.mockResolvedValueOnce(renamedSkill);
316+
const user = userEvent.setup();
317+
318+
render(<SkillsView />);
319+
await screen.findByText("code-review");
320+
321+
await user.click(
322+
screen.getByRole("button", { name: "Open code-review details" }),
323+
);
324+
await user.click(screen.getByRole("button", { name: "Edit" }));
325+
326+
const nameInput = screen.getByPlaceholderText("my-skill-name");
327+
await user.clear(nameInput);
328+
await user.type(nameInput, "renamed-review");
329+
await user.click(screen.getByRole("button", { name: /save changes/i }));
330+
331+
await waitFor(() => {
332+
expect(updateSkill).toHaveBeenCalledWith(
333+
"/path/code-review",
334+
"renamed-review",
335+
"Reviews code",
336+
"Review the code...",
337+
);
338+
});
339+
await waitFor(() => {
340+
expect(
341+
screen.getByRole("button", { name: "Back to skills" }),
342+
).toBeInTheDocument();
343+
expect(
344+
screen.getByRole("heading", { name: "renamed-review" }),
345+
).toBeInTheDocument();
346+
});
347+
expect(
348+
screen.queryByPlaceholderText("my-skill-name"),
349+
).not.toBeInTheDocument();
350+
});
351+
291352
it("filters skills by search text", async () => {
292353
listSkills.mockResolvedValue(mockSkills);
293354
const user = userEvent.setup();

0 commit comments

Comments
 (0)