chore: v2.8.15 — Oh My Pi login sharing, omp --allow-home, Markdown Preview default, first-login password
- omp-share.js: an admin's switch (S3) runs omp's own auth broker + gateway as the host on loopback; a member's omp pane reaches it as a scoped client keyed by its pane token (clients file = live omp panes only), runs on --model agenthub/<model>, never holds a credential. - omp catalog: --allow-home, so a workspace at the account's home keeps its sessions there (they were recorded under a temp dir and dropped). - Settings → Appearance → Open Markdown as: Preview (default) or Raw, for Files/Changes, the config viewer and a chat's file panel. - First-login password change and password rules, avatar URLs, Artifacts "Mine" includes installed shared skills/plugins, a member's own hook pages.
This commit is contained in:
+254
-417
File diff suppressed because it is too large
Load Diff
@@ -69,6 +69,7 @@ import { AiToolsSettings } from "../components/settings/AiToolsSettings";
|
||||
import { AgentInstructionsSettings } from "../components/settings/AgentInstructionsSettings";
|
||||
import { SessionDefaultsSettings } from "../components/settings/SessionDefaultsSettings";
|
||||
import { ArtifactsHubSettings } from "../components/settings/ArtifactsHubSettings";
|
||||
import { OmpSharingSettings } from "../components/settings/OmpSharingSettings";
|
||||
import { PricingSettings } from "../components/settings/PricingSettings";
|
||||
import { WorktreeVisibilitySettings } from "../components/settings/WorktreeVisibilitySettings";
|
||||
import { useAiTools } from "../hooks/useAiTools";
|
||||
@@ -297,6 +298,8 @@ export function SettingsHub({ section }: { section?: string } = {}) {
|
||||
<AiToolsSettings settingsHref={toolHref} />
|
||||
{/* The Hub's install switch: server mode, an admin's (it hides itself). */}
|
||||
<ArtifactsHubSettings />
|
||||
{/* The host's omp login for members: server mode + S3, an admin's. */}
|
||||
<OmpSharingSettings />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
* account — moved to the Server tab, `MemberTmuxCard` in `ServerPanel`.)
|
||||
* Every change asks for the admin's OWN password again (the
|
||||
* server refuses without it); an analyst only reads. A row shows the display
|
||||
* name and email under the username when set; right-clicking a row offers
|
||||
* name and email under the username when set, and "New password due" while
|
||||
* the account still has a password an admin gave; right-clicking a row offers
|
||||
* Edit, Open home (in /files), and copying the username or email.
|
||||
* Laid out like the other list pages: icon header, a row of totals
|
||||
* (`StatCard`), the accounts in a `card` table, the audit log in a card.
|
||||
@@ -273,6 +274,15 @@ export function Users({ embedded = false }: { embedded?: boolean } = {}) {
|
||||
{t("disabled")}
|
||||
</span>
|
||||
)}
|
||||
{!u.disabled && u.mustChangePassword && (
|
||||
<span
|
||||
className="badge py-0.5 text-[11px] bg-status-warning/10 text-status-warning border-status-warning/20"
|
||||
title={t("mustChangeHint")}
|
||||
data-testid="user-must-change"
|
||||
>
|
||||
{t("mustChange")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-5 py-3">
|
||||
|
||||
@@ -227,6 +227,66 @@ describe("Artifacts", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("splits the Hub and My library into yours and others'", async () => {
|
||||
vi.mocked(api.artifacts.settings).mockResolvedValue({
|
||||
serverMode: true,
|
||||
canShare: true,
|
||||
installsEnabled: true,
|
||||
canManage: false,
|
||||
});
|
||||
vi.mocked(api.artifacts.list).mockResolvedValue({
|
||||
items: [
|
||||
item({ id: 1, title: "my-page", shared: true }),
|
||||
item({
|
||||
id: 2,
|
||||
kind: "skill",
|
||||
title: "their-skill",
|
||||
mine: false,
|
||||
owner: "an",
|
||||
shared: true,
|
||||
}),
|
||||
],
|
||||
});
|
||||
const user = userEvent.setup();
|
||||
open();
|
||||
// All is split too, others' first.
|
||||
const allOthers = await screen.findByTestId("half-all.others");
|
||||
expect(within(allOthers).getByText("their-skill")).toBeInTheDocument();
|
||||
expect(
|
||||
allOthers.compareDocumentPosition(screen.getByTestId("half-all.own")) &
|
||||
Node.DOCUMENT_POSITION_FOLLOWING
|
||||
).toBeTruthy();
|
||||
await user.click(await screen.findByRole("button", { name: "Hub" }));
|
||||
const own = await screen.findByTestId("half-hub.own");
|
||||
expect(own).toHaveTextContent("Shared by me");
|
||||
expect(within(own).getByText("my-page")).toBeInTheDocument();
|
||||
const others = screen.getByTestId("half-hub.others");
|
||||
expect(within(others).getByText("their-skill")).toBeInTheDocument();
|
||||
expect(within(others).queryByText("my-page")).toBeNull();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "My library" }));
|
||||
expect(await screen.findByTestId("half-mine.others")).toHaveTextContent(
|
||||
"Installed from others"
|
||||
);
|
||||
expect(within(screen.getByTestId("half-mine.own")).getByText("my-page")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("pages a long section 12 cards at a time", async () => {
|
||||
vi.mocked(api.artifacts.list).mockResolvedValue({
|
||||
items: Array.from({ length: 14 }, (_, i) =>
|
||||
item({ id: i + 1, kind: "page", title: `p${i + 1}`, files: [] })
|
||||
),
|
||||
});
|
||||
const user = userEvent.setup();
|
||||
open();
|
||||
await screen.findByText("p1");
|
||||
expect(screen.queryByText("p13")).toBeNull();
|
||||
expect(screen.getByText("Showing 1-12 of 14")).toBeInTheDocument();
|
||||
await user.click(screen.getByRole("button", { name: "Next" }));
|
||||
expect(screen.getByText("p13")).toBeInTheDocument();
|
||||
expect(screen.queryByText("p1")).toBeNull();
|
||||
});
|
||||
|
||||
it("lists who installed an artifact, when and where, from its install count", async () => {
|
||||
vi.mocked(api.artifacts.list).mockResolvedValue({
|
||||
items: [item({ id: 2, kind: "skill", title: "deploy", source: "deploy", installs: 3 })],
|
||||
@@ -287,13 +347,13 @@ describe("Artifacts", () => {
|
||||
const card = await screen.findByTestId("artifact-2");
|
||||
expect(within(card).getByTestId("artifact-installed")).toHaveTextContent("Installed");
|
||||
expect(within(card).queryByRole("button", { name: "Install" })).toBeNull();
|
||||
|
||||
vi.mocked(confirmDialog).mockResolvedValueOnce(true);
|
||||
vi.mocked(api.artifacts.install).mockResolvedValueOnce({
|
||||
installed: "/h/.claude/skills/deploy",
|
||||
});
|
||||
await userEvent.setup().click(within(card).getByRole("button", { name: "Reinstall" }));
|
||||
await waitFor(() => expect(api.artifacts.install).toHaveBeenLastCalledWith(2, true));
|
||||
// No Reinstall anywhere, and Uninstall lives only in the ⋯ menu.
|
||||
expect(within(card).queryByRole("button", { name: "Reinstall" })).toBeNull();
|
||||
expect(within(card).queryByRole("button", { name: "Uninstall" })).toBeNull();
|
||||
await userEvent.setup().click(within(card).getByRole("button", { name: "More actions" }));
|
||||
const menu = screen.getByTestId("artifact-menu");
|
||||
expect(within(menu).queryByRole("menuitem", { name: "Reinstall" })).toBeNull();
|
||||
expect(within(menu).getByRole("menuitem", { name: "Uninstall" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("uninstalls an installed plugin only after a confirmation", async () => {
|
||||
@@ -307,7 +367,13 @@ describe("Artifacts", () => {
|
||||
const card = await screen.findByTestId("artifact-4");
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(confirmDialog).mockResolvedValueOnce(false);
|
||||
await user.click(within(card).getByRole("button", { name: "Uninstall" }));
|
||||
const uninstall = async () => {
|
||||
await user.click(within(card).getByRole("button", { name: "More actions" }));
|
||||
await user.click(
|
||||
within(screen.getByTestId("artifact-menu")).getByRole("menuitem", { name: "Uninstall" })
|
||||
);
|
||||
};
|
||||
await uninstall();
|
||||
await waitFor(() => expect(confirmDialog).toHaveBeenCalled());
|
||||
expect(api.artifacts.uninstall).not.toHaveBeenCalled();
|
||||
|
||||
@@ -316,7 +382,7 @@ describe("Artifacts", () => {
|
||||
uninstalled: "tool@market",
|
||||
paths: [],
|
||||
});
|
||||
await user.click(within(card).getByRole("button", { name: "Uninstall" }));
|
||||
await uninstall();
|
||||
await waitFor(() => expect(api.artifacts.uninstall).toHaveBeenCalledWith(4));
|
||||
});
|
||||
|
||||
|
||||
@@ -7239,7 +7239,7 @@ exports[`screen snapshots > Settings — about 1`] = `
|
||||
<span
|
||||
class="text-status-warning/90"
|
||||
>
|
||||
UI build v2.8.14 (rebuild client to match)
|
||||
UI build v2.8.15 (rebuild client to match)
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user