fix: discover repository-local .agents skills in Settings
Skills listing ignored the active project when the OpenCode client directory was unset, so project `.agents/skills` files were created but never shown. Prefer the active project path (matching Commands/Agents) and soft-fall back to it on skill API routes when directory is omitted. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
This commit is contained in:
co-authored by
Serhii Dziupin
parent
7afec99f80
commit
049600df72
@@ -10,23 +10,29 @@ import {
|
||||
import { createDeferredSafeJSONStorage } from "./utils/safeStorage";
|
||||
import { runtimeFetch } from "@/lib/runtime-fetch";
|
||||
import { runBackgroundNetworkTask } from "@/lib/background-network";
|
||||
import { useProjectsStore } from "@/stores/useProjectsStore";
|
||||
|
||||
import { opencodeClient } from '@/lib/opencode/client';
|
||||
|
||||
const getCurrentDirectory = (): string | null => {
|
||||
const opencodeDirectory = opencodeClient.getDirectory();
|
||||
if (typeof opencodeDirectory === 'string' && opencodeDirectory.trim().length > 0) {
|
||||
return opencodeDirectory;
|
||||
}
|
||||
|
||||
// Prefer the active project path so Settings/Skills discovery matches the
|
||||
// project selector (and Commands/Agents). Falling back only to the session
|
||||
// directory misses repository-local `.agents/skills` when the client directory
|
||||
// is unset or points elsewhere while an active project exists.
|
||||
const getRequestDirectory = (): string | null => {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const store = (window as any).__zustand_directory_store__;
|
||||
if (store) {
|
||||
return store.getState().currentDirectory;
|
||||
const projectsStore = useProjectsStore.getState();
|
||||
const activeProject = projectsStore.getActiveProject?.();
|
||||
|
||||
if (activeProject?.path?.trim()) {
|
||||
return activeProject.path.trim();
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
|
||||
const clientDir = opencodeClient.getDirectory();
|
||||
if (clientDir?.trim()) {
|
||||
return clientDir.trim();
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[SkillsStore] Error resolving config directory:', err);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -169,7 +175,7 @@ const getSkillsCacheKey = (directory: string | null): string => {
|
||||
return directory?.trim() || DEFAULT_SKILLS_CACHE_KEY;
|
||||
};
|
||||
|
||||
export const invalidateSkillsLoadCache = (directory: string | null = getCurrentDirectory()) => {
|
||||
export const invalidateSkillsLoadCache = (directory: string | null = getRequestDirectory()) => {
|
||||
skillsLastLoadedAt.delete(getSkillsCacheKey(directory));
|
||||
};
|
||||
|
||||
@@ -198,8 +204,8 @@ export const useSkillsStore = create<SkillsStore>()(
|
||||
},
|
||||
|
||||
loadSkills: async () => {
|
||||
const currentDirectory = getCurrentDirectory();
|
||||
const cacheKey = getSkillsCacheKey(currentDirectory);
|
||||
const directory = getRequestDirectory();
|
||||
const cacheKey = getSkillsCacheKey(directory);
|
||||
const now = Date.now();
|
||||
const loadedAt = skillsLastLoadedAt.get(cacheKey) ?? 0;
|
||||
const hasCachedSkills = get().skills.length > 0;
|
||||
@@ -220,9 +226,12 @@ export const useSkillsStore = create<SkillsStore>()(
|
||||
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
try {
|
||||
const queryParams = currentDirectory ? `?directory=${encodeURIComponent(currentDirectory)}` : '';
|
||||
const queryParams = directory ? `?directory=${encodeURIComponent(directory)}` : '';
|
||||
|
||||
const response = await runBackgroundNetworkTask(() => runtimeFetch(`/api/config/skills${queryParams}`, { priority: 'low' }));
|
||||
const response = await runBackgroundNetworkTask(() => runtimeFetch(`/api/config/skills${queryParams}`, {
|
||||
priority: 'low',
|
||||
headers: directory ? { 'x-opencode-directory': directory } : undefined,
|
||||
}));
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to list skills: ${response.status}`);
|
||||
}
|
||||
@@ -263,10 +272,12 @@ export const useSkillsStore = create<SkillsStore>()(
|
||||
|
||||
getSkillDetail: async (name: string) => {
|
||||
try {
|
||||
const currentDirectory = getCurrentDirectory();
|
||||
const queryParams = currentDirectory ? `?directory=${encodeURIComponent(currentDirectory)}` : '';
|
||||
const directory = getRequestDirectory();
|
||||
const queryParams = directory ? `?directory=${encodeURIComponent(directory)}` : '';
|
||||
|
||||
const response = await runtimeFetch(`/api/config/skills/${encodeURIComponent(name)}${queryParams}`);
|
||||
const response = await runtimeFetch(`/api/config/skills/${encodeURIComponent(name)}${queryParams}`, {
|
||||
headers: directory ? { 'x-opencode-directory': directory } : undefined,
|
||||
});
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
@@ -291,12 +302,15 @@ export const useSkillsStore = create<SkillsStore>()(
|
||||
if (config.source) skillConfig.source = config.source;
|
||||
if (config.supportingFiles) skillConfig.supportingFiles = config.supportingFiles;
|
||||
|
||||
const currentDirectory = getCurrentDirectory();
|
||||
const queryParams = currentDirectory ? `?directory=${encodeURIComponent(currentDirectory)}` : '';
|
||||
const directory = getRequestDirectory();
|
||||
const queryParams = directory ? `?directory=${encodeURIComponent(directory)}` : '';
|
||||
|
||||
const response = await runtimeFetch(`/api/config/skills/${encodeURIComponent(config.name)}${queryParams}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(directory ? { 'x-opencode-directory': directory } : {}),
|
||||
},
|
||||
body: JSON.stringify(skillConfig)
|
||||
});
|
||||
|
||||
@@ -307,7 +321,7 @@ export const useSkillsStore = create<SkillsStore>()(
|
||||
}
|
||||
|
||||
const needsReload = payload?.requiresReload ?? false;
|
||||
invalidateSkillsLoadCache(currentDirectory);
|
||||
invalidateSkillsLoadCache(directory);
|
||||
if (needsReload) {
|
||||
requiresReload = true;
|
||||
await refreshSkillsAfterOpenCodeRestart({
|
||||
@@ -342,12 +356,15 @@ export const useSkillsStore = create<SkillsStore>()(
|
||||
if (config.supportingFiles !== undefined) skillConfig.supportingFiles = config.supportingFiles;
|
||||
if (config.targetPath !== undefined) skillConfig.targetPath = config.targetPath;
|
||||
|
||||
const currentDirectory = getCurrentDirectory();
|
||||
const queryParams = currentDirectory ? `?directory=${encodeURIComponent(currentDirectory)}` : '';
|
||||
const directory = getRequestDirectory();
|
||||
const queryParams = directory ? `?directory=${encodeURIComponent(directory)}` : '';
|
||||
|
||||
const response = await runtimeFetch(`/api/config/skills/${encodeURIComponent(name)}${queryParams}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(directory ? { 'x-opencode-directory': directory } : {}),
|
||||
},
|
||||
body: JSON.stringify(skillConfig)
|
||||
});
|
||||
|
||||
@@ -358,7 +375,7 @@ export const useSkillsStore = create<SkillsStore>()(
|
||||
}
|
||||
|
||||
const needsReload = payload?.requiresReload ?? false;
|
||||
invalidateSkillsLoadCache(currentDirectory);
|
||||
invalidateSkillsLoadCache(directory);
|
||||
if (needsReload) {
|
||||
requiresReload = true;
|
||||
await refreshSkillsAfterOpenCodeRestart({
|
||||
@@ -386,11 +403,12 @@ export const useSkillsStore = create<SkillsStore>()(
|
||||
startConfigUpdate("Deleting skill...");
|
||||
let requiresReload = false;
|
||||
try {
|
||||
const currentDirectory = getCurrentDirectory();
|
||||
const queryParams = currentDirectory ? `?directory=${encodeURIComponent(currentDirectory)}` : '';
|
||||
const directory = getRequestDirectory();
|
||||
const queryParams = directory ? `?directory=${encodeURIComponent(directory)}` : '';
|
||||
|
||||
const response = await runtimeFetch(`/api/config/skills/${encodeURIComponent(name)}${queryParams}`, {
|
||||
method: 'DELETE'
|
||||
method: 'DELETE',
|
||||
headers: directory ? { 'x-opencode-directory': directory } : undefined,
|
||||
});
|
||||
|
||||
const payload = await response.json().catch(() => null);
|
||||
@@ -400,7 +418,7 @@ export const useSkillsStore = create<SkillsStore>()(
|
||||
}
|
||||
|
||||
const needsReload = payload?.requiresReload ?? false;
|
||||
invalidateSkillsLoadCache(currentDirectory);
|
||||
invalidateSkillsLoadCache(directory);
|
||||
if (needsReload) {
|
||||
requiresReload = true;
|
||||
await refreshSkillsAfterOpenCodeRestart({
|
||||
@@ -436,11 +454,12 @@ export const useSkillsStore = create<SkillsStore>()(
|
||||
|
||||
readSupportingFile: async (skillName: string, filePath: string) => {
|
||||
try {
|
||||
const currentDirectory = getCurrentDirectory();
|
||||
const queryParams = currentDirectory ? `&directory=${encodeURIComponent(currentDirectory)}` : '';
|
||||
const directory = getRequestDirectory();
|
||||
const queryParams = directory ? `&directory=${encodeURIComponent(directory)}` : '';
|
||||
|
||||
const response = await runtimeFetch(
|
||||
`/api/config/skills/${encodeURIComponent(skillName)}/files/${encodeURIComponent(filePath)}?${queryParams.slice(1)}`
|
||||
`/api/config/skills/${encodeURIComponent(skillName)}/files/${encodeURIComponent(filePath)}?${queryParams.slice(1)}`,
|
||||
{ headers: directory ? { 'x-opencode-directory': directory } : undefined },
|
||||
);
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
@@ -455,14 +474,17 @@ export const useSkillsStore = create<SkillsStore>()(
|
||||
|
||||
writeSupportingFile: async (skillName: string, filePath: string, content: string) => {
|
||||
try {
|
||||
const currentDirectory = getCurrentDirectory();
|
||||
const queryParams = currentDirectory ? `?directory=${encodeURIComponent(currentDirectory)}` : '';
|
||||
const directory = getRequestDirectory();
|
||||
const queryParams = directory ? `?directory=${encodeURIComponent(directory)}` : '';
|
||||
|
||||
const response = await runtimeFetch(
|
||||
`/api/config/skills/${encodeURIComponent(skillName)}/files/${encodeURIComponent(filePath)}${queryParams}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(directory ? { 'x-opencode-directory': directory } : {}),
|
||||
},
|
||||
body: JSON.stringify({ content })
|
||||
}
|
||||
);
|
||||
@@ -475,12 +497,15 @@ export const useSkillsStore = create<SkillsStore>()(
|
||||
|
||||
deleteSupportingFile: async (skillName: string, filePath: string) => {
|
||||
try {
|
||||
const currentDirectory = getCurrentDirectory();
|
||||
const queryParams = currentDirectory ? `?directory=${encodeURIComponent(currentDirectory)}` : '';
|
||||
const directory = getRequestDirectory();
|
||||
const queryParams = directory ? `?directory=${encodeURIComponent(directory)}` : '';
|
||||
|
||||
const response = await runtimeFetch(
|
||||
`/api/config/skills/${encodeURIComponent(skillName)}/files/${encodeURIComponent(filePath)}${queryParams}`,
|
||||
{ method: 'DELETE' }
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: directory ? { 'x-opencode-directory': directory } : undefined,
|
||||
}
|
||||
);
|
||||
|
||||
return response.ok;
|
||||
|
||||
Reference in New Issue
Block a user