Merge origin/main into deferred OpenCode restart branch.
Resolve skills store conflicts by keeping repository-local directory resolution from main and deferred-restart handling from this branch. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
This commit is contained in:
@@ -209,12 +209,32 @@ const JA_MESSAGES: BootstrapMessages = {
|
||||
loadingData: (providersText, agentsText) => `データを読み込み中 (${providersText}, ${agentsText})…`,
|
||||
};
|
||||
|
||||
const DE_MESSAGES: BootstrapMessages = {
|
||||
startingApi: 'OpenCode API wird gestartet…',
|
||||
initializing: 'Initialisierung…',
|
||||
connecting: 'Verbindung wird hergestellt…',
|
||||
connected: 'Verbunden!',
|
||||
connectionError: 'Verbindungsfehler',
|
||||
disconnected: 'Verbindung getrennt',
|
||||
reconnecting: 'Verbindung wird wiederhergestellt…',
|
||||
initialDataLoadFailed: 'OpenCode ist verbunden, aber die Anfangsdaten konnten nicht geladen werden.',
|
||||
cliNotFound: 'OpenCode CLI wurde nicht gefunden. Installieren Sie es zuerst.',
|
||||
providersReady: '✓ Anbieter',
|
||||
providersLoading: '… Anbieter',
|
||||
agentsReady: '✓ Agents',
|
||||
agentsLoading: '… Agents',
|
||||
startingDevServer: (hostLabel) => `Webview-Entwicklungsserver wird gestartet (${hostLabel})...`,
|
||||
waitingDevServer: (hostLabel, attempt) => `Warten auf den Webview-Entwicklungsserver (${hostLabel})... Versuch ${attempt}`,
|
||||
loadingData: (providersText, agentsText) => `Daten werden geladen (${providersText}, ${agentsText})…`,
|
||||
};
|
||||
|
||||
export const getBootstrapMessages = (locale: Locale): BootstrapMessages => {
|
||||
return BOOTSTRAP_MESSAGES[locale];
|
||||
};
|
||||
|
||||
const BOOTSTRAP_MESSAGES: Record<Locale, BootstrapMessages> = {
|
||||
en: EN_MESSAGES,
|
||||
de: DE_MESSAGES,
|
||||
fr: FR_MESSAGES,
|
||||
'zh-CN': ZH_CN_MESSAGES,
|
||||
'zh-TW': ZH_TW_MESSAGES,
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { Locale } from './runtime';
|
||||
|
||||
const INTL_LOCALE_BY_LOCALE: Record<Locale, string> = {
|
||||
en: 'en-US',
|
||||
de: 'de-DE',
|
||||
fr: 'fr-FR',
|
||||
'zh-CN': 'zh-CN',
|
||||
'zh-TW': 'zh-TW',
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, test } from 'bun:test';
|
||||
|
||||
import { dict as enDict } from './messages/en';
|
||||
import { dict as esDict } from './messages/es';
|
||||
import { dict as deDict } from './messages/de';
|
||||
import { dict as frDict } from './messages/fr';
|
||||
import { dict as jaDict } from './messages/ja';
|
||||
import { dict as koDict } from './messages/ko';
|
||||
@@ -13,6 +14,7 @@ import { dict as zhTwDict } from './messages/zh-TW';
|
||||
|
||||
const localeDictionaries = {
|
||||
en: enDict,
|
||||
de: deDict,
|
||||
fr: frDict,
|
||||
es: esDict,
|
||||
ja: jaDict,
|
||||
@@ -35,6 +37,7 @@ describe('i18n dictionaries', () => {
|
||||
|
||||
test('all locales expose language label keys', () => {
|
||||
for (const [, dictionary] of Object.entries(localeDictionaries)) {
|
||||
expect(dictionary['common.language.german']).toBeTruthy();
|
||||
expect(dictionary['common.language.french']).toBeTruthy();
|
||||
expect(dictionary['common.language.japanese']).toBeTruthy();
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,7 @@ export const dict = {
|
||||
'common.loading': 'Loading...',
|
||||
'common.unavailable': 'Unavailable',
|
||||
'common.language.english': 'English',
|
||||
'common.language.german': 'German',
|
||||
'common.language.french': 'French',
|
||||
'common.language.simplifiedChinese': 'Chinese (Simplified)',
|
||||
'common.language.traditionalChinese': 'Chinese (Traditional)',
|
||||
|
||||
@@ -15,6 +15,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"common.loading": "Cargando...",
|
||||
"common.unavailable": "No disponible",
|
||||
"common.language.english": "Inglés",
|
||||
"common.language.german": "Alemán",
|
||||
"common.language.french": "Francés",
|
||||
"common.language.simplifiedChinese": "Chino (simplificado)",
|
||||
"common.language.traditionalChinese": "Chino (tradicional)",
|
||||
|
||||
@@ -14,6 +14,7 @@ export const dict = {
|
||||
'common.loading': 'Chargement...',
|
||||
'common.unavailable': 'Indisponible',
|
||||
'common.language.english': 'Anglais',
|
||||
'common.language.german': 'Allemand',
|
||||
'common.language.french': 'Français',
|
||||
'common.language.simplifiedChinese': 'Chinois (simplifié)',
|
||||
'common.language.traditionalChinese': 'Chinois (traditionnel)',
|
||||
|
||||
@@ -15,6 +15,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'common.loading': '読み込み中...',
|
||||
'common.unavailable': '利用できません',
|
||||
'common.language.english': '英語',
|
||||
'common.language.german': 'ドイツ語',
|
||||
'common.language.french': 'フランス語',
|
||||
'common.language.simplifiedChinese': '中国語(簡体字)',
|
||||
'common.language.traditionalChinese': '中国語(繁体字)',
|
||||
|
||||
@@ -15,6 +15,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'common.loading': '로딩 중...',
|
||||
'common.unavailable': '사용할 수 없음',
|
||||
'common.language.english': '영어',
|
||||
'common.language.german': '독일어',
|
||||
'common.language.french': '프랑스어',
|
||||
'common.language.simplifiedChinese': '중국어(간체)',
|
||||
'common.language.traditionalChinese': '중국어(번체)',
|
||||
|
||||
@@ -16,6 +16,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'common.loading': 'Ładowanie...',
|
||||
'common.unavailable': 'Niedostępne',
|
||||
'common.language.english': 'Angielski',
|
||||
'common.language.german': 'Niemiecki',
|
||||
'common.language.french': 'Francuski',
|
||||
'common.language.simplifiedChinese': 'Chiński (Uproszczony)',
|
||||
'common.language.traditionalChinese': 'Chiński (Tradycyjny)',
|
||||
|
||||
@@ -15,6 +15,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"common.loading": "Carregando...",
|
||||
"common.unavailable": "Indisponível",
|
||||
"common.language.english": "Inglês",
|
||||
"common.language.german": "Alemão",
|
||||
"common.language.french": "Francês",
|
||||
"common.language.simplifiedChinese": "Chinês (simplificado)",
|
||||
"common.language.traditionalChinese": "Chinês (tradicional)",
|
||||
|
||||
@@ -15,6 +15,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"common.loading": "Завантаження...",
|
||||
"common.unavailable": "Недоступно",
|
||||
"common.language.english": "англійська",
|
||||
"common.language.german": "Німецька",
|
||||
"common.language.french": "Французька",
|
||||
"common.language.simplifiedChinese": "Китайська (спрощена)",
|
||||
"common.language.traditionalChinese": "Китайська (традиційна)",
|
||||
|
||||
@@ -15,6 +15,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'common.loading': '加载中...',
|
||||
'common.unavailable': '不可用',
|
||||
'common.language.english': 'English',
|
||||
'common.language.german': '德语',
|
||||
'common.language.french': '法语',
|
||||
'common.language.simplifiedChinese': '简体中文',
|
||||
'common.language.traditionalChinese': '繁体中文',
|
||||
|
||||
@@ -15,6 +15,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'common.loading': '載入中...',
|
||||
'common.unavailable': '無法使用',
|
||||
'common.language.english': 'English',
|
||||
'common.language.german': '德語',
|
||||
'common.language.french': '法語',
|
||||
'common.language.simplifiedChinese': '簡體中文',
|
||||
'common.language.traditionalChinese': '繁體中文',
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
export type Locale = 'en' | 'fr' | 'zh-CN' | 'zh-TW' | 'uk' | 'es' | 'pt-BR' | 'ko' | 'pl' | 'ja';
|
||||
export type Locale = 'en' | 'de' | 'fr' | 'zh-CN' | 'zh-TW' | 'uk' | 'es' | 'pt-BR' | 'ko' | 'pl' | 'ja';
|
||||
|
||||
export const LOCALES = ['en', 'fr', 'zh-CN', 'zh-TW', 'uk', 'es', 'pt-BR', 'ko', 'pl', 'ja'] as const satisfies readonly Locale[];
|
||||
export const LOCALES = ['en', 'de', 'fr', 'zh-CN', 'zh-TW', 'uk', 'es', 'pt-BR', 'ko', 'pl', 'ja'] as const satisfies readonly Locale[];
|
||||
|
||||
export const DEFAULT_LOCALE: Locale = 'en';
|
||||
|
||||
export const LOCALE_LABEL_KEYS: Record<Locale, 'common.language.english' | 'common.language.french' | 'common.language.simplifiedChinese' | 'common.language.traditionalChinese' | 'common.language.ukrainian' | 'common.language.spanish' | 'common.language.brazilianPortuguese' | 'common.language.korean' | 'common.language.polish' | 'common.language.japanese'> = {
|
||||
export const LOCALE_LABEL_KEYS: Record<Locale, 'common.language.english' | 'common.language.french' | 'common.language.simplifiedChinese' | 'common.language.traditionalChinese' | 'common.language.ukrainian' | 'common.language.spanish' | 'common.language.brazilianPortuguese' | 'common.language.korean' | 'common.language.polish' | 'common.language.german' | 'common.language.japanese'> = {
|
||||
en: 'common.language.english',
|
||||
fr: 'common.language.french',
|
||||
'zh-CN': 'common.language.simplifiedChinese',
|
||||
@@ -14,6 +14,7 @@ export const LOCALE_LABEL_KEYS: Record<Locale, 'common.language.english' | 'comm
|
||||
'pt-BR': 'common.language.brazilianPortuguese',
|
||||
ko: 'common.language.korean',
|
||||
pl: 'common.language.polish',
|
||||
de: 'common.language.german',
|
||||
ja: 'common.language.japanese',
|
||||
};
|
||||
|
||||
@@ -59,6 +60,9 @@ export function normalizeLocale(value: string | undefined | null): Locale {
|
||||
if (normalized === 'ja' || normalized.startsWith('ja-')) {
|
||||
return 'ja';
|
||||
}
|
||||
if (normalized === 'de' || normalized.startsWith('de-')) {
|
||||
return 'de';
|
||||
}
|
||||
if (normalized === 'pl' || normalized.startsWith('pl-')) {
|
||||
return 'pl';
|
||||
}
|
||||
|
||||
@@ -42,9 +42,11 @@ async function loadDictionary(locale: Locale): Promise<I18nDictionary> {
|
||||
? await import('./messages/ko') as { dict: I18nDictionary }
|
||||
: locale === 'pl'
|
||||
? await import('./messages/pl') as { dict: I18nDictionary }
|
||||
: locale === 'ja'
|
||||
? await import('./messages/ja') as { dict: I18nDictionary }
|
||||
: { dict: enDict };
|
||||
: locale === 'de'
|
||||
? await import('./messages/de') as { dict: I18nDictionary }
|
||||
: locale === 'ja'
|
||||
? await import('./messages/ja') as { dict: I18nDictionary }
|
||||
: { dict: enDict };
|
||||
dictionaries.set(locale, mod.dict);
|
||||
return mod.dict;
|
||||
}
|
||||
|
||||
@@ -765,7 +765,9 @@ async function performConfigRefresh(options: {
|
||||
uiRefreshTasks.push(commandsStore.loadCommands().then(() => undefined));
|
||||
}
|
||||
if (refreshSkills) {
|
||||
invalidateSkillsLoadCache(currentDirectory);
|
||||
// Match loadSkills cache key (active-project-first). Passing client/directory-store
|
||||
// path here misses the key when those diverge after getRequestDirectory().
|
||||
invalidateSkillsLoadCache();
|
||||
uiRefreshTasks.push(skillsStore.loadSkills().then(() => undefined));
|
||||
uiRefreshTasks.push(skillsCatalogStore.loadCatalog({ refresh: true }).then(() => undefined));
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import type {
|
||||
|
||||
import { invalidateSkillsLoadCache, refreshSkillsAfterOpenCodeRestart, useSkillsStore } from '@/stores/useSkillsStore';
|
||||
import { opencodeClient } from '@/lib/opencode/client';
|
||||
import { useProjectsStore } from '@/stores/useProjectsStore';
|
||||
import { startConfigUpdate } from '@/lib/configUpdate';
|
||||
import { runtimeFetch } from '@/lib/runtime-fetch';
|
||||
import { noteDeferredRestartFromPayload } from '@/lib/opencode/deferredRestart';
|
||||
@@ -46,20 +47,21 @@ const getSkillsCatalogCacheKey = (directory: string | null): string => {
|
||||
return directory?.trim() || DEFAULT_SKILLS_CATALOG_CACHE_KEY;
|
||||
};
|
||||
|
||||
const getCurrentDirectory = (): string | null => {
|
||||
const opencodeDirectory = opencodeClient.getDirectory();
|
||||
if (typeof opencodeDirectory === 'string' && opencodeDirectory.trim().length > 0) {
|
||||
return opencodeDirectory;
|
||||
}
|
||||
|
||||
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('[SkillsCatalogStore] Error resolving config directory:', err);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -119,7 +121,7 @@ export const useSkillsCatalogStore = create<SkillsCatalogState>()(
|
||||
setSelectedSource: (id) => set({ selectedSourceId: id }),
|
||||
|
||||
loadCatalog: async (options) => {
|
||||
const currentDirectory = getCurrentDirectory();
|
||||
const currentDirectory = getRequestDirectory();
|
||||
const cacheKey = getSkillsCatalogCacheKey(currentDirectory);
|
||||
const now = Date.now();
|
||||
const loadedAt = skillsCatalogLastLoadedAt.get(cacheKey) ?? 0;
|
||||
@@ -223,7 +225,7 @@ export const useSkillsCatalogStore = create<SkillsCatalogState>()(
|
||||
set({ isLoadingSource: true, lastCatalogError: null });
|
||||
|
||||
try {
|
||||
const currentDirectory = getCurrentDirectory();
|
||||
const currentDirectory = getRequestDirectory();
|
||||
const refresh = options?.refresh ? '&refresh=true' : '';
|
||||
const queryParams = currentDirectory
|
||||
? `?directory=${encodeURIComponent(currentDirectory)}&sourceId=${encodeURIComponent(sourceId)}${refresh}`
|
||||
@@ -294,7 +296,7 @@ export const useSkillsCatalogStore = create<SkillsCatalogState>()(
|
||||
|
||||
set({ isLoadingMore: true });
|
||||
try {
|
||||
const currentDirectory = getCurrentDirectory();
|
||||
const currentDirectory = getRequestDirectory();
|
||||
const parts = [`sourceId=${encodeURIComponent(selectedSourceId)}`];
|
||||
if (currentDirectory) {
|
||||
parts.push(`directory=${encodeURIComponent(currentDirectory)}`);
|
||||
@@ -356,7 +358,7 @@ export const useSkillsCatalogStore = create<SkillsCatalogState>()(
|
||||
scanRepo: async (request) => {
|
||||
set({ isScanning: true, lastScanError: null, scanResults: null });
|
||||
try {
|
||||
const currentDirectory = getCurrentDirectory();
|
||||
const currentDirectory = getRequestDirectory();
|
||||
const queryParams = currentDirectory ? `?directory=${encodeURIComponent(currentDirectory)}` : '';
|
||||
|
||||
const response = await runtimeFetch(`/api/config/skills/scan${queryParams}`, {
|
||||
@@ -390,7 +392,7 @@ export const useSkillsCatalogStore = create<SkillsCatalogState>()(
|
||||
const directoryOverride = typeof options?.directory === 'string' && options.directory.trim().length > 0
|
||||
? options.directory.trim()
|
||||
: null;
|
||||
const currentDirectory = directoryOverride ?? getCurrentDirectory();
|
||||
const currentDirectory = directoryOverride ?? getRequestDirectory();
|
||||
const queryParams = currentDirectory ? `?directory=${encodeURIComponent(currentDirectory)}` : '';
|
||||
|
||||
const response = await runtimeFetch(`/api/config/skills/install${queryParams}`, {
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import { beforeEach, describe, expect, mock, test } from 'bun:test';
|
||||
|
||||
const activeProjectPath = '/workspace/project-with-agents-skills';
|
||||
|
||||
let runtimeFetchCalls: Array<{ url: string; headers?: HeadersInit }> = [];
|
||||
let runtimeFetchImpl: (url: string, init?: RequestInit) => Promise<Response> = async () => (
|
||||
new Response(JSON.stringify({ skills: [] }), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
);
|
||||
let getDirectoryImpl: () => string | undefined = () => undefined;
|
||||
|
||||
const runtimeFetchMock = async (url: string, init?: RequestInit) => {
|
||||
runtimeFetchCalls.push({ url: String(url), headers: init?.headers });
|
||||
return runtimeFetchImpl(url, init);
|
||||
};
|
||||
|
||||
mock.module('@/lib/opencode/client', () => ({
|
||||
opencodeClient: {
|
||||
getDirectory: () => getDirectoryImpl(),
|
||||
checkHealth: async () => true,
|
||||
},
|
||||
}));
|
||||
|
||||
mock.module('@/stores/useProjectsStore', () => ({
|
||||
useProjectsStore: {
|
||||
getState: () => ({
|
||||
getActiveProject: () => ({ path: activeProjectPath }),
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
mock.module('@/lib/runtime-fetch', () => ({
|
||||
runtimeFetch: runtimeFetchMock,
|
||||
}));
|
||||
|
||||
mock.module('@/lib/background-network', () => ({
|
||||
runBackgroundNetworkTask: async <T,>(task: () => Promise<T>) => task(),
|
||||
}));
|
||||
|
||||
mock.module('@/lib/configUpdate', () => ({
|
||||
startConfigUpdate: mock(() => undefined),
|
||||
finishConfigUpdate: mock(() => undefined),
|
||||
updateConfigUpdateMessage: mock(() => undefined),
|
||||
}));
|
||||
|
||||
mock.module('@/lib/configSync', () => ({
|
||||
emitConfigChange: mock(() => undefined),
|
||||
scopeMatches: mock(() => false),
|
||||
subscribeToConfigChanges: mock(() => () => undefined),
|
||||
}));
|
||||
|
||||
mock.module('./utils/safeStorage', () => ({
|
||||
createDeferredSafeJSONStorage: () => ({
|
||||
getItem: async () => null,
|
||||
setItem: async () => undefined,
|
||||
removeItem: async () => undefined,
|
||||
}),
|
||||
}));
|
||||
|
||||
const { invalidateSkillsLoadCache, useSkillsStore } = await import('./useSkillsStore');
|
||||
|
||||
describe('useSkillsStore directory resolution', () => {
|
||||
beforeEach(() => {
|
||||
runtimeFetchCalls = [];
|
||||
getDirectoryImpl = () => undefined;
|
||||
runtimeFetchImpl = async () => new Response(JSON.stringify({
|
||||
skills: [{
|
||||
name: 'repo-local-skill',
|
||||
path: `${activeProjectPath}/.agents/skills/repo-local-skill/SKILL.md`,
|
||||
scope: 'project',
|
||||
source: 'agents',
|
||||
sources: { md: { description: 'Repository local' } },
|
||||
}],
|
||||
}), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
invalidateSkillsLoadCache(activeProjectPath);
|
||||
useSkillsStore.setState({
|
||||
selectedSkillName: null,
|
||||
skills: [],
|
||||
isLoading: false,
|
||||
skillDraft: null,
|
||||
});
|
||||
});
|
||||
|
||||
test('loadSkills scopes discovery to the active project even when client directory is unset', async () => {
|
||||
const loaded = await useSkillsStore.getState().loadSkills();
|
||||
|
||||
expect(loaded).toBe(true);
|
||||
expect(runtimeFetchCalls.length).toBe(1);
|
||||
expect(runtimeFetchCalls[0]?.url).toContain(`directory=${encodeURIComponent(activeProjectPath)}`);
|
||||
expect(useSkillsStore.getState().skills).toEqual([{
|
||||
name: 'repo-local-skill',
|
||||
path: `${activeProjectPath}/.agents/skills/repo-local-skill/SKILL.md`,
|
||||
scope: 'project',
|
||||
source: 'agents',
|
||||
description: 'Repository local',
|
||||
group: undefined,
|
||||
}]);
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
// Wrong key: client-directory-first null maps to __default__, not the active project.
|
||||
invalidateSkillsLoadCache(null);
|
||||
expect(await useSkillsStore.getState().loadSkills()).toBe(true);
|
||||
expect(runtimeFetchCalls.length).toBe(1);
|
||||
|
||||
// Default resolution must match loadSkills (active project first).
|
||||
invalidateSkillsLoadCache();
|
||||
expect(await useSkillsStore.getState().loadSkills()).toBe(true);
|
||||
expect(runtimeFetchCalls.length).toBe(2);
|
||||
expect(runtimeFetchCalls[1]?.url).toContain(`directory=${encodeURIComponent(activeProjectPath)}`);
|
||||
});
|
||||
});
|
||||
@@ -11,23 +11,29 @@ import { createDeferredSafeJSONStorage } from "./utils/safeStorage";
|
||||
import { runtimeFetch } from "@/lib/runtime-fetch";
|
||||
import { runBackgroundNetworkTask } from "@/lib/background-network";
|
||||
import { noteDeferredRestartFromPayload } from "@/lib/opencode/deferredRestart";
|
||||
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;
|
||||
@@ -170,7 +176,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));
|
||||
};
|
||||
|
||||
@@ -237,8 +243,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;
|
||||
@@ -259,9 +265,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}`);
|
||||
}
|
||||
@@ -302,10 +311,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;
|
||||
}
|
||||
@@ -328,12 +339,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)
|
||||
});
|
||||
|
||||
@@ -343,7 +357,7 @@ export const useSkillsStore = create<SkillsStore>()(
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
invalidateSkillsLoadCache(currentDirectory);
|
||||
invalidateSkillsLoadCache(directory);
|
||||
|
||||
if (payload?.requiresManualRestart) {
|
||||
upsertSkillLocal(set, get, config.name, config);
|
||||
@@ -384,12 +398,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)
|
||||
});
|
||||
|
||||
@@ -399,7 +416,7 @@ export const useSkillsStore = create<SkillsStore>()(
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
invalidateSkillsLoadCache(currentDirectory);
|
||||
invalidateSkillsLoadCache(directory);
|
||||
|
||||
if (payload?.requiresManualRestart) {
|
||||
upsertSkillLocal(set, get, name, config);
|
||||
@@ -433,11 +450,12 @@ export const useSkillsStore = create<SkillsStore>()(
|
||||
|
||||
deleteSkill: 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}`, {
|
||||
method: 'DELETE'
|
||||
method: 'DELETE',
|
||||
headers: directory ? { 'x-opencode-directory': directory } : undefined,
|
||||
});
|
||||
|
||||
const payload = await response.json().catch(() => null);
|
||||
@@ -446,7 +464,7 @@ export const useSkillsStore = create<SkillsStore>()(
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
invalidateSkillsLoadCache(currentDirectory);
|
||||
invalidateSkillsLoadCache(directory);
|
||||
|
||||
if (payload?.requiresManualRestart) {
|
||||
removeSkillLocal(set, get, name);
|
||||
@@ -490,11 +508,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;
|
||||
@@ -509,14 +528,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 })
|
||||
}
|
||||
);
|
||||
@@ -529,12 +551,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