Merge origin/main into deferred OpenCode restart branch.
Resolve ProvidersPage and lifecycle conflicts with custom providers and AppImage ARGV0 stripping. Address review follow-ups: OAuth index helper + tests, single auth-methods load trigger, shared Google env-alias module with VS Code parity coverage, and deferred restart for custom provider upsert. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
This commit is contained in:
@@ -127,6 +127,7 @@ mock.module('./useGlobalSessionsStore', () => ({
|
||||
}));
|
||||
|
||||
mock.module('@/sync/sync-refs', () => ({
|
||||
getSyncSessionDirectory: () => null,
|
||||
registerSessionDirectory: (sessionID: string, directory: string) => {
|
||||
registeredDirectories.push({ sessionID, directory });
|
||||
},
|
||||
|
||||
@@ -98,9 +98,94 @@ describe('useSkillsStore directory resolution', () => {
|
||||
source: 'agents',
|
||||
description: 'Repository local',
|
||||
group: undefined,
|
||||
renamable: false,
|
||||
}]);
|
||||
});
|
||||
|
||||
test('loadSkills maps authoritative renamable from the list response', async () => {
|
||||
runtimeFetchImpl = async () => new Response(JSON.stringify({
|
||||
skills: [
|
||||
{
|
||||
name: 'managed-skill',
|
||||
path: `${activeProjectPath}/.opencode/skills/managed-skill/SKILL.md`,
|
||||
scope: 'project',
|
||||
source: 'opencode',
|
||||
renamable: true,
|
||||
sources: { md: { description: 'Managed' } },
|
||||
},
|
||||
{
|
||||
name: 'cache-skill',
|
||||
path: '/home/ubuntu/.cache/opencode/skills/hash/cache-skill/SKILL.md',
|
||||
scope: 'user',
|
||||
source: 'opencode',
|
||||
renamable: false,
|
||||
sources: { md: { description: 'Cache' } },
|
||||
},
|
||||
],
|
||||
}), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
expect(await useSkillsStore.getState().loadSkills()).toBe(true);
|
||||
expect(useSkillsStore.getState().skills).toEqual([
|
||||
{
|
||||
name: 'managed-skill',
|
||||
path: `${activeProjectPath}/.opencode/skills/managed-skill/SKILL.md`,
|
||||
scope: 'project',
|
||||
source: 'opencode',
|
||||
description: 'Managed',
|
||||
group: undefined,
|
||||
renamable: true,
|
||||
},
|
||||
{
|
||||
name: 'cache-skill',
|
||||
path: '/home/ubuntu/.cache/opencode/skills/hash/cache-skill/SKILL.md',
|
||||
scope: 'user',
|
||||
source: 'opencode',
|
||||
description: 'Cache',
|
||||
group: 'hash',
|
||||
renamable: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('renameSkill uses getRequestDirectory query and x-opencode-directory header', async () => {
|
||||
runtimeFetchImpl = async (_url, init) => {
|
||||
if (init?.method === 'PATCH') {
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
requiresReload: false,
|
||||
}), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
skills: [{
|
||||
name: 'new-skill',
|
||||
path: `${activeProjectPath}/.opencode/skills/new-skill/SKILL.md`,
|
||||
scope: 'project',
|
||||
source: 'opencode',
|
||||
renamable: true,
|
||||
sources: { md: { description: 'Renamed' } },
|
||||
}],
|
||||
}), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
};
|
||||
|
||||
const renamed = await useSkillsStore.getState().renameSkill('old-skill', 'new-skill');
|
||||
expect(renamed).toBe(true);
|
||||
|
||||
const renameCall = runtimeFetchCalls.find((call) => String(call.url).includes('/api/config/skills/old-skill'));
|
||||
expect(renameCall).toBeTruthy();
|
||||
expect(renameCall?.url).toContain(`directory=${encodeURIComponent(activeProjectPath)}`);
|
||||
|
||||
const headers = new Headers(renameCall?.headers);
|
||||
expect(headers.get('content-type')).toBe('application/json');
|
||||
expect(headers.get('x-opencode-directory')).toBe(activeProjectPath);
|
||||
});
|
||||
|
||||
test('invalidateSkillsLoadCache() with no argument clears the active-project cache key used by loadSkills', async () => {
|
||||
expect(await useSkillsStore.getState().loadSkills()).toBe(true);
|
||||
expect(runtimeFetchCalls.length).toBe(1);
|
||||
|
||||
@@ -77,6 +77,8 @@ export interface DiscoveredSkill {
|
||||
description?: string;
|
||||
/** Domain folder parsed from file path, e.g. "automation-ai", "lark-ecosystem" */
|
||||
group?: string;
|
||||
/** Authoritative server flag: skill lives under a managed root and can be renamed in place. */
|
||||
renamable?: boolean;
|
||||
}
|
||||
|
||||
/** Parse the domain group folder from a skill file path.
|
||||
@@ -100,6 +102,7 @@ interface RawSkillResponse {
|
||||
path: string;
|
||||
scope?: SkillScope;
|
||||
source?: SkillSource;
|
||||
renamable?: boolean;
|
||||
sources?: {
|
||||
md?: {
|
||||
description?: string;
|
||||
@@ -150,6 +153,7 @@ interface SkillsStore {
|
||||
getSkillDetail: (name: string) => Promise<SkillDetail | null>;
|
||||
createSkill: (config: SkillConfig) => Promise<boolean>;
|
||||
updateSkill: (name: string, config: Partial<SkillConfig>) => Promise<boolean>;
|
||||
renameSkill: (name: string, newName: string) => Promise<boolean>;
|
||||
deleteSkill: (name: string) => Promise<boolean>;
|
||||
getSkillByName: (name: string) => DiscoveredSkill | undefined;
|
||||
|
||||
@@ -284,6 +288,7 @@ export const useSkillsStore = create<SkillsStore>()(
|
||||
source: s.source ?? 'opencode',
|
||||
description: s.sources?.md?.description || '',
|
||||
group: parseSkillGroup(s.path),
|
||||
renamable: s.renamable === true,
|
||||
}));
|
||||
|
||||
set({ skills: configSkills, isLoading: false });
|
||||
@@ -448,6 +453,53 @@ export const useSkillsStore = create<SkillsStore>()(
|
||||
}
|
||||
},
|
||||
|
||||
renameSkill: async (name: string, newName: string) => {
|
||||
startConfigUpdate("Renaming skill...");
|
||||
let requiresReload = false;
|
||||
try {
|
||||
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',
|
||||
...(directory ? { 'x-opencode-directory': directory } : {}),
|
||||
},
|
||||
body: JSON.stringify({ renameTo: newName }),
|
||||
});
|
||||
|
||||
const payload = await response.json().catch(() => null);
|
||||
if (!response.ok) {
|
||||
const message = payload?.error || 'Failed to rename skill';
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
const needsReload = payload?.requiresReload ?? false;
|
||||
invalidateSkillsLoadCache(directory);
|
||||
if (needsReload) {
|
||||
requiresReload = true;
|
||||
await refreshSkillsAfterOpenCodeRestart({
|
||||
message: payload?.message,
|
||||
delayMs: payload?.reloadDelayMs,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
const loaded = await get().loadSkills();
|
||||
if (loaded) {
|
||||
emitConfigChange("skills", { source: CONFIG_EVENT_SOURCE });
|
||||
}
|
||||
return loaded;
|
||||
} catch {
|
||||
return false;
|
||||
} finally {
|
||||
if (!requiresReload) {
|
||||
finishConfigUpdate();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
deleteSkill: async (name: string) => {
|
||||
try {
|
||||
const directory = getRequestDirectory();
|
||||
|
||||
Reference in New Issue
Block a user