Skip to content

Commit e89dcd7

Browse files
committed
fix: refresh skills settings after mutations
1 parent 164f44f commit e89dcd7

4 files changed

Lines changed: 97 additions & 6 deletions

File tree

packages/web/src/ee/features/chat/skills/actions.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ vi.mock("@/features/git", () => ({
2828
resolveFileBlobShaForRepo: vi.fn(),
2929
}));
3030

31+
vi.mock("next/cache", () => ({
32+
refresh: vi.fn(),
33+
revalidatePath: vi.fn(),
34+
}));
35+
3136
const {
3237
adoptSharedSkill,
3338
createSharedAgentSkill,
@@ -48,6 +53,7 @@ const {
4853
} = await import("./actions");
4954

5055
const gitMock = await import("@/features/git");
56+
const nextCache = await import("next/cache");
5157

5258
function createPrismaMock() {
5359
const prisma = {
@@ -1137,6 +1143,9 @@ describe("setSharedSkillFlag", () => {
11371143
id: "skill-1",
11381144
autoEnrolled: true,
11391145
});
1146+
expect(nextCache.revalidatePath).toHaveBeenCalledWith("/settings/skills");
1147+
expect(nextCache.revalidatePath).toHaveBeenCalledWith("/settings/workspaceAskAgent");
1148+
expect(nextCache.refresh).toHaveBeenCalled();
11401149
expect(result).not.toHaveProperty("isAdopted");
11411150
expect(result).not.toHaveProperty("isVisibleToUser");
11421151
expect(result).not.toHaveProperty("isCreatedByUser");
@@ -1158,6 +1167,7 @@ describe("setSharedSkillFlag", () => {
11581167
// The owner gate returns before its callback runs, so the skill is never looked up or updated.
11591168
expect(prisma.agentSkill.findFirst).not.toHaveBeenCalled();
11601169
expect(prisma.agentSkill.update).not.toHaveBeenCalled();
1170+
expect(nextCache.refresh).not.toHaveBeenCalled();
11611171
});
11621172

11631173
test("rejects updates with no flag", async () => {

packages/web/src/ee/features/chat/skills/actions.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { withAuth } from "@/middleware/withAuth";
1212
import { withMinimumOrgRole } from "@/middleware/withMinimumOrgRole";
1313
import { OrgRole, Prisma, sharedAgentSkillAuthScope, sharedAgentSkillScope, sharedAgentSkillVisibleToUserWhere, personalAgentSkillAuthScope, personalAgentSkillScope, type AgentSkill, type Org, type PrismaClient } from "@sourcebot/db";
1414
import { StatusCodes } from "http-status-codes";
15+
import { refresh, revalidatePath } from "next/cache";
1516
import { z } from "zod";
1617
import {
1718
agentSkillInputSchema,
@@ -66,6 +67,12 @@ const skillSourceInvalid = (): ServiceError => ({
6667
message: "The source file is no longer a valid skill.",
6768
});
6869

70+
const refreshSkillSettingsViews = () => {
71+
revalidatePath("/settings/skills");
72+
revalidatePath("/settings/workspaceAskAgent");
73+
refresh();
74+
};
75+
6976
const sharedCatalogSkillSelect = (userId: string, orgId: number) => ({
7077
id: true,
7178
visibility: true,
@@ -421,6 +428,7 @@ export const createPersonalAgentSkill = async (
421428
},
422429
});
423430

431+
refreshSkillSettingsViews();
424432
return toAgentSkillListItem(skill);
425433
} catch (error) {
426434
if (isUniqueConstraintError(error)) {
@@ -484,6 +492,7 @@ export const updatePersonalAgentSkill = async (
484492
},
485493
});
486494

495+
refreshSkillSettingsViews();
487496
return toAgentSkillListItem(skill);
488497
} catch (error) {
489498
if (isUniqueConstraintError(error)) {
@@ -655,6 +664,7 @@ export const updatePersonalAgentSkillFromSource = async (
655664
},
656665
});
657666

667+
refreshSkillSettingsViews();
658668
return toAgentSkillListItem(updated);
659669
}));
660670

@@ -720,6 +730,7 @@ export const updateSharedAgentSkillFromSource = async (
720730
select: sharedCatalogSkillSelect(user.id, org.id),
721731
});
722732

733+
refreshSkillSettingsViews();
723734
return toSharedAgentSkillCatalogItem(updated, user.id);
724735
}));
725736

@@ -743,6 +754,7 @@ export const deletePersonalAgentSkill = async (
743754
return skillNotFound();
744755
}
745756

757+
refreshSkillSettingsViews();
746758
return { success: true };
747759
}));
748760

@@ -812,6 +824,7 @@ export const publishPersonalAgentSkillToShared = async (
812824
return selectedSkill;
813825
});
814826

827+
refreshSkillSettingsViews();
815828
return toSharedAgentSkillCatalogItem(sharedSkill, user.id);
816829
} catch (error) {
817830
if (isUniqueConstraintError(error)) {
@@ -908,6 +921,7 @@ export const makeSharedAgentSkillPersonal = async (
908921
return createdSkill;
909922
});
910923

924+
refreshSkillSettingsViews();
911925
return toAgentSkillListItem(personalSkill);
912926
} catch (error) {
913927
if (isUniqueConstraintError(error)) {
@@ -1016,6 +1030,7 @@ export const createSharedAgentSkill = async (
10161030
});
10171031
});
10181032

1033+
refreshSkillSettingsViews();
10191034
return toAgentSkillListItem(skill);
10201035
} catch (error) {
10211036
if (isUniqueConstraintError(error)) {
@@ -1078,6 +1093,7 @@ export const updateSharedAgentSkill = async (
10781093
},
10791094
});
10801095

1096+
refreshSkillSettingsViews();
10811097
return toAgentSkillListItem(skill);
10821098
} catch (error) {
10831099
if (isUniqueConstraintError(error)) {
@@ -1115,6 +1131,7 @@ export const deleteSharedAgentSkill = async (
11151131
where: { id: existingSkill.id },
11161132
});
11171133

1134+
refreshSkillSettingsViews();
11181135
return { success: true };
11191136
}));
11201137

@@ -1159,6 +1176,7 @@ export const setSharedSkillFlag = async (
11591176
select: sharedManagementSkillSelect,
11601177
});
11611178

1179+
refreshSkillSettingsViews();
11621180
return toSharedAgentSkillManagementItem(skill);
11631181
});
11641182
}));
@@ -1213,6 +1231,7 @@ export const adoptSharedSkill = async (
12131231
},
12141232
});
12151233

1234+
refreshSkillSettingsViews();
12161235
return { success: true };
12171236
}));
12181237

@@ -1247,5 +1266,6 @@ export const unadoptSharedSkill = async (
12471266
skill,
12481267
});
12491268

1269+
refreshSkillSettingsViews();
12501270
return { success: true };
12511271
}));

packages/web/src/ee/features/chat/skills/components/skillsPage.test.tsx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,29 @@ describe('SkillsPage', () => {
281281
expect(screen.getByRole('switch', { name: 'Enable Deploy Checklist', checked: true })).toBeTruthy();
282282
});
283283

284+
test('keeps list skill names readable when status badges are present', () => {
285+
const longBadgedSkill: SharedAgentSkillCatalogItem = {
286+
...sharedSyncedSkill,
287+
id: 'long-badged-skill',
288+
name: 'Code Contribution Guidelines',
289+
slug: 'contributing',
290+
autoEnrolled: true,
291+
isCreatedByUser: false,
292+
};
293+
294+
renderSkillsPage({ sharedSkills: [longBadgedSkill] });
295+
296+
const rowButton = screen.getByRole('button', { name: /Code Contribution Guidelines/ });
297+
const name = within(rowButton).getByText('Code Contribution Guidelines');
298+
const command = within(rowButton).getByText('/contributing');
299+
const syncedBadge = within(rowButton).getByText('Synced');
300+
expect(name.className).not.toContain('truncate');
301+
expect(name.className).toContain('break-words');
302+
expect(syncedBadge.parentElement?.className).toContain('mt-1');
303+
expect(syncedBadge.parentElement).not.toBe(command.parentElement);
304+
expect(within(rowButton).getByText('Auto')).toBeTruthy();
305+
});
306+
284307
test('shows a repo-synced skill as read-only and updates it from source', async () => {
285308
vi.mocked(clientApi.getSkillSourceStatus).mockResolvedValue({ status: 'update_available' });
286309
vi.mocked(skillActions.updatePersonalAgentSkillFromSource).mockResolvedValue({
@@ -385,6 +408,30 @@ describe('SkillsPage', () => {
385408
expect(screen.getByTestId('markdown-preview').textContent).toContain('Say hi.');
386409
});
387410

411+
test('updates the list when refreshed server props include a new shared skill', async () => {
412+
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
413+
const renderPage = (sharedSkills: SharedAgentSkillCatalogItem[]) => (
414+
<QueryClientProvider client={queryClient}>
415+
<TooltipProvider>
416+
<SkillsPage
417+
initialPersonalSkills={[]}
418+
initialSharedSkills={sharedSkills}
419+
currentUserEmail="jack@sourcebot.dev"
420+
isOwner={true}
421+
permissionSyncEnabled={true}
422+
/>
423+
</TooltipProvider>
424+
</QueryClientProvider>
425+
);
426+
427+
const { rerender } = render(renderPage([]));
428+
expect(screen.getByText('No shared skills yet.')).toBeTruthy();
429+
430+
rerender(renderPage([sharedSkill]));
431+
432+
await waitFor(() => expect(screen.getByText('Deploy Checklist')).toBeTruthy());
433+
});
434+
388435
test('lets an owner jump to workspace settings for a shared skill', async () => {
389436
renderSkillsPage({ sharedSkills: [sharedSkill], isOwner: true });
390437

packages/web/src/ee/features/chat/skills/components/skillsPage.tsx

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -191,15 +191,21 @@ function SkillListRow({ name, slug, isActive, badge, enabled, togglePending, onT
191191
<button
192192
type="button"
193193
onClick={onSelect}
194-
className="flex min-w-0 flex-1 items-center gap-3 rounded-lg px-3 py-2.5 text-left"
194+
className="flex min-w-0 flex-1 items-start gap-3 rounded-lg px-3 py-2.5 text-left"
195195
>
196196
<SkillAvatar name={name} size="sm" />
197-
<div className="min-w-0 flex-1">
198-
<div className="flex items-center gap-2">
199-
<p className="truncate text-sm font-medium text-foreground">{name}</p>
200-
{badge}
197+
<div className="min-w-0 flex-1 space-y-1">
198+
<p className="max-w-full whitespace-normal break-words text-sm font-medium leading-5 text-foreground">
199+
{name}
200+
</p>
201+
<div className="min-w-0">
202+
<p className="min-w-0 break-all font-mono text-xs text-muted-foreground">/{slug}</p>
203+
{badge && (
204+
<div className="mt-1 flex min-w-0 flex-wrap items-center gap-1.5">
205+
{badge}
206+
</div>
207+
)}
201208
</div>
202-
<p className="truncate font-mono text-xs text-muted-foreground">/{slug}</p>
203209
</div>
204210
</button>
205211
{onToggleEnabled && (
@@ -247,6 +253,14 @@ export function SkillsPage({
247253
const [sharedSkills, setSharedSkills] = useState(() => sortSharedAgentSkillCatalogItems(initialSharedSkills));
248254
const [searchQuery, setSearchQuery] = useState("");
249255

256+
useEffect(() => {
257+
setPersonalSkills(sortAgentSkillListItems(initialPersonalSkills));
258+
}, [initialPersonalSkills]);
259+
260+
useEffect(() => {
261+
setSharedSkills(sortSharedAgentSkillCatalogItems(initialSharedSkills));
262+
}, [initialSharedSkills]);
263+
250264
const [selectedId, setSelectedId] = useState<string | null>(() => {
251265
// Honor a deep-linked selection, but only when it matches a skill visible
252266
// on this page; otherwise fall back to the first skill.

0 commit comments

Comments
 (0)