chore: v2.8.7 — Git SSH keys in tabs, safer Artifacts preview and search

- Settings → Git → Your git identity: SSH keys are one tab per host kind
  (GitHub · GitLab, AWS CodeCommit, More keys, Other hosts); opens on the
  AWS tab when only an RSA key exists.
- Artifacts: a file type the sandboxed preview cannot show (Office, zip,
  PDF) offers Download only, never a blank Preview.
- Artifacts search: `%`/`_`/`\` in a query match literally (LIKE ... ESCAPE).
- Skill frontmatter rewrite replaces a key with its continuation lines and
  quotes values YAML could misread.
- Plugin marketplace.json is read with a 1 MB streaming cap.
- Artifact lists look each owner up once.
This commit is contained in:
2026-10-15 17:17:47 +07:00
parent 4d6067650a
commit a58e95a957
16 changed files with 763 additions and 380 deletions
+43 -3
View File
@@ -13,6 +13,9 @@
* the server's sandboxed `raw` route in a sandboxed iframe, so nothing a
* shared HTML file carries runs on the dashboard's origin. Right-clicking a
* card offers the same actions as its buttons, plus copying a page's link.
* Preview is offered only for what a browser shows inside that sandbox (text,
* images, HTML, audio/video); an Office file, a zip or a PDF is downloaded —
* the sandbox blocks both a download and the PDF viewer, so it would be blank.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
@@ -63,6 +66,9 @@ function fmtBytes(n: number | null): string {
}
const IMAGE = /\.(png|jpe?g|gif|webp|svg)$/i;
// What the sandboxed preview can show: the server's typed list minus PDF.
const PREVIEWABLE =
/\.(html?|md|markdown|txt|log|csv|tsv|json|ya?ml|sh|py|js|ts|png|jpe?g|gif|webp|svg|mp4|webm|mp3|wav)$/i;
export function Artifacts() {
const { t } = useTranslation("artifacts");
@@ -73,6 +79,7 @@ export function Artifacts() {
const [items, setItems] = useState<Artifact[] | null>(null);
const [adding, setAdding] = useState<ArtifactKind | null>(null);
const [addMenu, setAddMenu] = useState(false);
const addMenuRef = useRef<HTMLDivElement>(null);
const [preview, setPreview] = useState<{ item: Artifact; path: string } | null>(null);
useEffect(() => {
@@ -105,6 +112,23 @@ export function Artifacts() {
[load]
);
// The Add menu closes on Escape or a click anywhere outside it.
useEffect(() => {
if (!addMenu) return;
const onDown = (e: MouseEvent) => {
if (!addMenuRef.current?.contains(e.target as Node)) setAddMenu(false);
};
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") setAddMenu(false);
};
document.addEventListener("mousedown", onDown);
document.addEventListener("keydown", onKey);
return () => {
document.removeEventListener("mousedown", onDown);
document.removeEventListener("keydown", onKey);
};
}, [addMenu]);
const server = !!settings?.serverMode;
const share = async (a: Artifact) => {
@@ -186,11 +210,13 @@ export function Artifacts() {
<p className="text-sm text-fg-muted">{t(server ? "subtitleServer" : "subtitle")}</p>
</div>
<div className="flex items-center gap-3">
<div className="relative">
<div className="relative" ref={addMenuRef}>
<button
type="button"
className="btn-primary flex items-center gap-1.5 text-sm"
data-testid="artifacts-add"
aria-haspopup="menu"
aria-expanded={addMenu}
onClick={() => setAddMenu((v) => !v)}
>
<Plus className="h-4 w-4" />
@@ -334,7 +360,9 @@ function ArtifactCard({
fn();
};
const previewPath = a.kind === "skill" ? "SKILL.md" : (a.files[0]?.path ?? "");
const canPreview = (a.kind === "file" || a.kind === "skill") && a.files.length > 0;
const canPreview =
a.files.length > 0 &&
(a.kind === "skill" || (a.kind === "file" && PREVIEWABLE.test(previewPath)));
const btn =
"flex items-center gap-1 rounded px-1.5 py-0.5 text-[11px] text-fg-secondary hover:bg-surface-3 hover:text-fg-primary";
return (
@@ -598,6 +626,7 @@ function PreviewDialog({
path: string;
onClose: () => void;
}) {
const { t } = useTranslation("artifacts");
const [current, setCurrent] = useState(path);
const url = api.artifacts.rawUrl(item.id, current);
return (
@@ -624,7 +653,18 @@ function PreviewDialog({
</ul>
)}
<div className="min-w-0 flex-1 rounded border border-border bg-white">
{IMAGE.test(current) && !/\.svg$/i.test(current) ? (
{!PREVIEWABLE.test(current) ? (
<div className="flex h-full flex-col items-center justify-center gap-3 bg-surface-1 p-6 text-center">
<p className="text-sm text-fg-secondary">{t("noPreview")}</p>
<a
href={api.artifacts.rawUrl(item.id, current, true)}
className="btn-primary flex items-center gap-1.5 text-sm"
>
<Download className="h-4 w-4" />
{t("download")}
</a>
</div>
) : IMAGE.test(current) && !/\.svg$/i.test(current) ? (
<img src={url} alt={current} className="h-full w-full object-contain" />
) : (
// No allow-* tokens: a shared page renders, nothing in it runs.