diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b60e3d0ba..fc8c485ebe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## 0.11.0 - 2026-10-07 +Paseo 0.11 adds support for the [plugin registry](https://paseo.sh/plugins). Browse community plugins there, install one with `paseo plugin add owner/slug`, or [submit your own](https://paseo.sh/docs/plugins/publishing#plugin-registry). + +**Before upgrading:** `paseo plugin add owner/repo` now installs from the registry, so use `github:owner/repo` to install straight from GitHub and `./path` for a local directory. + ### Added - Added Muse Code as a provider, with model, approval mode, and reasoning effort controls ([#5719](https://github.com/getpaseo/paseo/pull/5719), [#5775](https://github.com/getpaseo/paseo/pull/5775) by [@millerben95](https://github.com/millerben95)) @@ -19,18 +23,21 @@ - Added syntax highlighting for Vue files ([#6205](https://github.com/getpaseo/paseo/pull/6205) by [@Davnn1](https://github.com/Davnn1)) - Added `agents.providers..options` in `config.json` as default options for every provider, with per-agent overrides ([#5780](https://github.com/getpaseo/paseo/pull/5780)) - Added the terminal key bar and Paste to terminal panes on iPad and Android tablets ([#5712](https://github.com/getpaseo/paseo/pull/5712) by [@lardissone](https://github.com/lardissone)) -- Added screens and sidebar header and footer items to the plugin SDK ([#5685](https://github.com/getpaseo/paseo/pull/5685)) -- Added a display name, icon, screenshots, and videos to the plugin manifest ([#6263](https://github.com/getpaseo/paseo/pull/6263)) -- Added an `OVERVIEW.md` template to `paseo plugin init` for plugin registry listings ([#6226](https://github.com/getpaseo/paseo/pull/6226)) + +### Plugins + +- Changed `paseo plugin add owner/slug` to install the reviewed plugin from the plugin registry, so local directories need a path such as `./slug` and direct GitHub installs need `github:owner/repo` ([#6224](https://github.com/getpaseo/paseo/pull/6224)) +- Added [screens and sidebar header and footer items](https://paseo.sh/docs/plugins/reference#screens-and-sidebar-items) to the plugin SDK ([#5685](https://github.com/getpaseo/paseo/pull/5685)) - Added usage sources to the plugin SDK ([#5465](https://github.com/getpaseo/paseo/pull/5465), [#5876](https://github.com/getpaseo/paseo/pull/5876)) - Added `spawnProcess()`, `execCommand()`, and `terminateProcess()` to the plugin SDK for launching CLIs, including Windows `.cmd` and `.bat` scripts ([#6151](https://github.com/getpaseo/paseo/pull/6151)) - Added `client.playAudio()` to the plugin SDK for playing audio on every client ([#5976](https://github.com/getpaseo/paseo/pull/5976)) - Added an `agent.closed` plugin lifecycle event ([#5971](https://github.com/getpaseo/paseo/pull/5971) by [@ilteoood](https://github.com/ilteoood)) - Added command and environment overrides and availability diagnostics for plugin providers ([#5707](https://github.com/getpaseo/paseo/pull/5707)) +- Added a display name, icon, screenshots, and videos to the plugin manifest ([#6263](https://github.com/getpaseo/paseo/pull/6263)) +- Added an `OVERVIEW.md` template to `paseo plugin init` for plugin registry listings ([#6226](https://github.com/getpaseo/paseo/pull/6226)) ### Changed -- Changed `paseo plugin add owner/slug` to install the reviewed plugin from the plugin registry, so local directories need a path such as `./slug` and direct GitHub installs need `github:owner/repo` ([#6224](https://github.com/getpaseo/paseo/pull/6224)) - Changed the context window details to show usage for the account the agent runs under, including custom `CLAUDE_CONFIG_DIR` and `CODEX_HOME` homes ([#5465](https://github.com/getpaseo/paseo/pull/5465)) - Changed the context window details on desktop to a hover card with Refresh ([#5975](https://github.com/getpaseo/paseo/pull/5975), [#6185](https://github.com/getpaseo/paseo/pull/6185)) - Changed the context window meter to show before an agent's first turn ([#6089](https://github.com/getpaseo/paseo/pull/6089)) diff --git a/packages/app/src/contexts/session-context.tsx b/packages/app/src/contexts/session-context.tsx index 3e8165146c..430e4ade74 100644 --- a/packages/app/src/contexts/session-context.tsx +++ b/packages/app/src/contexts/session-context.tsx @@ -50,7 +50,7 @@ import { useToast } from "@/contexts/toast-context"; import { toErrorMessage } from "@/utils/error-messages"; import { showProviderNoticeToast } from "@/utils/provider-notice-toast"; import { applyCheckoutStatusUpdateFromEvent } from "@/git/checkout-status-cache"; -import { useProviderSubagentStore } from "@/subagents/provider-store"; +import { resyncProviderSubagents, useProviderSubagentStore } from "@/subagents/provider-store"; // Re-export types from session-store and draft-store for backward compatibility export type { DraftInput } from "@/stores/draft-store"; @@ -532,9 +532,14 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider } }); - const unsubProviderSubagentUpdate = onFeed("agent.provider_subagents.update", (message) => { - if (message.type !== "agent.provider_subagents.update") return; - useProviderSubagentStore.getState().applyUpdate(serverId, message.payload); + const unsubProviderSubagentUpdate = feeds.subscribe({ + snapshot: () => { + void resyncProviderSubagents(client, serverId); + }, + update: (message) => { + if (message.type !== "agent.provider_subagents.update") return; + useProviderSubagentStore.getState().applyUpdate(serverId, message.payload); + }, }); const unsubCheckoutStatusUpdate = onFeed("checkout_status_update", (message) => { diff --git a/packages/app/src/subagents/provider-store.test.ts b/packages/app/src/subagents/provider-store.test.ts index 96badfd354..01d6880d5b 100644 --- a/packages/app/src/subagents/provider-store.test.ts +++ b/packages/app/src/subagents/provider-store.test.ts @@ -1,8 +1,11 @@ import { DaemonConnectionError } from "@getpaseo/client/internal/daemon-client"; import { afterEach, describe, expect, test } from "vitest"; +import type { ProviderSubagentDescriptorPayload } from "@getpaseo/protocol/messages"; import { observeProviderSubagentTimeline, providerSubagentKey, + refreshProviderSubagents, + resyncProviderSubagents, useProviderSubagentStore, } from "./provider-store"; @@ -15,6 +18,7 @@ afterEach(() => { descriptors: new Map(), timelines: new Map(), hiddenFromTrack: new Set(), + trackedParents: new Set(), }); }); @@ -727,3 +731,96 @@ describe("projected child history", () => { expect(current().lastSeq).toBe(2000); }); }); + +describe("resyncing after the update feed resubscribes", () => { + function child( + id: string, + parentAgentId: string, + status: ProviderSubagentDescriptorPayload["status"], + ): ProviderSubagentDescriptorPayload { + return { + id, + parentAgentId, + provider: "claude", + title: "Explore", + description: null, + status, + createdAt: "2026-10-07T10:00:00.000Z", + updatedAt: "2026-10-07T10:00:00.000Z", + toolCallId: id, + }; + } + + function fakeDaemon(lists: Record) { + const requested: string[] = []; + return { + requested, + lists, + client: { + listProviderSubagents: async (parentAgentId: string) => { + requested.push(parentAgentId); + return { + requestId: "list", + parentAgentId, + subagents: lists[parentAgentId] ?? [], + error: null, + }; + }, + }, + }; + } + + const status = (parentAgentId: string, subagentId: string) => + useProviderSubagentStore + .getState() + .descriptors.get(providerSubagentKey(SERVER_ID, parentAgentId, subagentId))?.status; + + test("catches up on children that finished or started while updates were not delivered", async () => { + const daemon = fakeDaemon({ [PARENT_ID]: [child("child-1", PARENT_ID, "running")] }); + await refreshProviderSubagents(daemon.client, SERVER_ID, PARENT_ID); + expect(status(PARENT_ID, "child-1")).toBe("running"); + + // The connection dropped: the daemon finished child-1 and ran child-2 without telling us. + daemon.lists[PARENT_ID] = [ + child("child-1", PARENT_ID, "completed"), + child("child-2", PARENT_ID, "completed"), + ]; + await resyncProviderSubagents(daemon.client, SERVER_ID); + + expect(status(PARENT_ID, "child-1")).toBe("completed"); + expect(status(PARENT_ID, "child-2")).toBe("completed"); + }); + + test("catches up on a parent whose first list failed while disconnected", async () => { + const daemon = fakeDaemon({}); + const disconnected = { + listProviderSubagents: async () => { + throw new DaemonConnectionError("Transport not connected"); + }, + }; + await expect( + refreshProviderSubagents(disconnected, SERVER_ID, PARENT_ID), + ).rejects.toBeInstanceOf(DaemonConnectionError); + useProviderSubagentStore.getState().applyUpdate(SERVER_ID, { + kind: "upsert", + subagent: child("child-1", PARENT_ID, "running"), + }); + + // The connection dropped again: the daemon finished child-1 without telling us. + daemon.lists[PARENT_ID] = [child("child-1", PARENT_ID, "completed")]; + await resyncProviderSubagents(daemon.client, SERVER_ID); + + expect(status(PARENT_ID, "child-1")).toBe("completed"); + }); + + test("lists again only the parents this server was showing", async () => { + const daemon = fakeDaemon({}); + await refreshProviderSubagents(daemon.client, SERVER_ID, PARENT_ID); + await refreshProviderSubagents(daemon.client, "server-2", "other-parent"); + daemon.requested.length = 0; + + await resyncProviderSubagents(daemon.client, SERVER_ID); + + expect(daemon.requested).toEqual([PARENT_ID]); + }); +}); diff --git a/packages/app/src/subagents/provider-store.ts b/packages/app/src/subagents/provider-store.ts index 86e857d774..fd44dae8d2 100644 --- a/packages/app/src/subagents/provider-store.ts +++ b/packages/app/src/subagents/provider-store.ts @@ -28,6 +28,9 @@ interface ProviderSubagentState { descriptors: Map; timelines: Map; hiddenFromTrack: Set; + /** Parents whose list this client asked for, so a resubscribed update feed can catch them up. */ + trackedParents: Set; + trackParent(serverId: string, parentAgentId: string): void; hideFromTrack(serverId: string, parentAgentId: string, subagentIds: readonly string[]): void; replaceList( serverId: string, @@ -84,6 +87,7 @@ export function refreshProviderSubagents( const pending = clientRequests.get(requestKey); if (pending) return pending; + useProviderSubagentStore.getState().trackParent(serverId, parentAgentId); const request = client .listProviderSubagents(parentAgentId) .then((payload) => { @@ -101,6 +105,27 @@ function parentPrefix(serverId: string, parentAgentId: string): string { return `${serverId}\0${parentAgentId}\0`; } +/** + * Updates are not replayed across a reconnect, so a child that finished or started while the + * update feed was down stays stale until its parent is listed again. Call this once the feed has + * resubscribed, so every update after the list is delivered. + */ +export async function resyncProviderSubagents( + client: ProviderSubagentListClient, + serverId: string, +): Promise { + const serverPrefix = `${serverId}\0`; + const parentAgentIds = [...useProviderSubagentStore.getState().trackedParents] + .filter((key) => key.startsWith(serverPrefix)) + .map((key) => key.slice(serverPrefix.length, -1)); + await Promise.all( + parentAgentIds.map((parentAgentId) => + // A parent that was archived meanwhile has nothing left to catch up. + refreshProviderSubagents(client, serverId, parentAgentId).catch(() => undefined), + ), + ); +} + const EMPTY_TIMELINE: ProviderSubagentTimelineState = { tail: [], head: [], @@ -147,6 +172,14 @@ export const useProviderSubagentStore = create((set) => ( descriptors: new Map(), timelines: new Map(), hiddenFromTrack: new Set(), + trackedParents: new Set(), + trackParent(serverId, parentAgentId) { + set((state) => { + const prefix = parentPrefix(serverId, parentAgentId); + if (state.trackedParents.has(prefix)) return state; + return { trackedParents: new Set(state.trackedParents).add(prefix) }; + }); + }, hideFromTrack(serverId, parentAgentId, subagentIds) { set((state) => { const hiddenFromTrack = new Set(state.hiddenFromTrack); diff --git a/packages/cli/tests/15-provider.test.ts b/packages/cli/tests/15-provider.test.ts index bc1183de81..3081ee3584 100644 --- a/packages/cli/tests/15-provider.test.ts +++ b/packages/cli/tests/15-provider.test.ts @@ -122,10 +122,15 @@ const EXPECTED_CLAUDE_MODELS = [ model: "Opus 4.6", descriptionFragment: "Most capable", }, + { + id: "claude-haiku-5-5", + model: "Haiku 5.5", + descriptionFragment: "Fastest", + }, { id: "claude-haiku-4-5", model: "Haiku 4.5", - descriptionFragment: "Fastest", + descriptionFragment: "Previous release", }, ] as const; @@ -435,8 +440,8 @@ try { "--quiet should print the same ordered model IDs returned by --json", ); assert( - claudeModelsFromJson.some((m) => m.id === "claude-sonnet-5-5"), - "captured --json output should include the current Claude everyday model id", + claudeModelsFromJson.some((m) => m.id === "claude-haiku-5-5"), + "captured --json output should include the current Claude quick-answer model id", ); console.log("✓ provider models --quiet outputs model IDs only\n"); } diff --git a/packages/server/src/server/agent/providers/claude/agent.sub-agent-sidechain.test.ts b/packages/server/src/server/agent/providers/claude/agent.sub-agent-sidechain.test.ts index efb452e9bf..84293afa6a 100644 --- a/packages/server/src/server/agent/providers/claude/agent.sub-agent-sidechain.test.ts +++ b/packages/server/src/server/agent/providers/claude/agent.sub-agent-sidechain.test.ts @@ -587,4 +587,75 @@ describe("ClaudeAgentSession sub-agent sidechain updates", () => { expect(latest.detail.log).toContain("[Read] file-6.md"); expect(latest.detail.log).toContain("[Read] file-205.md"); }); + + test("keeps a long main-thread tool call out of the subagents track", async () => { + // Claude Code sends a heartbeat every 30 s while a tool runs, with the tool's own id as + // parent_tool_use_id. Frames captured from Claude Code 2.1.291 for a 40 s MCP call. + queryFactory.mockImplementation(() => + buildQueryMock([ + { + type: "system", + subtype: "init", + session_id: "heartbeat-session", + permissionMode: "bypassPermissions", + model: "opus", + }, + { + type: "assistant", + parent_tool_use_id: null, + message: { + id: "msg-heartbeat-1", + role: "assistant", + content: [ + { type: "tool_use", id: "toolu_slow", name: "mcp__slow__slow_check", input: {} }, + ], + }, + }, + { + type: "tool_progress", + tool_use_id: "toolu_slow-heartbeat-0", + tool_name: "mcp__slow__slow_check", + parent_tool_use_id: "toolu_slow", + elapsed_time_seconds: 30, + heartbeat: true, + }, + { + type: "user", + parent_tool_use_id: null, + message: { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "toolu_slow", + content: [{ type: "text", text: "SLOW_CHECK_OK" }], + is_error: false, + }, + ], + }, + }, + { + type: "result", + subtype: "success", + usage: { input_tokens: 1, cache_read_input_tokens: 0, output_tokens: 1 }, + total_cost_usd: 0, + }, + ]), + ); + const session = await new ClaudeAgentClient({ + logger, + queryFactory, + resolveBinary: async () => "/test/claude/bin", + }).createSession({ provider: "claude", cwd: process.cwd() }); + + const events = await collectUntilTerminal(streamSession(session, "run the slow check")); + await session.close(); + + expect(events.filter((event) => event.type === "provider_subagent")).toEqual([]); + const toolCallNames = events.flatMap((event) => + event.type === "timeline" && event.item.type === "tool_call" ? [event.item.name] : [], + ); + expect(toolCallNames).toContain("mcp__slow__slow_check"); + expect(toolCallNames).not.toContain("Task"); + }); }); diff --git a/packages/server/src/server/agent/providers/claude/agent.test.ts b/packages/server/src/server/agent/providers/claude/agent.test.ts index 2d970c49de..9f7430b103 100644 --- a/packages/server/src/server/agent/providers/claude/agent.test.ts +++ b/packages/server/src/server/agent/providers/claude/agent.test.ts @@ -496,7 +496,7 @@ describe("ClaudeAgentClient.fetchCatalog", () => { const client = new ClaudeAgentClient({ logger, resolveBinary: async () => "/test/claude/bin", - resolveVersion: async () => "2.1.284", + resolveVersion: async () => "2.1.293", runtimeSettings: { env: { CLAUDE_CONFIG_DIR: emptyConfigDir } }, }); const { models } = await client.fetchCatalog({ @@ -515,6 +515,15 @@ describe("ClaudeAgentClient.fetchCatalog", () => { expect(getThinkingIds("claude-opus-4-8")).toContain("ultracode"); expect(getThinkingIds("claude-sonnet-5")).toContain("xhigh"); expect(getThinkingIds("claude-sonnet-5")).toContain("ultracode"); + expect(getThinkingIds("claude-haiku-5-5")).toEqual([ + "off", + "low", + "medium", + "high", + "xhigh", + "max", + "ultracode", + ]); expect(getThinkingIds("claude-sonnet-5-5")).toContain("xhigh"); expect(getThinkingIds("claude-sonnet-5-5")).not.toContain("off"); expect(getThinkingIds("claude-opus-4-7[1m]")).toContain("ultracode"); @@ -1223,6 +1232,7 @@ describe("ClaudeAgentSession features", () => { test.each([ ["supported model", "claude-opus-4-8", { type: "disabled" }, undefined], + ["Haiku 5.5", "claude-haiku-5-5", { type: "disabled" }, undefined], ["unsupported model", "claude-fable-5", { type: "adaptive", display: "summarized" }, "high"], ["custom model", "openrouter/anthropic/claude-opus-4-8", undefined, undefined], ["provider default", null, undefined, undefined], diff --git a/packages/server/src/server/agent/providers/claude/agent.ts b/packages/server/src/server/agent/providers/claude/agent.ts index 08640f472f..b797ce2c94 100644 --- a/packages/server/src/server/agent/providers/claude/agent.ts +++ b/packages/server/src/server/agent/providers/claude/agent.ts @@ -1916,6 +1916,11 @@ function readClaudeParentToolUseId(message: SDKMessage): string | null { if (!("parent_tool_use_id" in message)) { return null; } + // Claude Code's liveness tick for a tool still running after 30 s names that tool as its + // parent, so a long main-thread call would otherwise read as a subagent. + if (message.type === "tool_progress" && message.heartbeat === true) { + return null; + } const parentToolUseId = (message as { parent_tool_use_id?: unknown }).parent_tool_use_id; return typeof parentToolUseId === "string" && parentToolUseId.length > 0 ? parentToolUseId : null; } diff --git a/packages/server/src/server/agent/providers/claude/model-manifest.ts b/packages/server/src/server/agent/providers/claude/model-manifest.ts index e2e64f28fd..398fcb1ccd 100644 --- a/packages/server/src/server/agent/providers/claude/model-manifest.ts +++ b/packages/server/src/server/agent/providers/claude/model-manifest.ts @@ -170,10 +170,20 @@ export const CLAUDE_MODEL_MANIFEST = [ effortLevels: CLAUDE_EFFORT_LEVELS.standard, supportsThinkingDisabled: true, }, + { + id: "claude-haiku-5-5", + label: "Haiku 5.5", + description: "Haiku 5.5 · Fastest for quick answers", + minimumClaudeCodeVersion: "2.1.293", + defaultThinkingOptionId: "medium", + contextWindowMaxTokens: 1_000_000, + effortLevels: CLAUDE_EFFORT_LEVELS.xhigh, + supportsThinkingDisabled: true, + }, { id: "claude-haiku-4-5", label: "Haiku 4.5", - description: "Haiku 4.5 · Fastest for quick answers", + description: "Haiku 4.5 · Previous release", contextWindowMaxTokens: 200_000, }, ] as const satisfies readonly ClaudeModelManifestEntry[]; diff --git a/packages/server/src/server/agent/providers/claude/models.test.ts b/packages/server/src/server/agent/providers/claude/models.test.ts index 0e187734b8..ed7db8841c 100644 --- a/packages/server/src/server/agent/providers/claude/models.test.ts +++ b/packages/server/src/server/agent/providers/claude/models.test.ts @@ -39,7 +39,7 @@ async function createClaudeConfigDirWithRawSettings(settings: string): Promise claudeCodeVersion, @@ -66,6 +66,7 @@ describe("getClaudeModels", () => { "claude-opus-4-6", "claude-sonnet-4-6[1m]", "claude-sonnet-4-6", + "claude-haiku-5-5", "claude-haiku-4-5", ]); }); @@ -100,6 +101,7 @@ describe("getClaudeModels", () => { ["claude-opus-4-6", 200_000], ["claude-sonnet-4-6[1m]", 1_000_000], ["claude-sonnet-4-6", 200_000], + ["claude-haiku-5-5", 1_000_000], ["claude-haiku-4-5", 200_000], ]), ); @@ -123,6 +125,12 @@ describe("getClaudeModels", () => { expect(getClaudeModels("2.1.284").map((model) => model.id)).toContain("claude-sonnet-5-5"); }); + it("gates Haiku 5.5 on Claude Code 2.1.293 without changing the default", () => { + expect(getClaudeModels("2.1.292").map((model) => model.id)).not.toContain("claude-haiku-5-5"); + expect(getClaudeModels("2.1.293").map((model) => model.id)).toContain("claude-haiku-5-5"); + expect(getClaudeModels("2.1.293").find((model) => model.isDefault)?.id).toBe("claude-opus-5-5"); + }); + it("derives thinking options from model effort capabilities", () => { const models = new Map(getClaudeModels().map((model) => [model.id, model])); @@ -192,6 +200,8 @@ describe("getClaudeModels", () => { ["claude-sonnet-5-20260101", true, "high"], ["claude-fable-5", false, "high"], ["claude-fable-5-1", false, "high"], + ["claude-haiku-5-5", true, "medium"], + ["claude-haiku-5-5-20261007", true, "medium"], ["claude-haiku-4-5", false, undefined], ["openrouter/anthropic/claude-opus-4-8", false, undefined], [null, false, undefined], @@ -653,6 +663,51 @@ describe("Claude Sonnet 5.5 catalog", () => { }); }); +describe("Claude Haiku 5.5 catalog", () => { + it("offers one Haiku 5.5 entry with a 1M context window", () => { + const haiku55Models = getClaudeModels() + .filter((model) => model.id.startsWith("claude-haiku-5-5")) + .map(({ id, label, contextWindowMaxTokens }) => ({ id, label, contextWindowMaxTokens })); + + expect(haiku55Models).toEqual([ + { id: "claude-haiku-5-5", label: "Haiku 5.5", contextWindowMaxTokens: 1_000_000 }, + ]); + }); + + it("offers Off and every effort level with medium as the default", () => { + const haiku55 = getClaudeModels().find((model) => model.id === "claude-haiku-5-5"); + + expect(haiku55?.thinkingOptions?.map((option) => option.id)).toEqual([ + CLAUDE_DISABLED_THINKING_OPTION_ID, + "low", + "medium", + "high", + "xhigh", + "max", + CLAUDE_ULTRACODE_THINKING_OPTION_ID, + ]); + expect(haiku55?.defaultThinkingOptionId).toBe("medium"); + expect( + haiku55?.thinkingOptions?.filter((option) => option.isDefault).map((option) => option.id), + ).toEqual(["medium"]); + }); + + it("normalizes provider-prefixed runtime IDs", () => { + expect(normalizeClaudeRuntimeModelId("anthropic/claude-haiku-5-5")).toBe("claude-haiku-5-5"); + expect(normalizeClaudeRuntimeModelId("us.anthropic.claude-haiku-5-5-v1:0")).toBe( + "claude-haiku-5-5", + ); + expect(claudeManifestModelSupportsFastMode("claude-haiku-5-5")).toBe(false); + }); + + it("resolves suffixed and dated Haiku 5.5 IDs to the single catalog entry", () => { + expect(findClaudeModel("claude-haiku-5-5[1m]")?.id).toBe("claude-haiku-5-5"); + expect(findClaudeModel("claude-haiku-5-5-20261007")?.id).toBe("claude-haiku-5-5"); + expect(findClaudeModel("claude-haiku-5-5-20261007[1m]")?.id).toBe("claude-haiku-5-5"); + expect(findClaudeModel("claude-haiku-5-5[1m]")?.contextWindowMaxTokens).toBe(1_000_000); + }); +}); + describe("claudeManifestModelSupportsFastMode", () => { it("keeps fast mode strict to first-party manifest model IDs", () => { expect(normalizeClaudeManifestModelId("openrouter/anthropic/claude-opus-4-8")).toBeNull(); diff --git a/packages/website/e2e/plugins.spec.ts b/packages/website/e2e/plugins.spec.ts index 015d362338..26c0d9619a 100644 --- a/packages/website/e2e/plugins.spec.ts +++ b/packages/website/e2e/plugins.spec.ts @@ -388,19 +388,18 @@ async function expectPageMetadata(page: Page, title: string, path: string) { ); } -test("keeps the directory unlinked until the coordinated announcement", async ({ page }) => { +test("links the directory from the site navigation", async ({ page }) => { await page.goto("/"); await expect( - page.getByRole("banner").getByRole("link", { name: "Plugins", exact: true }), - ).toHaveCount(0); + page.getByRole("navigation").getByRole("link", { name: "Plugins", exact: true }), + ).toHaveAttribute("href", "/plugins"); await expect( page.getByRole("contentinfo").getByRole("link", { name: "Plugins", exact: true }), - ).toHaveCount(0); + ).toHaveAttribute("href", "/plugins"); await expect(page.getByRole("link", { name: "Browse plugins" })).toHaveAttribute( "href", - "https://paseo.cafe", + "/plugins", ); - await expect(page.locator('a[href="/plugins"]')).toHaveCount(0); const response = await page.goto("/plugins"); expect(response?.status()).toBe(200); await expect(page.getByRole("heading", { level: 1, name: /^Plugins/ })).toBeVisible(); diff --git a/packages/website/src/components/site-header.tsx b/packages/website/src/components/site-header.tsx index c691761b56..fc70e11f39 100644 --- a/packages/website/src/components/site-header.tsx +++ b/packages/website/src/components/site-header.tsx @@ -12,6 +12,14 @@ export function SiteHeader() { Paseo
+ {PLUGINS_LINKED && ( + + Plugins + + )} Docs - {PLUGINS_LINKED && ( - - Plugins - - )}