diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 79abcff6..ac5c95df 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -346,7 +346,7 @@ graph TD | `server/routes/remote-sources.js` | HTTP surface for **Remote Data Sources**: `GET /api/remote-sources` (list), `POST /api/remote-sources` (create), `PATCH /api/remote-sources/:id`, `DELETE /api/remote-sources/:id` (`?purge=true` also deletes that source's imported sessions), `POST /api/remote-sources/:id/test` (SSH connectivity probe), and `POST /api/remote-sources/:id/sync` (on-demand pull). Delegates the pull/validation to `remote-sync.js`; broadcasts `remote_source.status` on every transition | | `server/lib/source-filter.js` | Parses the optional `?sources=` query param (comma-separated source ids) into SQL predicates + bind params, shared by the data endpoints (`GET /api/sessions`, `/api/events`, `/api/agents`, `/api/stats`, `/api/analytics`) so a client **data-scope** selection narrows every query consistently. No filter → the existing unscoped queries run unchanged | | `server/lib/scoped-stats.js` | Source-scoped variants of the stats/analytics aggregates, used **only** when a `?sources=` scope filter is active — the unscoped fast paths in `routes/stats.js` / `routes/analytics.js` are untouched when no scope is set | -| `lib/cc-discovery.js` | Read-only discovery of every Claude Code config surface for the Config Explorer page. Pure file reads; never writes. Surfaces: skills (`/skills//SKILL.md`), subagents (`/agents/*.md`), slash commands (`/commands/*.md`), output styles (`/output-styles/*.md`), plugins (`/plugins/installed_plugins.json` joined with `enabledPlugins` in settings + per-plugin `contributes` count by scanning the install dir + `plugin.json` metadata), marketplaces (`known_marketplaces.json` enriched with each `marketplace.json`), MCP servers (top-level + per-project from `~/.claude.json`), hooks (across user / project / project-local settings.json), keybindings (`/keybindings.json`), statusline config + `statusline.py` / `statusline-command.sh` content, hook scripts dir (`/hooks/`), settings (with secret-key redaction matching `/token\|secret\|password\|api[_-]?key\|auth/i`), memory (`CLAUDE.md` at user + project **plus** the per-project file-based auto-memory store — every `*.md` under `~/.claude/projects//memory/`, returned as `scope:"auto-memory"` items carrying `project`, `name`, `isIndex`, and parsed `frontmatter`, so a `MEMORY.md` index and one file per remembered fact, often 100+, all surface). Path containment via `isUnder()` — every read must resolve under CLAUDE_HOME, project `.claude/`, or be a project CLAUDE.md. 256 KB read cap. Minimal YAML frontmatter parser handles `key: value` + quoted strings + indented continuation lines | +| `lib/cc-discovery.js` | Read-only discovery of every Claude Code config surface for the Config Explorer page. Pure file reads; never writes. Surfaces: skills (`/skills//SKILL.md`), subagents (`/agents/*.md`), slash commands (`/commands/*.md`), output styles (`/output-styles/*.md`), plugins (`/plugins/installed_plugins.json` joined with `enabledPlugins` in settings + per-plugin `contributes` counts **and** `contributes.items`, which names every skill / subagent / command / output style the plugin ships — each surface read from the conventional install subdirectory *and* from whatever path `plugin.json` declares for that key, with declared paths outside the plugin's own install dir dropped by `isUnder()`, so a plugin that lists its commands explicitly and keeps `commands/` empty no longer reports zero + `plugin.json` metadata), marketplaces (`known_marketplaces.json` enriched with each `marketplace.json`), MCP servers (top-level + per-project from `~/.claude.json`), hooks (across user / project / project-local settings.json), keybindings (`/keybindings.json`), statusline config + `statusline.py` / `statusline-command.sh` content, hook scripts dir (`/hooks/`), settings (with secret-key redaction matching `/token\|secret\|password\|api[_-]?key\|auth/i`), memory (`CLAUDE.md` at user + project **plus** the per-project file-based auto-memory store — every `*.md` under `~/.claude/projects//memory/`, returned as `scope:"auto-memory"` items carrying `project`, `name`, `isIndex`, and parsed `frontmatter`, so a `MEMORY.md` index and one file per remembered fact, often 100+, all surface). Path containment via `isUnder()` — every read must resolve under CLAUDE_HOME, project `.claude/`, or be a project CLAUDE.md. 256 KB read cap. Minimal YAML frontmatter parser handles `key: value` + quoted strings + indented continuation lines | | `lib/cc-mutate.js` | Create / overwrite / delete for the **low-risk text-file surfaces only** (skills, subagents, slash commands, output styles, memory — including the per-project file-based auto-memory store, mutated via `scope: "auto-memory"`, `type: "auto-memory"`, `project`, `name`, with its backups landing in `/.cc-config-backups/auto-memory/`), plus `writeKeybindings()` for the structured `keybindings.json` editor (read-modify-write that preserves top-level metadata, rejects duplicate contexts/keys, and backs up to `/cc-config-backups/keybindings/`). Plugins, MCP, hooks-in-settings, and `settings.json` files are NEVER written from here — they have concurrent-write races with the live Claude Code CLI. Every mutation creates a timestamped backup at `/cc-config-backups//..bak[.dir]` BEFORE the change — backups land outside the directories Claude Code scans, so a deleted skill cannot resurface as a backup-named one. Writes are atomic: temp file in same dir → fsync → `renameSync`. Tmp removed on every failure path. Skill dirs are backed up whole (preserving bundled assets) before recursive removal. Strict `name` regex (`^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$`), 256 KB content cap, double-checked path containment via `isUnder()` | | `routes/cc-config.js` | HTTP surface for the Claude Config Explorer. Read endpoints for every surface (skills, agents, commands, output-styles, plugins, marketplaces, mcp, hooks, hook-scripts, keybindings, statusline, settings, memory, file, overview), plus mutation endpoints (`PUT /file`, `DELETE /file`, and a structured `PUT /keybindings`) that delegate to `cc-mutate.js`, plus a `GET /backups` listing for the recovery modal. After every successful PUT/DELETE the route broadcasts `cc_config_changed` over the WebSocket so any open `/cc-config` tab refetches without polling. All errors return structured `{error: {code, message}}` shapes mapped to 400/404/413/500 statuses | | `lib/cc-watcher.js` | Best-effort `fs.watch` over `~/.claude/` (recursive where the platform / Node version honors it — macOS / Windows always; Linux from Node 20) plus `~/.claude.json`. Coalesces bursts at 500 ms and broadcasts `cc_config_changed` with `{ source: "fs", paths: [...] }` so the Config Explorer picks up changes from external tools (CLI installs a plugin, manual `settings.json` edits, dropping a new skill) without a manual refresh. Started from `server/index.js` after the HTTP server boots; failures are caught and logged so a flaky watcher can't take the server down | diff --git a/client/src/lib/api.ts b/client/src/lib/api.ts index be59370f..eaa4ccaa 100644 --- a/client/src/lib/api.ts +++ b/client/src/lib/api.ts @@ -2288,6 +2288,18 @@ export interface CcMdItem { preview: string; } +/** One named artifact a plugin contributes — a skill, subagent, command or + * output style. `name` is the frontmatter name Claude Code addresses it by + * (`/ccam:ship-feature`, `subagent_type: qc-local`), falling back to the + * filename when the file declares none. */ +export interface CcPluginItem { + name: string; + /** Frontmatter `description`; null when the file declares none. */ + description: string | null; + /** Absolute path of the backing `.md` file. */ + file: string; +} + /** Counts of what a plugin contributes to Claude Code, plus its manifest * metadata, embedded in {@link CcPlugin}. */ export interface CcPluginContributions { @@ -2296,6 +2308,14 @@ export interface CcPluginContributions { commands: number; outputStyles: number; hooks: number; + /** The same surfaces by name, so a card can list WHICH artifacts a plugin + * ships rather than only how many. Counts above are these lists' lengths. */ + items: { + skills: CcPluginItem[]; + agents: CcPluginItem[]; + commands: CcPluginItem[]; + outputStyles: CcPluginItem[]; + }; /** Parsed `plugin.json` fields; null if the plugin has no manifest. */ pluginJson: { name?: string; diff --git a/client/src/pages/CcConfig.tsx b/client/src/pages/CcConfig.tsx index 767f295e..9791960e 100644 --- a/client/src/pages/CcConfig.tsx +++ b/client/src/pages/CcConfig.tsx @@ -114,6 +114,7 @@ import type { CcMutationResult, CcOverview, CcPlugin, + CcPluginItem, CcPluginsResponse, CcScope, CcSettingsSource, @@ -638,7 +639,19 @@ function Header({ className="inline-flex items-center gap-2 rounded-lg border border-border bg-surface-2 px-3 py-1.5 text-xs font-medium text-fg-secondary hover:bg-surface-3 disabled:opacity-60 transition-colors" > - {loading ? t("refreshing") : t("refresh")} + {/* Both labels occupy the same grid cell, so the button is always as + wide as the longer one ("Refreshing…" / "Đang làm mới…") and does + not resize the toolbar when the state flips. Measuring the widest + string beats a hardcoded min-width, which would be wrong in the + next translation. */} + + + {t("refreshing")} + + + {loading ? t("refreshing") : t("refresh")} + + {lastUpdated && ( @@ -792,6 +805,7 @@ function Tabs({ current, onSelect, counts }: TabsProps) { return ( + ) : ( + + {c.label} + + ) + )} + {opened && ( +
    + {opened.items?.map((item) => ( +
  • + + {opened.key === "commands" ? `/${item.name}` : item.name} + + {item.description && ( +

    + {item.description} +

    + )} +
  • + ))} +
+ )} )}
diff --git a/client/src/pages/__tests__/__snapshots__/screens.snapshot.test.tsx.snap b/client/src/pages/__tests__/__snapshots__/screens.snapshot.test.tsx.snap index 2262c0ca..c4b5625b 100644 --- a/client/src/pages/__tests__/__snapshots__/screens.snapshot.test.tsx.snap +++ b/client/src/pages/__tests__/__snapshots__/screens.snapshot.test.tsx.snap @@ -850,7 +850,21 @@ exports[`screen snapshots > Claude Config 1`] = ` d="M8 16H3v5" /> - Refresh + + + + Refresh + +
Claude Config 1`] = `