From b05817917756e1f0a930fea58338f237fe7fc918 Mon Sep 17 00:00:00 2001 From: nntrivi2001 Date: Thu, 15 Oct 2026 16:42:47 +0700 Subject: [PATCH] feat: right-click menus in Files/Users/Artifacts; several SSH keys and accounts per host MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Files: right-click a sidebar workspace (Open, Copy path) or home (Open, Open Trash), a path crumb (Open, Download as zip, Paste into, Copy path), a Trash row (Restore, Copy path, Delete forever), the Trash area (Refresh, Empty Trash) and an admin's per-user Trash folder (Open, Empty Trash). - Users: row menu (Edit + Open home in Files for an admin, Copy username/email). - Artifacts: card menu repeating its buttons, plus Copy link for a page. - Git identity: named SSH keys beside the default ones (id__, max 10, DELETE ?id=); SSH logins per alias (`ssh:///` → `ssh://@/`, so several AWS CodeCommit accounts share a host), each optionally tied to one key (`agenthub..sshKey`) — a clone of that URL offers only that key and persists it. Terminal sshCommand follows the keys. Settings → Git: "More SSH keys" + account name/key in the AWS and other-host forms. Docs/API.md, OpenAPI updated. --- client/src/components/lanes/FileExplorer.tsx | 179 +++++++++- .../lanes/__tests__/FileExplorer.test.tsx | 85 +++++ .../settings/GitIdentitySettings.tsx | 240 +++++++++++-- .../__tests__/GitIdentitySettings.test.tsx | 68 +++- client/src/i18n/locales/en/artifacts.json | 1 + client/src/i18n/locales/en/lanes.json | 1 + client/src/i18n/locales/en/settings.json | 16 +- client/src/i18n/locales/en/users.json | 5 + client/src/i18n/locales/vi/artifacts.json | 1 + client/src/i18n/locales/vi/lanes.json | 1 + client/src/i18n/locales/vi/settings.json | 16 +- client/src/i18n/locales/vi/users.json | 5 + client/src/lib/api.ts | 43 ++- client/src/pages/Artifacts.tsx | 62 +++- client/src/pages/Users.tsx | 51 ++- client/src/pages/__tests__/Artifacts.test.tsx | 30 +- client/src/pages/__tests__/Users.test.tsx | 31 +- docs/API.md | 12 +- docs/LANES.md | 2 +- openapi.yaml | 327 +++++++++++++++++- server/__tests__/git-clone.test.js | 132 ++++++- server/lib/git-identity.js | 255 +++++++++++--- server/openapi-extra/files.js | 43 ++- server/routes/git-identity.js | 30 +- 24 files changed, 1510 insertions(+), 126 deletions(-) diff --git a/client/src/components/lanes/FileExplorer.tsx b/client/src/components/lanes/FileExplorer.tsx index 6ed9bd0d..7387ea9d 100644 --- a/client/src/components/lanes/FileExplorer.tsx +++ b/client/src/components/lanes/FileExplorer.tsx @@ -102,6 +102,11 @@ type Clip = { place: FsPlace; paths: string[]; cut: boolean }; // The clipboard outlives the dialog and a workspace switch, like a desktop's. let clipboard: Clip | null = null; +/** A menu item's handler that closes its menu first. */ +const pick = (m: { close: () => void }, fn: () => void) => () => { + m.close(); + fn(); +}; /** The workspace id of a place, or null for a home. */ const laneOf = (t: FsPlace) => (isHomePlace(t) ? null : typeof t === "number" ? t : t.lane); /** A workspace's own folder (not a session worktree or an extra folder), or a home. */ @@ -279,6 +284,9 @@ export function FileBrowser({ const fileInput = useRef(null); const folderInput = useRef(null); const ctx = useContextMenu(); + // The sidebar's places and the path bar's crumbs have their own menus. + const placeCtx = useContextMenu<{ home: string } | { lane: Lane }>(); + const crumbCtx = useContextMenu(); // A new caller target (another workspace's Files tab, the page's URL) — // not the echo of our own `go()` through the page's URL, which would wipe @@ -668,6 +676,18 @@ export function FileBrowser({ const item = trash.find((i) => i.id === id); await run(() => api.files.purge(target, id, item?.store), t("fx.purged")); }; + /** Empty one user's whole Trash from the Trash root — asked first. */ + const emptyTrashOf = async (home: string, count: number) => { + const ok = await confirmDialog({ + title: t("fx.emptyTrashTitle"), + message: t("fx.emptyTrashBody", { + count, + where: home === "me" ? t("fx.myTrash") : t("fx.trashOf", { name: homeName(home) }), + }), + confirmLabel: t("fx.emptyTrash"), + }); + if (ok) await run(() => api.files.purge({ home }, "all"), t("fx.purged")); + }; const restoreItem = (id: string) => { const item = trash.find((i) => i.id === id); void run(() => api.files.restore(target, id, item?.store), t("fx.restored")); @@ -772,6 +792,7 @@ export function FileBrowser({ @@ -442,7 +507,7 @@ export function GitIdentitySettings() { ))} {id.writable && (
{ e.preventDefault(); void saveAws(); @@ -468,11 +533,22 @@ export function GitIdentitySettings() { ))} + setAws({ ...aws, alias: e.target.value })} + className={`${INPUT} font-mono`} + /> + {keySelect(aws.key, (key) => setAws({ ...aws, key }), "rsa")}
)} +
{t("gitIdentity.awsMany")}
)} @@ -480,12 +556,119 @@ export function GitIdentitySettings() {
{t("gitIdentity.awsReady")} {awsRows.map((r) => ( - git clone ssh://{r.host}/v1/repos/<repo> + + git clone ssh://{r.alias ?? r.host}/v1/repos/<repo> + ))}
)} + {(named.length > 0 || id.writable) && ( +
+
+
+
+ {t("gitIdentity.moreKeysTitle")} +
+
{t("gitIdentity.moreKeysSub")}
+
+ {id.writable && !newKey && ( + + )} +
+ {newKey && ( +
{ + e.preventDefault(); + void makeNamed(); + }} + > + setNewKey({ ...newKey, name: e.target.value })} + className={`${INPUT} font-mono`} + /> + + + +
+ )} + {named.map((k) => ( +
+
+ + {k.name} + + + {k.type === "rsa" ? "RSA" : "ED25519"} + + + {k.fingerprint} + + + {id.writable && ( + + )} +
+