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
@@ -0,0 +1,20 @@
/**
* @file promptAttach.test.ts — `claudePastes`: how files reach a `claude` pane.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import { describe, expect, it } from "vitest";
import { claudePastes } from "../promptAttach";
describe("claudePastes", () => {
it("pastes each image as its bare path and the other files as one @ mention paste", () => {
expect(
claudePastes(["/d/drops/a.png", "/d/drops/b-report.pdf", "/d/x.JPG", "/d/c.log"])
).toEqual(["/d/drops/a.png", "/d/x.JPG", "@/d/drops/b-report.pdf @/d/c.log"]);
});
it("keeps a Windows or spaced image path a mention", () => {
expect(claudePastes(["C:\\u\\drops\\a.png", "/home/u/my shot.png"])).toEqual([
'@C:/u/drops/a.png @"/home/u/my shot.png"',
]);
});
});
+2 -1
View File
@@ -2210,7 +2210,8 @@ export const api = {
* each one contributes (skills/agents/commands/hooks counts).
* @returns {@link CcPluginsResponse} — the manifest path/status plus plugins.
*/
plugins: () => request<CcPluginsResponse>("/cc-config/plugins"),
plugins: (cwd?: string) =>
request<CcPluginsResponse>(`/cc-config/plugins${ccQuery(undefined, cwd)}`),
/**
* GET /api/cc-config/mcp - configured MCP servers, user and project-scoped.
* @returns {@link CcMcpResponse} — servers split into `user`/`projectScoped`.
+16
View File
@@ -169,6 +169,22 @@ export function claudeMention(path: string): string {
return /\s/.test(p) ? `@"${p}"` : `@${p}`;
}
const IMAGE_PATH = /\.(png|jpe?g|gif|webp)$/i;
/** What a terminal pastes for files dropped or pasted on a `claude` pane, one
* paste per entry, as a local terminal hands them over: the images as their
* bare paths, each a paste of its own — the shape Claude Code turns into
* `[Image #n]`, as it does a native drop — and every other file as an `@`
* mention, together in one. A Windows path, or
* one with a space, stays a mention — the bare path stayed plain text there
* (see `claudeMention`). */
export function claudePastes(paths: string[]): string[] {
const bare = (p: string) => IMAGE_PATH.test(p) && p.startsWith("/") && !/\s/.test(p);
const images = paths.filter(bare);
const others = paths.filter((p) => !bare(p)).map(claudeMention);
return [...images, others.join(" ")].filter(Boolean);
}
export const SKILL_MIME = "application/x-agenthub-skill";
// A skill's name as Claude Code types it: `name` or `plugin:name`.
const SKILL_RE = /^[\w.-]+(?::[\w.-]+)?$/;