feat: Discard all, claude-style image pastes, large local drops, per-account / list

- Changes panel: **Discard all** beside Commit discards every non-conflicted
  file in one `discard`, after a dialog naming how many untracked files go.
- Terminal: a `claude` pane gets dropped/pasted images as bare paths (shown as
  `[Image #n]`) and other files as `@` mentions (`claudePastes`); a local
  browser's copied `file://` paths and text-drag file lists paste by path.
- `POST /api/run/drop-file`: a loopback browser outside server mode may drop
  up to 2 GB, streamed to disk; remote stays 20 MB.
- cc-config skills/commands/plugins read the USER scope of the account the
  pane runs as (S3 member home by `cwd`), never another user's or the host's
  for a non-admin without an account; the run form passes its cwd to plugins.
This commit is contained in:
2026-10-21 14:27:36 +07:00
parent 0547488f13
commit aee4fc190e
16 changed files with 380 additions and 65 deletions
+17 -8
View File
@@ -215,10 +215,15 @@ function readSkillsAt(scope, claudeDir) {
return skills.sort((a, b) => a.name.localeCompare(b.name));
}
/**
* @param {{scope?: string, cwd?: string, userClaudeDir?: string}} opts
* `userClaudeDir` = whose user scope to read (a member's `~/.claude` under
* S3); the host's Claude home when absent.
*/
function readSkills(opts = {}) {
const out = [];
if (opts.scope !== "project") {
out.push(...readSkillsAt("user", getClaudeHome()));
out.push(...readSkillsAt("user", opts.userClaudeDir || getClaudeHome()));
}
if (opts.scope !== "user") {
out.push(...readSkillsAt("project", getProjectClaudeDir(opts.cwd)));
@@ -257,7 +262,7 @@ function readSimpleMdSurface(subdir) {
return (opts = {}) => {
const out = [];
if (opts.scope !== "project") {
out.push(...readMdFilesAt("user", getClaudeHome(), subdir));
out.push(...readMdFilesAt("user", opts.userClaudeDir || getClaudeHome(), subdir));
}
if (opts.scope !== "user") {
out.push(...readMdFilesAt("project", getProjectClaudeDir(opts.cwd), subdir));
@@ -423,18 +428,18 @@ function readPluginContributions(installPath) {
};
}
function readEnabledPluginsMap() {
const userSettings = readJson(path.join(getClaudeHome(), "settings.json"));
function readEnabledPluginsMap(home = getClaudeHome()) {
const userSettings = readJson(path.join(home, "settings.json"));
if (!userSettings.ok || !userSettings.data) return {};
const ep = userSettings.data.enabledPlugins;
return ep && typeof ep === "object" ? ep : {};
}
function readPlugins() {
const home = getClaudeHome();
/** @param {string} [home] whose Claude home (a member's under S3); the host's by default. */
function readPlugins(home = getClaudeHome()) {
const manifestPath = path.join(home, "plugins", "installed_plugins.json");
const manifest = readJson(manifestPath);
const enabledMap = readEnabledPluginsMap();
const enabledMap = readEnabledPluginsMap(home);
const plugins = [];
if (manifest.ok && manifest.data && manifest.data.plugins) {
for (const [pluginKey, instances] of Object.entries(manifest.data.plugins)) {
@@ -847,7 +852,11 @@ function isSecretConfigPath(resolved) {
const base = path.basename(resolved);
if (!base.endsWith(".md") && require("./ai-tools").CONFIG_SECRET_RE.test(base)) return true;
const rel = path.relative(getClaudeHome(), resolved);
if (!rel.startsWith("..") && !path.isAbsolute(rel) && rel.split(path.sep)[0].startsWith("agenthub-"))
if (
!rel.startsWith("..") &&
!path.isAbsolute(rel) &&
rel.split(path.sep)[0].startsWith("agenthub-")
)
return true;
// AgentHub's data dir is no Claude config: it holds the database, the push
// keys and every gateway pane's settings file WITH its live pane token.