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:
2026-10-22 10:04:30 +07:00
parent 8a6dc6a29e
commit 750d74a2af
66 changed files with 3021 additions and 696 deletions
File diff suppressed because it is too large Load Diff
+3
View File
@@ -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>
),
},
+11 -1
View File
@@ -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">
+75 -9
View File
@@ -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>