Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ Actions that can change local state include:
- creating, updating, syncing, importing, or deleting a slash command
- changing harness support settings

App-owned files live under `~/Library/Application Support/skill-manager` on macOS and XDG base directories on Linux.
App-owned files live under `~/.skill-manager` on macOS (with a legacy fallback to `~/Library/Application Support/skill-manager` if it already exists) and XDG base directories on Linux.

## How it works

Expand Down Expand Up @@ -242,17 +242,17 @@ CLI marketplace entries are preview-only.

## Configuration

On macOS, app-owned files live under `~/Library/Application Support/skill-manager`. On Linux, app-owned files use XDG base directories.
On macOS, app-owned files live under `~/.skill-manager` (with a legacy fallback to `~/Library/Application Support/skill-manager` if it already exists). On Linux, app-owned files use XDG base directories.

Useful macOS paths:

- shared skills store: `~/Library/Application Support/skill-manager/shared`
- MCP manifest: `~/Library/Application Support/skill-manager/mcp/manifest.json`
- slash command library: `~/Library/Application Support/skill-manager/slash-commands/commands`
- slash command sync state: `~/Library/Application Support/skill-manager/slash-commands/sync-state.json`
- marketplace cache: `~/Library/Application Support/skill-manager/marketplace`
- app database and LLM scan configs: `~/Library/Application Support/skill-manager/skill-manager.db`
- app settings: `~/Library/Application Support/skill-manager/settings.json`
- shared skills store: `~/.skill-manager/shared`
- MCP manifest: `~/.skill-manager/mcp/manifest.json`
- slash command library: `~/.skill-manager/slash-commands/commands`
- slash command sync state: `~/.skill-manager/slash-commands/sync-state.json`
- marketplace cache: `~/.skill-manager/marketplace`
- app database and LLM scan configs: `~/.skill-manager/skill-manager.db`
- app settings: `~/.skill-manager/settings.json`

Useful Linux paths:

Expand Down
18 changes: 9 additions & 9 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ Skill Manager 是本地配置管理工具。它在你的机器上运行,并读
- 创建、更新、同步、导入或删除 slash command
- 修改 harness 支持设置

在 macOS 上,应用拥有的文件位于 `~/Library/Application Support/skill-manager`;在 Linux 上使用 XDG base directories。
在 macOS 上,应用拥有的文件位于 `~/.skill-manager`(如果已存在,则回退到 `~/Library/Application Support/skill-manager`;在 Linux 上使用 XDG base directories。

## 工作方式

Expand Down Expand Up @@ -212,17 +212,17 @@ CLI marketplace 条目仅用于预览。

## 配置

在 macOS 上,应用拥有的文件位于 `~/Library/Application Support/skill-manager`;在 Linux 上使用 XDG base directories。
在 macOS 上,应用拥有的文件位于 `~/.skill-manager`(如果已存在,则回退到 `~/Library/Application Support/skill-manager`;在 Linux 上使用 XDG base directories。

常用 macOS 路径:

- 共享 Skill 存储:`~/Library/Application Support/skill-manager/shared`
- MCP manifest:`~/Library/Application Support/skill-manager/mcp/manifest.json`
- slash command 库:`~/Library/Application Support/skill-manager/slash-commands/commands`
- slash command 同步状态:`~/Library/Application Support/skill-manager/slash-commands/sync-state.json`
- 商城缓存:`~/Library/Application Support/skill-manager/marketplace`
- 应用数据库和 LLM 扫描配置:`~/Library/Application Support/skill-manager/skill-manager.db`
- 应用设置:`~/Library/Application Support/skill-manager/settings.json`
- 共享 Skill 存储:`~/.skill-manager/shared`
- MCP manifest:`~/.skill-manager/mcp/manifest.json`
- slash command 库:`~/.skill-manager/slash-commands/commands`
- slash command 同步状态:`~/.skill-manager/slash-commands/sync-state.json`
- 商城缓存:`~/.skill-manager/marketplace`
- 应用数据库和 LLM 扫描配置:`~/.skill-manager/skill-manager.db`
- 应用设置:`~/.skill-manager/settings.json`

常用 Linux 路径:

Expand Down
9 changes: 9 additions & 0 deletions assets/harness-logos/agy-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 11 additions & 7 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import ScanConfigPage from "./features/skills/screens/ScanConfigPage";
import SkillsWorkspacePage from "./features/skills/screens/SkillsWorkspacePage";
import { LocaleProvider, useCommonCopy } from "./i18n";

import { ThemeProvider } from "./lib/theme";

const MarketplaceLayout = lazy(() => import("./features/marketplace/components/MarketplaceLayout"));
const OverviewPage = lazy(() => import("./features/overview/screens/OverviewPage"));
const SettingsPage = lazy(() => import("./features/settings/screens/SettingsPage"));
Expand All @@ -36,13 +38,15 @@ export function App() {

return (
<QueryClientProvider client={queryClient}>
<LocaleProvider>
<ToastProvider>
<UiTooltipProvider>
<AppContent />
</UiTooltipProvider>
</ToastProvider>
</LocaleProvider>
<ThemeProvider>
<LocaleProvider>
<ToastProvider>
<UiTooltipProvider>
<AppContent />
</UiTooltipProvider>
</ToastProvider>
</LocaleProvider>
</ThemeProvider>
</QueryClientProvider>
);
}
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/assets/harness-logos/agy-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 7 additions & 3 deletions frontend/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
Command,
Languages,
LayoutDashboard,
Moon,
RefreshCw,
Settings,
Store,
Expand All @@ -27,6 +28,7 @@ import { useSidebarModel, type SidebarIconKey } from "../app/capability-registry
import { LoadingSpinner } from "./LoadingSpinner";
import { useToast } from "./Toast";
import { useCommonCopy, useLocale } from "../i18n";
import { useTheme } from "../lib/theme";

interface SidebarProps {
onRefresh: () => void | Promise<void>;
Expand All @@ -37,6 +39,7 @@ export function Sidebar({ onRefresh, refreshPending }: SidebarProps) {
const model = useSidebarModel();
const { toast } = useToast();
const common = useCommonCopy();
const { theme, toggleTheme } = useTheme();

return (
<aside className="sidebar ui-scrollbar--thin" aria-label={common.nav.primary}>
Expand Down Expand Up @@ -89,10 +92,11 @@ export function Sidebar({ onRefresh, refreshPending }: SidebarProps) {
<button
type="button"
className="sidebar-footer-btn"
onClick={() => toast(common.nav.lightComingSoon)}
onClick={toggleTheme}
aria-label={theme === "light" ? "Switch to dark theme" : "Switch to light theme"}
>
<SunMedium size={16} />
<span>{common.nav.light}</span>
{theme === "light" ? <Moon size={16} /> : <SunMedium size={16} />}
<span>{theme === "light" ? common.nav.dark : common.nav.light}</span>
</button>
<SidebarLanguageMenu />
<NavLink
Expand Down
8 changes: 7 additions & 1 deletion frontend/src/components/harness/harnessPresentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import codexLogo from "../../assets/harness-logos/codex-logo.svg";
import cursorLogo from "../../assets/harness-logos/cursor-logo.svg";
import openclawLogo from "../../assets/harness-logos/openclaw-logo.svg";
import opencodeLogo from "../../assets/harness-logos/opencode-logo.svg";
import agyLogo from "../../assets/harness-logos/agy-logo.svg";

export type HarnessLogoKey = "claude" | "codex" | "cursor" | "opencode" | "openclaw";
export type HarnessLogoKey = "claude" | "codex" | "cursor" | "opencode" | "openclaw" | "agy";

interface HarnessPresentation {
logoSrc: string;
Expand Down Expand Up @@ -32,6 +33,10 @@ const HARNESS_LOGO_ASSETS: Record<HarnessLogoKey, HarnessPresentation> = {
logoSrc: openclawLogo,
variant: "openclaw",
},
agy: {
logoSrc: agyLogo,
variant: "agy",
},
};

export function getHarnessPresentation(logoKey: string | null | undefined): HarnessPresentation | null {
Expand All @@ -40,3 +45,4 @@ export function getHarnessPresentation(logoKey: string | null | undefined): Harn
}
return HARNESS_LOGO_ASSETS[logoKey as HarnessLogoKey] ?? null;
}

Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ describe("ScanConfigPage", () => {
expect(screen.queryByText(/Missing required fields: API Key/)).not.toBeInTheDocument();
expect(screen.getByRole("button", { name: "Update" })).toBeDisabled();
expect(screen.queryByRole("columnheader", { name: "Last validation" })).not.toBeInTheDocument();
expect(screen.getByLabelText("Last validation")).toHaveTextContent(/May 12|12 May|Failed|Not validated/);
expect(screen.getByLabelText("Last validation")).toHaveTextContent(/May 11|11 May|May 12|12 May|Failed|Not validated/);
const apiKeyInput = screen.getByLabelText("API Key", { selector: "input" });
expect(apiKeyInput).toHaveAttribute("type", "password");
expect(String(apiKeyInput.getAttribute("value") ?? "")).not.toBe("");
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/i18n/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const englishCommonCopy = {
clis: "CLIs",
settings: "Settings",
light: "Light",
dark: "Dark",
lightComingSoon: "Light theme — coming soon",
},
language: {
Expand Down Expand Up @@ -95,6 +96,7 @@ export const commonCopy = {
clis: "CLI",
settings: "设置",
light: "浅色",
dark: "深色",
lightComingSoon: "浅色主题即将推出",
},
language: {
Expand Down
83 changes: 83 additions & 0 deletions frontend/src/lib/theme.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { createContext, useContext, useEffect, useState, type ReactNode } from "react";

export type Theme = "light" | "dark";

interface ThemeContextValue {
theme: Theme;
toggleTheme: () => void;
setTheme: (theme: Theme) => void;
}

const ThemeContext = createContext<ThemeContextValue | null>(null);

const STORAGE_KEY = "skillmgr.theme";

export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setThemeState] = useState<Theme>(() => {
try {
const stored = window.localStorage.getItem(STORAGE_KEY);
if (stored === "light" || stored === "dark") {
return stored;
}
} catch {
// noop
}
// Check system preference
if (typeof window !== "undefined" && window.matchMedia) {
if (window.matchMedia("(prefers-color-scheme: light)").matches) {
return "light";
}
}
return "dark"; // Default is dark
});

const setTheme = (nextTheme: Theme) => {
setThemeState(nextTheme);
try {
window.localStorage.setItem(STORAGE_KEY, nextTheme);
} catch {
// noop
}
};

const toggleTheme = () => {
setTheme(theme === "light" ? "dark" : "light");
};

useEffect(() => {
const root = window.document.documentElement;
root.setAttribute("data-theme", theme);
}, [theme]);

// Sync with system preference changes
useEffect(() => {
if (typeof window === "undefined" || !window.matchMedia) return;
const mediaQuery = window.matchMedia("(prefers-color-scheme: light)");
const handler = (e: MediaQueryListEvent) => {
// Only sync if the user hasn't explicitly set a preference in localStorage
try {
if (!window.localStorage.getItem(STORAGE_KEY)) {
setThemeState(e.matches ? "light" : "dark");
}
} catch {
setThemeState(e.matches ? "light" : "dark");
}
};
mediaQuery.addEventListener("change", handler);
return () => mediaQuery.removeEventListener("change", handler);
}, []);

return (
<ThemeContext.Provider value={{ theme, toggleTheme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}

export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error("useTheme must be used within a ThemeProvider");
}
return context;
}
102 changes: 102 additions & 0 deletions frontend/src/styles/tokens.css
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
--font-size-xl: 1.32rem;
--font-size-2xl: 2rem;

color-scheme: dark;

/* Surfaces */
--color-bg: #0b0c0f;
--color-surface: #1c1d21;
Expand Down Expand Up @@ -82,3 +84,103 @@
/* Layout */
--sidebar-width: 256px;
}

/* System light theme preference if the user hasn't explicitly selected dark theme */
@media (prefers-color-scheme: light) {
:root:not([data-theme="dark"]) {
color-scheme: light;

/* Surfaces */
--color-bg: #f8f5ed;
--color-surface: #ffffff;
--color-surface-raised: #ffffff;
--color-surface-sunken: #eae6db;
--color-sidebar-bg: #f2efe7;

/* Borders */
--color-border: #e2dccb;
--color-border-strong: #c8bfa6;

/* Text */
--color-text: #1f1f1c;
--color-text-muted: #6e6859;
--color-text-subtle: #9a8e72;
--color-text-inverted: #ffffff;

/* Accent (Brand Amber from logo) */
--color-accent: #c07204;
--color-accent-strong: #965303;
--color-accent-soft: rgba(192, 114, 4, 0.12);
--color-accent-softer: rgba(192, 114, 4, 0.06);

/* Status */
--color-success: #0f6e56;
--color-success-soft: #e8f5ee;
--color-danger: #b14828;
--color-danger-soft: #f7e5da;
--color-warning: #9a8e72;
--color-warning-soft: #fbf9f4;

/* Shadows */
--shadow-sm: 0 1px 2px rgba(31, 31, 28, 0.05);
--shadow-md: 0 4px 12px rgba(31, 31, 28, 0.08);
--shadow-panel: 0 12px 28px rgba(31, 31, 28, 0.1);
--shadow-lift: 0 10px 32px rgba(31, 31, 28, 0.12), 0 2px 8px rgba(31, 31, 28, 0.06);

/* Scrollbars */
--scrollbar-track: rgba(0, 0, 0, 0.02);
--scrollbar-thumb: rgba(154, 142, 114, 0.3);
--scrollbar-thumb-hover: rgba(154, 142, 114, 0.5);
--scrollbar-thumb-active: rgba(154, 142, 114, 0.7);
--scrollbar-corner: #f8f5ed;
}
}

/* Explicit light theme setting */
:root[data-theme="light"] {
color-scheme: light;

/* Surfaces */
--color-bg: #f8f5ed;
--color-surface: #ffffff;
--color-surface-raised: #ffffff;
--color-surface-sunken: #eae6db;
--color-sidebar-bg: #f2efe7;

/* Borders */
--color-border: #e2dccb;
--color-border-strong: #c8bfa6;

/* Text */
--color-text: #1f1f1c;
--color-text-muted: #6e6859;
--color-text-subtle: #9a8e72;
--color-text-inverted: #ffffff;

/* Accent (Brand Amber from logo) */
--color-accent: #c07204;
--color-accent-strong: #965303;
--color-accent-soft: rgba(192, 114, 4, 0.12);
--color-accent-softer: rgba(192, 114, 4, 0.06);

/* Status */
--color-success: #0f6e56;
--color-success-soft: #e8f5ee;
--color-danger: #b14828;
--color-danger-soft: #f7e5da;
--color-warning: #9a8e72;
--color-warning-soft: #fbf9f4;

/* Shadows */
--shadow-sm: 0 1px 2px rgba(31, 31, 28, 0.05);
--shadow-md: 0 4px 12px rgba(31, 31, 28, 0.08);
--shadow-panel: 0 12px 28px rgba(31, 31, 28, 0.1);
--shadow-lift: 0 10px 32px rgba(31, 31, 28, 0.12), 0 2px 8px rgba(31, 31, 28, 0.06);

/* Scrollbars */
--scrollbar-track: rgba(0, 0, 0, 0.02);
--scrollbar-thumb: rgba(154, 142, 114, 0.3);
--scrollbar-thumb-hover: rgba(154, 142, 114, 0.5);
--scrollbar-thumb-active: rgba(154, 142, 114, 0.7);
--scrollbar-corner: #f8f5ed;
}
2 changes: 2 additions & 0 deletions skill_manager/application/mcp/adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,8 @@ def _load_document(self, config_path: Path) -> dict[str, object]:
if not config_path.is_file():
return {}
text = config_path.read_text(encoding="utf-8")
if not text.strip():
return {}
if self._file_format in {"json", "jsonc"}:
try:
payload = json.loads(_strip_jsonc(text) if self._file_format == "jsonc" else text)
Expand Down
Loading