feat: v2.7.6 — edit users in one form, Settings → Account, display names, copy over plain HTTP
- Users page: one Edit user form (display name, email, role, active, a new or generated password, workspaces given or taken back, sign out everywhere), validated as a whole before anything is written (PATCH /api/users/:id, GET /api/users/:id); users.display_name/email added migration-safe. - Settings → Account for every signed-in user: profile, own password (the current one required, this login kept), sign out other devices (/api/me/account, /api/me/password, /api/me/sign-out-others; login cookie only, never a pane token). - Display names wherever a person is named (sidebar, workspace owner groups, Users page and audit log); owner_display_name / reader_display additive. - Users page: admins add and remove Claude/Codex pool accounts. - Folder browser: an admin with their own Linux account opens in their home; the "Server folder" tick opens the host's (never remembered). - AI Proxy: "Config 9router" beside Add profile when 9router is installed, opened on the dashboard's own host (GET /api/ai-proxies/9router). - Terminal: a tmux selection reaches the clipboard over plain HTTP too (writeClipboard's textarea fallback, focus handed back). - Chat: a file the session touched outside the workspace previews (GET /api/sessions/:id/file). - install-system: `!requiretty` only when visudo accepts it (sudo-rs on Ubuntu 25.10+ rejects the setting). - openapi.yaml regenerated; docs (API, SERVER-MODE, LANES) updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
* pane's folder through the lane's git file read, a memory file under
|
||||
* `~/.claude` through the config pages' read, a dropped text file through the
|
||||
* drop route, an image read outside the folder through the session's image
|
||||
* route, a Windows path like a POSIX one, anything else by path only; the
|
||||
* route, a text file read or written outside it through the session's file route, a Windows path like a POSIX one, anything else by path only; the
|
||||
* list dialog previewing the file picked in it; and the skills dialog finding
|
||||
* each skill's SKILL.md by name.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
@@ -31,7 +31,14 @@ vi.mock("../../../lib/api", async (importOriginal) => {
|
||||
}),
|
||||
gitRawUrl: () => "",
|
||||
},
|
||||
sessions: { readImageUrl: (id: string, p: string) => `/img/${id}?path=${p}` },
|
||||
sessions: {
|
||||
readImageUrl: (id: string, p: string) => `/img/${id}?path=${p}`,
|
||||
readFile: vi.fn((_id: string, p: string) =>
|
||||
p.endsWith(".md")
|
||||
? Promise.resolve({ text: "# Audit report", truncated: false })
|
||||
: Promise.reject(new Error("404"))
|
||||
),
|
||||
},
|
||||
ccConfig: {
|
||||
skills: vi.fn().mockResolvedValue({
|
||||
items: [{ scope: "user", name: "xlsx", file: "/h/.claude/skills/xlsx/SKILL.md" }],
|
||||
@@ -110,9 +117,16 @@ describe("FilePreview", () => {
|
||||
expect(screen.getByText("No preview for this file here")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("shows only the path of a file it may not read", () => {
|
||||
it("reads a text file it wrote outside the folder through the session", async () => {
|
||||
const report = "/home/a/Documents/audit/REPORT_ssh_ec2.md";
|
||||
render(<FilePreview path={report} scope={scope} onClose={() => {}} />);
|
||||
expect(await screen.findByText("Audit report")).toBeTruthy();
|
||||
expect(vi.mocked(api.sessions.readFile)).toHaveBeenCalledWith("s1", report);
|
||||
});
|
||||
|
||||
it("shows only the path of a file it may not read", async () => {
|
||||
render(<FilePreview path="/etc/hosts" scope={scope} onClose={() => {}} />);
|
||||
expect(screen.getByText("No preview for this file here")).toBeTruthy();
|
||||
expect(await screen.findByText("No preview for this file here")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -87,6 +87,7 @@ vi.mock("../../../lib/api", async (importOriginal) => {
|
||||
skills: [],
|
||||
mcp: [],
|
||||
}),
|
||||
readFile: r({ text: "", truncated: false }),
|
||||
},
|
||||
lanes: {
|
||||
gitFile: r({
|
||||
@@ -355,7 +356,9 @@ describe("PaneChat — the activity panel", () => {
|
||||
);
|
||||
const first = render(pane());
|
||||
fireEvent.click(
|
||||
within(await screen.findByTestId("chat-outline")).getByRole("button", { name: "Hide side panel" })
|
||||
within(await screen.findByTestId("chat-outline")).getByRole("button", {
|
||||
name: "Hide side panel",
|
||||
})
|
||||
);
|
||||
first.unmount();
|
||||
render(pane());
|
||||
|
||||
Reference in New Issue
Block a user