From a57f67328d222130c90f25a86e629aff8215e10a Mon Sep 17 00:00:00 2001
From: Bersabel Tadesse
Date: Mon, 3 Aug 2026 21:18:46 -0700
Subject: [PATCH] Polish extensions consistency states
---
.../plugin/PluginThreadChat.test.tsx | 5 +-
.../components/plugin/PluginThreadChat.tsx | 1 +
.../plugin/PluginsOverview.test.tsx | 8 ++-
.../management/BrowsePluginsTab.test.tsx | 53 +++++++++++++-
.../plugin/management/BrowsePluginsTab.tsx | 30 ++------
.../PluginSettingsCompatibilityRoute.test.tsx | 63 ++++++++++-------
.../PluginSettingsCompatibilityRoute.tsx | 17 ++++-
.../plugins/BrowsePluginsTab.test.tsx | 6 +-
.../settings/plugins/BrowsePluginsTab.tsx | 5 +-
.../components/settings/settings-nav.test.tsx | 37 ++++++++--
.../src/components/settings/settings-nav.tsx | 5 ++
.../src/components/tools/SkillsCollection.tsx | 3 +
.../queries/plugin-catalog-queries.test.ts | 12 +++-
.../hooks/queries/plugin-catalog-queries.ts | 8 ++-
apps/app/src/views/RootComposeView.tsx | 2 +-
apps/app/src/views/SkillsView.test.tsx | 21 +++---
.../thread-detail/PaneMaximizeButton.test.tsx | 5 ++
.../thread-detail/PaneMaximizeButton.tsx | 50 ++++++-------
.../ThreadDetailSecondaryContent.test.tsx | 17 +++++
.../ThreadDetailSecondaryContent.tsx | 34 +--------
.../plugin-catalog/plugin-catalog-service.ts | 25 ++++---
.../plugin-catalog-routes.test.ts | 6 +-
.../plugin-catalog-service.test.ts | 22 ++++--
.../bundled-types/bb-plugin-sdk.d.ts | 70 ++++++++++---------
packages/sdk/test/sdk.test.ts | 6 +-
packages/server-contract/src/api/plugins.ts | 2 +
packages/server-contract/test/plugins.test.ts | 13 ++--
.../src/components/ui/resource/toolbar.tsx | 10 ++-
.../src/generated/plugin-sdk-dts.generated.ts | 2 +-
29 files changed, 338 insertions(+), 200 deletions(-)
diff --git a/apps/app/src/components/plugin/PluginThreadChat.test.tsx b/apps/app/src/components/plugin/PluginThreadChat.test.tsx
index e73b1b145..12d210b42 100644
--- a/apps/app/src/components/plugin/PluginThreadChat.test.tsx
+++ b/apps/app/src/components/plugin/PluginThreadChat.test.tsx
@@ -104,9 +104,7 @@ afterEach(() => {
beforeEach(() => {
mocks.embeddedChatProps = [];
mocks.timelinePanelProps = [];
- vi.mocked(sdk.threads.get).mockResolvedValue(
- THREAD_FIXTURE as never,
- );
+ vi.mocked(sdk.threads.get).mockResolvedValue(THREAD_FIXTURE as never);
});
describe("PluginThreadChat", () => {
@@ -136,6 +134,7 @@ describe("PluginThreadChat", () => {
expect(props.providerId).toBe("provider_demo");
expect(props.variant).toBe("compact");
expect(props.measure).toBe("panel");
+ expect(props.surfaceTone).toBe("sidebar");
expect(props.composer).toEqual(
expect.objectContaining({
permissionPolicy: "snapshot",
diff --git a/apps/app/src/components/plugin/PluginThreadChat.tsx b/apps/app/src/components/plugin/PluginThreadChat.tsx
index f1ed6a762..94bce3559 100644
--- a/apps/app/src/components/plugin/PluginThreadChat.tsx
+++ b/apps/app/src/components/plugin/PluginThreadChat.tsx
@@ -218,6 +218,7 @@ function PluginThreadChatBody({
variant="compact"
layout={layout}
measure={variant === "full" ? "page" : "panel"}
+ surfaceTone={variant === "compact" ? "sidebar" : "background"}
threadId={threadId}
projectId={thread.projectId}
providerId={thread.providerId}
diff --git a/apps/app/src/components/plugin/PluginsOverview.test.tsx b/apps/app/src/components/plugin/PluginsOverview.test.tsx
index 00c8f3b85..847754a33 100644
--- a/apps/app/src/components/plugin/PluginsOverview.test.tsx
+++ b/apps/app/src/components/plugin/PluginsOverview.test.tsx
@@ -102,7 +102,13 @@ function installFetch(plugins: readonly unknown[] = [AUTOMATIONS_PLUGIN]) {
return responseJson({ plugins });
}
if (url.pathname === "/api/v1/plugin-catalog") {
- return responseJson({ catalog: { pluginCount: 4 } });
+ return responseJson({
+ catalog: {
+ pluginCount: 13,
+ includedPluginCount: 9,
+ optionalPluginCount: 4,
+ },
+ });
}
if (url.pathname === "/api/v1/plugin-catalog/search") {
return responseJson({ results: [GITHUB_CATALOG_ENTRY] });
diff --git a/apps/app/src/components/plugin/management/BrowsePluginsTab.test.tsx b/apps/app/src/components/plugin/management/BrowsePluginsTab.test.tsx
index 67b7190e5..ea7a1e634 100644
--- a/apps/app/src/components/plugin/management/BrowsePluginsTab.test.tsx
+++ b/apps/app/src/components/plugin/management/BrowsePluginsTab.test.tsx
@@ -32,7 +32,11 @@ const MEMORY_ENTRY: PluginCatalogSearchEntry = {
incompatibleReason: null,
};
-const CATALOG_STATUS = { pluginCount: 4 };
+const CATALOG_STATUS = {
+ pluginCount: 13,
+ includedPluginCount: 9,
+ optionalPluginCount: 4,
+};
const INCOMPATIBLE_ENTRY: PluginCatalogSearchEntry = {
...MEMORY_ENTRY,
@@ -87,6 +91,49 @@ afterEach(() => {
});
describe("BrowsePluginsTab", () => {
+ it("renders every official catalog card represented by the catalog count", async () => {
+ const entries = Array.from(
+ { length: CATALOG_STATUS.pluginCount },
+ (_, index) => ({
+ ...MEMORY_ENTRY,
+ entryId: `official-${index + 1}`,
+ pluginId: `official-${index + 1}`,
+ displayName: `Official ${index + 1}`,
+ category:
+ index < CATALOG_STATUS.includedPluginCount
+ ? "Included with BB"
+ : "Productivity",
+ }),
+ );
+ vi.stubGlobal(
+ "fetch",
+ vi.fn(async (url: string) => {
+ if (url === "/api/v1/plugin-catalog") {
+ return jsonResponse({ catalog: CATALOG_STATUS });
+ }
+ if (url === "/api/v1/plugin-catalog/search?q=") {
+ return jsonResponse({ results: entries });
+ }
+ if (url === "/api/v1/plugins") {
+ return jsonResponse({ enabled: true, plugins: [] });
+ }
+ return jsonResponse({ error: "not found" }, 404);
+ }),
+ );
+
+ const { wrapper } = createQueryClientTestHarness();
+ render( {}} onOpenPlugin={() => {}} />, {
+ wrapper,
+ });
+
+ expect(
+ await screen.findByText("13 plugins · 9 included with BB, 4 optional"),
+ ).toBeTruthy();
+ expect(
+ screen.getAllByRole("button", { name: /^Open Official \d+ details$/ }),
+ ).toHaveLength(CATALOG_STATUS.pluginCount);
+ });
+
it("shows the official plugins and entries", async () => {
vi.stubGlobal(
"fetch",
@@ -114,7 +161,9 @@ describe("BrowsePluginsTab", () => {
{ wrapper },
);
- expect(await screen.findByText("BB Official plugins")).toBeTruthy();
+ expect(
+ await screen.findByText("13 plugins · 9 included with BB, 4 optional"),
+ ).toBeTruthy();
const officialCatalog = screen.getByRole("region", {
name: "BB Official plugins",
});
diff --git a/apps/app/src/components/plugin/management/BrowsePluginsTab.tsx b/apps/app/src/components/plugin/management/BrowsePluginsTab.tsx
index 009ca4392..74dd70d1d 100644
--- a/apps/app/src/components/plugin/management/BrowsePluginsTab.tsx
+++ b/apps/app/src/components/plugin/management/BrowsePluginsTab.tsx
@@ -1,11 +1,6 @@
import { useState } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useDebounceValue } from "usehooks-ts";
-import {
- RESOURCE_GRID_PAGE_SIZE,
- ResourcePagination,
- useResourcePagination,
-} from "@bb/shared-ui/resource-pagination";
import {
ResourceBrowseCard,
ResourceBrowseGrid,
@@ -48,13 +43,8 @@ export function BrowsePluginsTab({
const searchQuery = usePluginCatalogSearch(debouncedQuery, { enabled: true });
const status = statusQuery.data;
const entries = searchQuery.data ?? [];
- const pagination = useResourcePagination(entries, {
- pageSize: RESOURCE_GRID_PAGE_SIZE,
- resetKey: debouncedQuery.toLowerCase(),
- });
-
const byCategory = new Map();
- for (const entry of pagination.items) {
+ for (const entry of entries) {
const bucket = byCategory.get(entry.category);
if (bucket === undefined) byCategory.set(entry.category, [entry]);
else bucket.push(entry);
@@ -71,18 +61,6 @@ export function BrowsePluginsTab({
onSearchChange={setQuery}
/>
}
- footer={
- pagination.total > pagination.pageSize ? (
-
- ) : undefined
- }
>
{status.pluginCount} plugin
- {status.pluginCount === 1 ? "" : "s"} · bundled with BB and
- installed with one click
+ {status.pluginCount === 1 ? "" : "s"} ·{" "}
+ {status.includedPluginCount}
+ {" included with BB, "}
+ {status.optionalPluginCount} optional
)}
diff --git a/apps/app/src/components/settings/PluginSettingsCompatibilityRoute.test.tsx b/apps/app/src/components/settings/PluginSettingsCompatibilityRoute.test.tsx
index 40ade07a4..d622e518d 100644
--- a/apps/app/src/components/settings/PluginSettingsCompatibilityRoute.test.tsx
+++ b/apps/app/src/components/settings/PluginSettingsCompatibilityRoute.test.tsx
@@ -4,34 +4,37 @@ import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it } from "vitest";
import { MemoryRouter, Route, Routes } from "react-router-dom";
import { PluginSettingsCompatibilityRoute } from "./PluginSettingsCompatibilityRoute";
+import { ToolsHubExperimentProvider } from "@/components/tools/tools-experiment-context";
-function renderRoute(path: string) {
+function renderRoute(path: string, toolsHubEnabled = false) {
render(
-
-
-
- Settings plugin manager
-
- }
- />
-
- Settings plugin detail
-
- }
- />
- Tools plugins} />
- Tools plugin detail}
- />
-
- ,
+
+
+
+
+ Settings plugin manager
+
+ }
+ />
+
+ Settings plugin detail
+
+ }
+ />
+ Tools plugins} />
+ Tools plugin detail}
+ />
+
+
+ ,
);
}
@@ -46,10 +49,16 @@ describe("PluginSettingsCompatibilityRoute", () => {
});
it("keeps Settings plugin detail routes available", () => {
- renderRoute("/settings/plugins/example");
+ renderRoute("/settings/plugins/example", true);
expect(screen.getByText("Settings plugin detail")).toBeTruthy();
expect(screen.queryByText("Tools plugin detail")).toBeNull();
});
+ it("moves legacy plugin management to Extensions while enabled", () => {
+ renderRoute("/settings/plugins", true);
+
+ expect(screen.getByText("Tools plugins")).toBeTruthy();
+ expect(screen.queryByText("Settings plugin manager")).toBeNull();
+ });
});
diff --git a/apps/app/src/components/settings/PluginSettingsCompatibilityRoute.tsx b/apps/app/src/components/settings/PluginSettingsCompatibilityRoute.tsx
index 0aca2bcd9..eaa10a97d 100644
--- a/apps/app/src/components/settings/PluginSettingsCompatibilityRoute.tsx
+++ b/apps/app/src/components/settings/PluginSettingsCompatibilityRoute.tsx
@@ -1,9 +1,24 @@
import type { ReactNode } from "react";
-/** Plugin configuration always belongs to Settings, independent of Tools Hub. */
+import { Navigate, useLocation } from "react-router-dom";
+import { useToolsHubExperiment } from "@/components/tools/tools-experiment-context";
+import {
+ SETTINGS_PLUGINS_ROUTE_PATH,
+ TOOLS_PLUGINS_ROUTE_PATH,
+} from "@/lib/route-paths";
+
+/**
+ * The Extensions collection replaces legacy plugin management while enabled.
+ * Plugin-registered settings keep their own Settings routes in both modes.
+ */
export function PluginSettingsCompatibilityRoute({
children,
}: {
children: ReactNode;
}) {
+ const location = useLocation();
+ const toolsHubEnabled = useToolsHubExperiment();
+ if (toolsHubEnabled && location.pathname === SETTINGS_PLUGINS_ROUTE_PATH) {
+ return ;
+ }
return children;
}
diff --git a/apps/app/src/components/settings/plugins/BrowsePluginsTab.test.tsx b/apps/app/src/components/settings/plugins/BrowsePluginsTab.test.tsx
index 8a3bf7607..329e56bec 100644
--- a/apps/app/src/components/settings/plugins/BrowsePluginsTab.test.tsx
+++ b/apps/app/src/components/settings/plugins/BrowsePluginsTab.test.tsx
@@ -26,7 +26,11 @@ const MEMORY_ENTRY: PluginCatalogSearchEntry = {
incompatibleReason: null,
};
-const CATALOG_STATUS = { pluginCount: 4 };
+const CATALOG_STATUS = {
+ pluginCount: 13,
+ includedPluginCount: 9,
+ optionalPluginCount: 4,
+};
afterEach(() => {
cleanup();
diff --git a/apps/app/src/components/settings/plugins/BrowsePluginsTab.tsx b/apps/app/src/components/settings/plugins/BrowsePluginsTab.tsx
index 5fd42226d..5ba4d5740 100644
--- a/apps/app/src/components/settings/plugins/BrowsePluginsTab.tsx
+++ b/apps/app/src/components/settings/plugins/BrowsePluginsTab.tsx
@@ -52,8 +52,9 @@ export function BrowsePluginsTab({
) : (
{status.pluginCount} plugin
- {status.pluginCount === 1 ? "" : "s"} · bundled with BB and
- installed with one click
+ {status.pluginCount === 1 ? "" : "s"} · {status.includedPluginCount}
+ {" included with BB, "}
+ {status.optionalPluginCount} optional
)}
diff --git a/apps/app/src/components/settings/settings-nav.test.tsx b/apps/app/src/components/settings/settings-nav.test.tsx
index eefbec1ab..06cc0ad2a 100644
--- a/apps/app/src/components/settings/settings-nav.test.tsx
+++ b/apps/app/src/components/settings/settings-nav.test.tsx
@@ -6,22 +6,31 @@ import type { ReactNode } from "react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { resetPluginSlotStoreForTest } from "@/lib/plugin-slots";
import { createQueryClientTestHarness } from "@/test/queryClientTestHarness";
+import { ToolsHubExperimentProvider } from "@/components/tools/tools-experiment-context";
import { useSettingsNavState } from "./settings-nav";
+const mocks = vi.hoisted(() => ({
+ plugins: [] as Array>,
+}));
+
vi.mock("@/hooks/queries/plugin-settings-queries", () => ({
- usePluginList: () => ({ data: { plugins: [] } }),
+ usePluginList: () => ({ data: { plugins: mocks.plugins } }),
}));
vi.mock("@/hooks/useHostDaemon", () => ({
useHostDaemon: () => ({ hasDaemon: false }),
}));
-function wrapperFor(path: string) {
+function wrapperFor(path: string, toolsHubEnabled = false) {
const { wrapper: QueryWrapper } = createQueryClientTestHarness();
return function Wrapper({ children }: { children: ReactNode }) {
return (
- {children}
+
+
+ {children}
+
+
);
};
@@ -31,6 +40,7 @@ afterEach(() => {
cleanup();
resetPluginSlotStoreForTest();
vi.clearAllMocks();
+ mocks.plugins = [];
});
describe("useSettingsNavState", () => {
@@ -67,7 +77,7 @@ describe("useSettingsNavState", () => {
);
});
- it("keeps plugin management in Settings", () => {
+ it("keeps legacy plugin management in Settings while Extensions is disabled", () => {
const { result } = renderHook(() => useSettingsNavState(), {
wrapper: wrapperFor("/settings"),
});
@@ -77,4 +87,23 @@ describe("useSettingsNavState", () => {
);
});
+ it("hides legacy plugin management but preserves registered plugin settings while Extensions is enabled", () => {
+ mocks.plugins = [
+ {
+ id: "workflows",
+ enabled: true,
+ hasSettings: true,
+ },
+ ];
+ const { result } = renderHook(() => useSettingsNavState(), {
+ wrapper: wrapperFor("/settings", true),
+ });
+
+ expect(result.current.sections.map((section) => section.id)).not.toContain(
+ "plugins",
+ );
+ expect(result.current.pluginEntries.map((plugin) => plugin.id)).toEqual([
+ "workflows",
+ ]);
+ });
});
diff --git a/apps/app/src/components/settings/settings-nav.tsx b/apps/app/src/components/settings/settings-nav.tsx
index b6b871ef7..c73da2727 100644
--- a/apps/app/src/components/settings/settings-nav.tsx
+++ b/apps/app/src/components/settings/settings-nav.tsx
@@ -7,6 +7,7 @@ import {
import { useHostDaemon } from "@/hooks/useHostDaemon";
import { usePluginSlots } from "@/lib/plugin-slots";
import { PluginIcon } from "@/components/plugin/PluginIcon";
+import { useToolsHubExperiment } from "@/components/tools/tools-experiment-context";
import {
SETTINGS_MACHINE_ROUTE_PATH,
SETTINGS_PLUGIN_ROUTE_PATH,
@@ -81,6 +82,7 @@ export interface SettingsNavState {
*/
export function useSettingsNavState(): SettingsNavState {
const location = useLocation();
+ const toolsHubEnabled = useToolsHubExperiment();
const { hasDaemon } = useHostDaemon();
const { fileOpeners, settingsSections } = usePluginSlots();
const settingsSectionPluginIds = new Set(
@@ -126,6 +128,9 @@ export function useSettingsNavState(): SettingsNavState {
: "general";
const sections = SETTINGS_NAV_SECTIONS.filter((section) => {
+ if (section.id === "plugins" && toolsHubEnabled) {
+ return false;
+ }
if (section.id === "files") {
return hasDaemon || fileOpeners.length > 0;
}
diff --git a/apps/app/src/components/tools/SkillsCollection.tsx b/apps/app/src/components/tools/SkillsCollection.tsx
index 0c0245598..ac61672d2 100644
--- a/apps/app/src/components/tools/SkillsCollection.tsx
+++ b/apps/app/src/components/tools/SkillsCollection.tsx
@@ -395,6 +395,9 @@ export function SkillsOverview({
icon="Layers"
selectedValues={providerFilters}
options={providerOptions}
+ selectedLabel={(options) =>
+ options.map((option) => option.label).join(", ")
+ }
onChange={(values) =>
setProviderFilters(values as ResourceProviderFilter[])
}
diff --git a/apps/app/src/hooks/queries/plugin-catalog-queries.test.ts b/apps/app/src/hooks/queries/plugin-catalog-queries.test.ts
index eaa5692c5..e09d287ed 100644
--- a/apps/app/src/hooks/queries/plugin-catalog-queries.test.ts
+++ b/apps/app/src/hooks/queries/plugin-catalog-queries.test.ts
@@ -133,10 +133,18 @@ describe("plugin catalog queries", () => {
it("binds browser fetch and parses the status count", async () => {
const status = await fetchPluginCatalogStatus(
receiverSensitiveFetch({
- catalog: { pluginCount: 4 },
+ catalog: {
+ pluginCount: 13,
+ includedPluginCount: 9,
+ optionalPluginCount: 4,
+ },
}),
);
- expect(status).toEqual({ pluginCount: 4 });
+ expect(status).toEqual({
+ pluginCount: 13,
+ includedPluginCount: 9,
+ optionalPluginCount: 4,
+ });
});
it("preserves canonical plugin identity without source-catalog fields", async () => {
diff --git a/apps/app/src/hooks/queries/plugin-catalog-queries.ts b/apps/app/src/hooks/queries/plugin-catalog-queries.ts
index 9ce9994c3..422c5618d 100644
--- a/apps/app/src/hooks/queries/plugin-catalog-queries.ts
+++ b/apps/app/src/hooks/queries/plugin-catalog-queries.ts
@@ -153,12 +153,18 @@ export async function applyPluginUpdate(
export interface PluginCatalogStatus {
pluginCount: number;
+ includedPluginCount: number;
+ optionalPluginCount: number;
}
function toPluginCatalogStatus(
data: SdkPluginCatalogStatus,
): PluginCatalogStatus {
- return { pluginCount: data.pluginCount };
+ return {
+ pluginCount: data.pluginCount,
+ includedPluginCount: data.includedPluginCount,
+ optionalPluginCount: data.optionalPluginCount,
+ };
}
export async function fetchPluginCatalogStatus(
diff --git a/apps/app/src/views/RootComposeView.tsx b/apps/app/src/views/RootComposeView.tsx
index 24147c08a..3ac2b3e8d 100644
--- a/apps/app/src/views/RootComposeView.tsx
+++ b/apps/app/src/views/RootComposeView.tsx
@@ -3468,7 +3468,7 @@ export function RootComposeView() {
showConversationCollapseControl: false,
showGitDiffTab: false,
showInfoTab: false,
- showNewTabButton: true,
+ showNewTabButton: false,
inlinePanelToggle: panelTogglePlacement.inlinePanelToggle,
onClose: closeSecondaryPanel,
onCollapse: closeSecondaryPanel,
diff --git a/apps/app/src/views/SkillsView.test.tsx b/apps/app/src/views/SkillsView.test.tsx
index 81efd259a..9fda50959 100644
--- a/apps/app/src/views/SkillsView.test.tsx
+++ b/apps/app/src/views/SkillsView.test.tsx
@@ -253,7 +253,8 @@ describe("SkillsOverview", () => {
});
expect(markup).not.toContain("claude-skill");
expect(markup).toContain("Review the current diff.");
- expect(markup).toContain('aria-label="Provider: 1 selected"');
+ expect(markup).toContain('aria-label="bb"');
+ expect(markup).not.toContain("Provider: 1 selected");
expect(markup).toContain("Sort");
expect(markup).toContain('role="tab"');
expect(markup).toContain("Library");
@@ -335,9 +336,7 @@ describe("SkillsOverview", () => {
/>,
);
- fireEvent.pointerDown(
- screen.getByRole("button", { name: "Provider: 1 selected" }),
- );
+ fireEvent.pointerDown(screen.getByRole("button", { name: "bb" }));
await waitFor(() => {
expect(
@@ -381,15 +380,11 @@ describe("SkillsOverview", () => {
);
await waitFor(() => {
- expect(
- screen.getByRole("button", { name: "Provider: 1 selected" }),
- ).toBeTruthy();
+ expect(screen.getByRole("button", { name: "bb" })).toBeTruthy();
expect(screen.queryByText("codex-skill")).toBeNull();
});
- fireEvent.pointerDown(
- screen.getByRole("button", { name: "Provider: 1 selected" }),
- );
+ fireEvent.pointerDown(screen.getByRole("button", { name: "bb" }));
const bbFilter = screen.getByRole("menuitemcheckbox", { name: "bb" });
expect(bbFilter.getAttribute("aria-checked")).toBe("true");
expect(bbFilter.getAttribute("aria-disabled")).toBeNull();
@@ -419,17 +414,17 @@ describe("SkillsOverview", () => {
/>,
);
- fireEvent.pointerDown(
- screen.getByRole("button", { name: "Provider: 1 selected" }),
- );
+ fireEvent.pointerDown(screen.getByRole("button", { name: "bb" }));
fireEvent.click(screen.getByRole("menuitemcheckbox", { name: "bb" }));
fireEvent.click(
screen.getByRole("menuitemcheckbox", { name: "Claude Code" }),
);
+ fireEvent.keyDown(document, { key: "Escape" });
await waitFor(() => {
expect(screen.getByText("claude-skill")).toBeTruthy();
expect(screen.queryByText("bb-skill")).toBeNull();
+ expect(screen.getByRole("button", { name: "Claude Code" })).toBeTruthy();
});
view.rerender(
diff --git a/apps/app/src/views/thread-detail/PaneMaximizeButton.test.tsx b/apps/app/src/views/thread-detail/PaneMaximizeButton.test.tsx
index 9a37d5261..1ba3ec8f7 100644
--- a/apps/app/src/views/thread-detail/PaneMaximizeButton.test.tsx
+++ b/apps/app/src/views/thread-detail/PaneMaximizeButton.test.tsx
@@ -94,6 +94,7 @@ describe("PaneMaximizeButton", () => {
fireEvent.pointerEnter(button);
const menu = await screen.findByRole("menu", { name: "Pane arrangement" });
expect(menu.textContent).toContain("Full Screen");
+ expect(menu.textContent).toContain("Move");
expect(
screen.getAllByRole("menuitem").map((item) => item.textContent),
).toEqual(["Full Screen⌘⇧E", "", "", "", ""]);
@@ -104,7 +105,11 @@ describe("PaneMaximizeButton", () => {
expect(
action.querySelector(`[data-pane-arrangement-glyph="${side}"]`),
).not.toBeNull();
+ expect(action.className).toContain("cursor-pointer");
}
+ expect(
+ screen.getByRole("menuitem", { name: /Full Screen/ }).className,
+ ).toContain("cursor-pointer");
fireEvent.click(screen.getByRole("menuitem", { name: "Move left" }));
expect(onMoveToSide).toHaveBeenCalledWith("left");
diff --git a/apps/app/src/views/thread-detail/PaneMaximizeButton.tsx b/apps/app/src/views/thread-detail/PaneMaximizeButton.tsx
index 67e7f1f2f..91e65dbe1 100644
--- a/apps/app/src/views/thread-detail/PaneMaximizeButton.tsx
+++ b/apps/app/src/views/thread-detail/PaneMaximizeButton.tsx
@@ -21,7 +21,7 @@ const ARRANGEMENT_ACTIONS: ReadonlyArray<{
];
const MENU_ITEM_CLASS =
- "flex w-full cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-left text-xs text-foreground outline-none transition-colors hover:bg-state-hover focus-visible:bg-state-hover focus-visible:outline-none [&>svg]:size-4 [&>svg]:shrink-0";
+ "flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-left text-xs text-foreground outline-none transition-colors hover:bg-state-hover focus-visible:bg-state-hover focus-visible:outline-none [&>svg]:size-4 [&>svg]:shrink-0";
const ARRANGEMENT_REGION_CLASS: Record = {
left: "inset-y-[3px] left-[3px] w-2.5",
@@ -135,29 +135,31 @@ export function PaneMaximizeButton({
) : null}
{onMoveToSide ? (
-
- {ARRANGEMENT_ACTIONS.map((action) => (
-
-
- {
- handleOpenChange(false);
- onMoveToSide(action.side);
- }}
- >
-
-
-
- {action.label}
-
- ))}
+
+
+ Move
+
+
+ {ARRANGEMENT_ACTIONS.map((action) => (
+
+
+ {
+ handleOpenChange(false);
+ onMoveToSide(action.side);
+ }}
+ >
+
+
+
+ {action.label}
+
+ ))}
+
) : null}
diff --git a/apps/app/src/views/thread-detail/ThreadDetailSecondaryContent.test.tsx b/apps/app/src/views/thread-detail/ThreadDetailSecondaryContent.test.tsx
index dd285ff0f..e9ecf536c 100644
--- a/apps/app/src/views/thread-detail/ThreadDetailSecondaryContent.test.tsx
+++ b/apps/app/src/views/thread-detail/ThreadDetailSecondaryContent.test.tsx
@@ -456,6 +456,23 @@ beforeEach(() => {
});
describe("ThreadDetailSecondaryContent compact drawer settling", () => {
+ it("keeps the thread header inside the timeline column beside the side panel", () => {
+ renderThreadDetail({
+ isCompactViewport: false,
+ isSecondaryPanelOpen: true,
+ renderBrowserDeck: createBrowserDeckRenderer(),
+ threadId: "thread-1",
+ });
+
+ const timelinePanel = screen.getByTestId("panel");
+ const sidePanel = screen.getByTestId("inline-secondary-panel");
+ const panelGroup = screen.getByTestId("panel-group");
+ expect(timelinePanel.contains(screen.getByTestId("header"))).toBe(true);
+ expect(timelinePanel.contains(sidePanel)).toBe(false);
+ expect(panelGroup.contains(timelinePanel)).toBe(true);
+ expect(panelGroup.contains(sidePanel)).toBe(true);
+ });
+
it("hides and restores native browser readiness as hosted pane focus changes", () => {
const order: string[] = [];
const renderBrowserDeck = createBrowserDeckRenderer(order);
diff --git a/apps/app/src/views/thread-detail/ThreadDetailSecondaryContent.tsx b/apps/app/src/views/thread-detail/ThreadDetailSecondaryContent.tsx
index adebc4d2f..2d17e86d2 100644
--- a/apps/app/src/views/thread-detail/ThreadDetailSecondaryContent.tsx
+++ b/apps/app/src/views/thread-detail/ThreadDetailSecondaryContent.tsx
@@ -22,7 +22,6 @@ import { cn } from "@bb/shared-ui/lib/utils";
import { ThreadSecondaryPanel } from "@/components/secondary-panel/ThreadSecondaryPanel";
import {
secondaryPanelWidthPercentAtom,
- threadSecondaryPanelResizingAtom,
} from "@/components/secondary-panel/threadSecondaryPanelAtoms";
import {
ThreadMetadataCard,
@@ -120,12 +119,6 @@ function ThreadDetailSecondaryContentBody({
const persistedSecondaryWidthPercent = useAtomValue(
secondaryPanelWidthPercentAtom,
);
- const isSecondaryPanelResizing = useAtomValue(
- threadSecondaryPanelResizingAtom,
- );
- const [liveSecondaryWidthPercent, setLiveSecondaryWidthPercent] = useState(
- persistedSecondaryWidthPercent,
- );
// Collapsing the conversation only makes sense on a wide viewport with the
// secondary panel open — there is otherwise nothing to expand into.
const canCollapseConversation = isSecondaryPanelOpen && !renderAsDrawer;
@@ -291,7 +284,6 @@ function ThreadDetailSecondaryContentBody({
- {/*
- The thread header is a full-width bar above the split, so its right-aligned
- actions stay anchored to the window edge instead of riding the timeline
- panel's width as the secondary panel opens and closes.
- */}
-
{/*
When collapsed we keep the resizable PanelGroup mounted: the timeline
lifts to 0% and the panel to 100% via the layout effect. Nothing
@@ -462,6 +429,7 @@ function ThreadDetailSecondaryContentBody({
isConversationCollapsedActive && "opacity-0",
)}
>
+ {header}
diff --git a/apps/server/src/services/plugin-catalog/plugin-catalog-service.ts b/apps/server/src/services/plugin-catalog/plugin-catalog-service.ts
index ddf649bca..8acc7ace8 100644
--- a/apps/server/src/services/plugin-catalog/plugin-catalog-service.ts
+++ b/apps/server/src/services/plugin-catalog/plugin-catalog-service.ts
@@ -31,17 +31,20 @@ export function createPluginCatalogService(deps: {
db: DbConnection;
appVersion: string;
plugins: Pick;
- officialPlugins?: readonly BundledPluginRegistration[];
+ bundledPlugins?: readonly BundledPluginRegistration[];
warn?: (message: string) => void;
}): PluginCatalogService {
- const officialPlugins = (
- deps.officialPlugins ??
- listBundledPluginRegistrations().filter((plugin) => !plugin.autoInstall)
- ).map((plugin) => ({ ...plugin, category: plugin.category ?? "Other" }));
+ const bundledPlugins =
+ deps.bundledPlugins ?? listBundledPluginRegistrations();
+ const officialPlugins = bundledPlugins.map((plugin) => ({
+ ...plugin,
+ category:
+ plugin.category ?? (plugin.autoInstall ? "Included with BB" : "Other"),
+ }));
// Manifests are read per search so a dev checkout editing a bundled
- // plugin's package.json sees fresh store metadata; four local files is
- // cheap enough not to cache.
+ // plugin's package.json sees fresh store metadata; this small local catalog
+ // is cheap enough not to cache.
function entryManifest(
entry: BundledPluginRegistration,
): Promise {
@@ -86,7 +89,13 @@ export function createPluginCatalogService(deps: {
}
return {
- status: () => ({ pluginCount: officialPlugins.length }),
+ status: () => ({
+ pluginCount: bundledPlugins.length,
+ includedPluginCount: bundledPlugins.filter((plugin) => plugin.autoInstall)
+ .length,
+ optionalPluginCount: bundledPlugins.filter((plugin) => !plugin.autoInstall)
+ .length,
+ }),
async search(rawQuery) {
const query = rawQuery.trim().toLowerCase();
diff --git a/apps/server/test/services/plugin-catalog/plugin-catalog-routes.test.ts b/apps/server/test/services/plugin-catalog/plugin-catalog-routes.test.ts
index 171a942f7..faed7b555 100644
--- a/apps/server/test/services/plugin-catalog/plugin-catalog-routes.test.ts
+++ b/apps/server/test/services/plugin-catalog/plugin-catalog-routes.test.ts
@@ -29,7 +29,11 @@ describe("plugin catalog routes", () => {
const status = await app.request("/plugin-catalog");
await expect(status.json()).resolves.toMatchObject({
- catalog: { pluginCount: 4 },
+ catalog: {
+ pluginCount: 13,
+ includedPluginCount: 9,
+ optionalPluginCount: 4,
+ },
});
const search = await app.request("/plugin-catalog/search?q=memory");
await expect(search.json()).resolves.toMatchObject({
diff --git a/apps/server/test/services/plugin-catalog/plugin-catalog-service.test.ts b/apps/server/test/services/plugin-catalog/plugin-catalog-service.test.ts
index 4f5a41aa7..19cf4e9c1 100644
--- a/apps/server/test/services/plugin-catalog/plugin-catalog-service.test.ts
+++ b/apps/server/test/services/plugin-catalog/plugin-catalog-service.test.ts
@@ -11,6 +11,8 @@ import {
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { createPluginCatalogService } from "../../../src/services/plugin-catalog/plugin-catalog-service.js";
import {
+ BUILTIN_PLUGINS,
+ BUNDLED_PLUGINS,
OFFICIAL_PLUGINS,
listBundledPluginRegistrations,
} from "../../../src/services/plugins/builtin-registry.js";
@@ -28,9 +30,9 @@ describe("bundled plugin catalog service", () => {
afterEach(() => db.$client.close());
function service(options?: {
- officialPlugins?: Parameters<
+ bundledPlugins?: Parameters<
typeof createPluginCatalogService
- >[0]["officialPlugins"];
+ >[0]["bundledPlugins"];
warn?: (message: string) => void;
}) {
return createPluginCatalogService({
@@ -42,9 +44,9 @@ describe("bundled plugin catalog service", () => {
throw new Error("installation stopped by test");
},
},
- ...(options?.officialPlugins === undefined
+ ...(options?.bundledPlugins === undefined
? {}
- : { officialPlugins: options.officialPlugins }),
+ : { bundledPlugins: options.bundledPlugins }),
...(options?.warn === undefined ? {} : { warn: options.warn }),
});
}
@@ -75,13 +77,19 @@ describe("bundled plugin catalog service", () => {
it("lists every bundled official plugin from its manifest", async () => {
const catalog = service();
expect(catalog.status()).toEqual({
- pluginCount: OFFICIAL_PLUGINS.length,
+ pluginCount: BUNDLED_PLUGINS.length,
+ includedPluginCount: BUILTIN_PLUGINS.length,
+ optionalPluginCount: OFFICIAL_PLUGINS.length,
});
const results = await catalog.search("");
expect(results.map((entry) => entry.entryId).sort()).toEqual(
- OFFICIAL_PLUGINS.map((plugin) => plugin.name).sort(),
+ BUNDLED_PLUGINS.map((plugin) => plugin.name).sort(),
);
+ expect(results).toHaveLength(catalog.status().pluginCount);
+ expect(
+ results.filter((entry) => entry.category === "Included with BB"),
+ ).toHaveLength(BUILTIN_PLUGINS.length);
const docs = results.find((entry) => entry.entryId === "docs");
expect(docs).toMatchObject({
pluginId: "simple-notes",
@@ -145,7 +153,7 @@ describe("bundled plugin catalog service", () => {
);
if (github === undefined) throw new Error("github registration missing");
const catalog = service({
- officialPlugins: [
+ bundledPlugins: [
github,
{
name: "broken",
diff --git a/packages/plugin-sdk/bundled-types/bb-plugin-sdk.d.ts b/packages/plugin-sdk/bundled-types/bb-plugin-sdk.d.ts
index b7c204bac..3d6e89e23 100644
--- a/packages/plugin-sdk/bundled-types/bb-plugin-sdk.d.ts
+++ b/packages/plugin-sdk/bundled-types/bb-plugin-sdk.d.ts
@@ -310,8 +310,8 @@ declare const providerPendingInteractionSchema: z$1.ZodObject<{
id: z$1.ZodString;
threadId: z$1.ZodString;
status: z$1.ZodEnum<{
- pending: "pending";
interrupted: "interrupted";
+ pending: "pending";
resolving: "resolving";
resolved: "resolved";
}>;
@@ -448,8 +448,8 @@ declare const pluginPendingInteractionSchema: z$1.ZodObject<{
id: z$1.ZodString;
threadId: z$1.ZodString;
status: z$1.ZodEnum<{
- pending: "pending";
interrupted: "interrupted";
+ pending: "pending";
resolving: "resolving";
resolved: "resolved";
}>;
@@ -696,8 +696,8 @@ declare const threadEventSchema: z$1.ZodPipe;
@@ -741,10 +741,10 @@ declare const threadEventSchema: z$1.ZodPipe;
approvalStatus: z$1.ZodNullable;
}, z$1.core.$strip>>;
status: z$1.ZodEnum<{
- pending: "pending";
completed: "completed";
failed: "failed";
interrupted: "interrupted";
+ pending: "pending";
}>;
approvalStatus: z$1.ZodNullable>;
status: z$1.ZodEnum<{
- pending: "pending";
completed: "completed";
failed: "failed";
interrupted: "interrupted";
+ pending: "pending";
}>;
result: z$1.ZodOptional;
error: z$1.ZodOptional;
@@ -878,17 +878,17 @@ declare const threadEventSchema: z$1.ZodPipe;
taskStatus: z$1.ZodEnum<{
- pending: "pending";
- running: "running";
- paused: "paused";
completed: "completed";
failed: "failed";
+ paused: "paused";
+ pending: "pending";
+ running: "running";
killed: "killed";
stopped: "stopped";
}>;
@@ -904,8 +904,8 @@ declare const threadEventSchema: z$1.ZodPipe;
approvalStatus: z$1.ZodNullable;
}, z$1.core.$strip>>;
status: z$1.ZodEnum<{
- pending: "pending";
completed: "completed";
failed: "failed";
interrupted: "interrupted";
+ pending: "pending";
}>;
approvalStatus: z$1.ZodNullable>;
status: z$1.ZodEnum<{
- pending: "pending";
completed: "completed";
failed: "failed";
interrupted: "interrupted";
+ pending: "pending";
}>;
result: z$1.ZodOptional;
error: z$1.ZodOptional;
@@ -1110,17 +1110,17 @@ declare const threadEventSchema: z$1.ZodPipe;
taskStatus: z$1.ZodEnum<{
- pending: "pending";
- running: "running";
- paused: "paused";
completed: "completed";
failed: "failed";
+ paused: "paused";
+ pending: "pending";
+ running: "running";
killed: "killed";
stopped: "stopped";
}>;
@@ -1136,8 +1136,8 @@ declare const threadEventSchema: z$1.ZodPipe;
taskStatus: z$1.ZodEnum<{
- pending: "pending";
- running: "running";
- paused: "paused";
completed: "completed";
failed: "failed";
+ paused: "paused";
+ pending: "pending";
+ running: "running";
killed: "killed";
stopped: "stopped";
}>;
@@ -1265,8 +1265,8 @@ declare const threadEventSchema: z$1.ZodPipe;
taskStatus: z$1.ZodEnum<{
- pending: "pending";
- running: "running";
- paused: "paused";
completed: "completed";
failed: "failed";
+ paused: "paused";
+ pending: "pending";
+ running: "running";
killed: "killed";
stopped: "stopped";
}>;
@@ -1337,8 +1337,8 @@ declare const threadEventSchema: z$1.ZodPipe>;
}, z$1.core.$strip>>;
explanation: z$1.ZodOptional;
@@ -1816,8 +1816,8 @@ declare const threadEventSchema: z$1.ZodPipe;
@@ -1868,8 +1868,8 @@ declare const threadEventSchema: z$1.ZodPipe;
@@ -2038,8 +2038,8 @@ declare const threadTimelinePendingTodosSchema: z$1.ZodObject<{
id: z$1.ZodString;
text: z$1.ZodString;
status: z$1.ZodEnum<{
- pending: "pending";
completed: "completed";
+ pending: "pending";
in_progress: "in_progress";
}>;
}, z$1.core.$strip>>;
@@ -2839,8 +2839,8 @@ declare const environmentDiffFileResponseSchema: z$1.ZodObject<{
path: z$1.ZodString;
content: z$1.ZodString;
contentEncoding: z$1.ZodEnum<{
- utf8: "utf8";
base64: "base64";
+ utf8: "utf8";
}>;
mimeType: z$1.ZodOptional;
sizeBytes: z$1.ZodNumber;
@@ -6282,6 +6282,8 @@ declare const pluginTokenResponseSchema: z$1.ZodObject<{
type PluginTokenResponse = z$1.infer;
declare const pluginCatalogStatusSchema: z$1.ZodObject<{
pluginCount: z$1.ZodNumber;
+ includedPluginCount: z$1.ZodNumber;
+ optionalPluginCount: z$1.ZodNumber;
}, z$1.core.$strip>;
type PluginCatalogStatus = z$1.infer;
declare const pluginCatalogSearchResultSchema: z$1.ZodObject<{
diff --git a/packages/sdk/test/sdk.test.ts b/packages/sdk/test/sdk.test.ts
index 9411e5008..4fdb38219 100644
--- a/packages/sdk/test/sdk.test.ts
+++ b/packages/sdk/test/sdk.test.ts
@@ -1239,7 +1239,11 @@ describe("@bb/sdk", () => {
logoUrl: null,
logoDarkUrl: null,
};
- const catalog = { pluginCount: 1 };
+ const catalog = {
+ pluginCount: 1,
+ includedPluginCount: 1,
+ optionalPluginCount: 0,
+ };
const checked = {
id: "notes",
outcome: "update-available" as const,
diff --git a/packages/server-contract/src/api/plugins.ts b/packages/server-contract/src/api/plugins.ts
index c96a53c77..cb631e542 100644
--- a/packages/server-contract/src/api/plugins.ts
+++ b/packages/server-contract/src/api/plugins.ts
@@ -310,6 +310,8 @@ export type PluginTokenResponse = z.infer;
export const pluginCatalogStatusSchema = z.object({
pluginCount: z.number(),
+ includedPluginCount: z.number(),
+ optionalPluginCount: z.number(),
});
export type PluginCatalogStatus = z.infer;
diff --git a/packages/server-contract/test/plugins.test.ts b/packages/server-contract/test/plugins.test.ts
index b72300517..39010f2c1 100644
--- a/packages/server-contract/test/plugins.test.ts
+++ b/packages/server-contract/test/plugins.test.ts
@@ -26,13 +26,16 @@ describe("plugin catalog contracts", () => {
});
it("keeps status to the bundled plugin count and search fields required", () => {
- expect(pluginCatalogStatusSchema.parse({ pluginCount: 4 })).toEqual({
- pluginCount: 4,
- });
+ const status = {
+ pluginCount: 13,
+ includedPluginCount: 9,
+ optionalPluginCount: 4,
+ };
+ expect(pluginCatalogStatusSchema.parse(status)).toEqual(status);
// Refresh-era freshness fields no longer survive parsing.
expect(
- pluginCatalogStatusSchema.parse({ pluginCount: 4, lastError: null }),
- ).toEqual({ pluginCount: 4 });
+ pluginCatalogStatusSchema.parse({ ...status, lastError: null }),
+ ).toEqual(status);
expect(() =>
pluginCatalogSearchResultSchema.parse({
diff --git a/packages/shared-ui/src/components/ui/resource/toolbar.tsx b/packages/shared-ui/src/components/ui/resource/toolbar.tsx
index d4032098b..bb97b69d1 100644
--- a/packages/shared-ui/src/components/ui/resource/toolbar.tsx
+++ b/packages/shared-ui/src/components/ui/resource/toolbar.tsx
@@ -199,21 +199,25 @@ export function ResourceMultiSelectMenu({
selectedValues,
options,
onChange,
+ selectedLabel,
}: {
label: string;
icon: IconName;
selectedValues: readonly string[];
options: readonly ResourceOption[];
onChange: (values: string[]) => void;
+ selectedLabel?: (options: readonly ResourceOption[]) => string;
}) {
const selected = new Set(selectedValues);
- const activeSelectedCount = options.filter(
+ const activeOptions = options.filter(
(option) => !option.disabled && selected.has(option.id),
- ).length;
+ );
+ const activeSelectedCount = activeOptions.length;
const triggerLabel =
activeSelectedCount === 0
? label
- : `${label}: ${activeSelectedCount} selected`;
+ : (selectedLabel?.(activeOptions) ??
+ `${label}: ${activeSelectedCount} selected`);
function updateValue(option: ResourceOption, checked: boolean) {
if (option.disabled) return;
diff --git a/packages/templates/src/generated/plugin-sdk-dts.generated.ts b/packages/templates/src/generated/plugin-sdk-dts.generated.ts
index eda48e847..7f521635f 100644
--- a/packages/templates/src/generated/plugin-sdk-dts.generated.ts
+++ b/packages/templates/src/generated/plugin-sdk-dts.generated.ts
@@ -2,6 +2,6 @@
// Generated by packages/templates/scripts/generate-templates.mjs from
// @bb/plugin-sdk/bundled-types. Do not edit directly.
-export const PLUGIN_SDK_DTS = "// Portable type declarations for `@bb/plugin-sdk`. Unpublished BB\n// workspace contracts are flattened; public subpaths may reuse the\n// package root without requiring any other @bb/* package.\n//\n// Confused by the API, or need a symbol that isn't here? Clone the BB repo\n// and read the real source: https://github.com/ymichael/bb\n\nimport * as react from 'react';\nimport { ComponentType, ReactNode } from 'react';\nimport * as z from 'zod';\nimport { z as z$1 } from 'zod';\nimport Database from 'better-sqlite3';\nimport { Context } from 'hono';\n\n/**\n * App-wide server-backed preferences.\n * Client-local settings stay in the frontend localStorage helpers instead.\n */\ndeclare const appSettingsSchema: z$1.ZodObject<{\n caffeinate: z$1.ZodBoolean;\n showKeyboardHints: z$1.ZodBoolean;\n steerActiveThreadOnEnter: z$1.ZodBoolean;\n showUnhandledProviderEvents: z$1.ZodBoolean;\n codexMemoryEnabled: z$1.ZodBoolean;\n claudeCodeMemoryEnabled: z$1.ZodBoolean;\n codexSubagentsDisabled: z$1.ZodBoolean;\n claudeCodeSubagentsDisabled: z$1.ZodBoolean;\n claudeCodeWorkflowsDisabled: z$1.ZodBoolean;\n}, z$1.core.$strict>;\ntype AppSettings = z$1.infer;\n\ndeclare const appKeybindingOverridesSchema: z$1.ZodArray;\n shortcut: z$1.ZodNullable>;\n}, z$1.core.$strict>>;\ntype AppKeybindingOverrides = z$1.infer;\n\ndeclare const appThemeSchema: z$1.ZodObject<{\n themeId: z$1.ZodString;\n customCss: z$1.ZodNullable;\n faviconColor: z$1.ZodEnum<{\n default: \"default\";\n red: \"red\";\n orange: \"orange\";\n yellow: \"yellow\";\n green: \"green\";\n teal: \"teal\";\n blue: \"blue\";\n purple: \"purple\";\n pink: \"pink\";\n }>;\n}, z$1.core.$strip>;\ntype AppTheme = z$1.infer;\n/**\n * The complete appearance selection a client sends when changing the palette\n * and/or favicon tint. The server validates `themeId` (built-in id or an\n * existing custom theme) and resolves the CSS from disk for custom themes.\n * Callers changing only one facet must carry the other facet forward explicitly.\n */\ndeclare const appThemeSelectionSchema: z$1.ZodObject<{\n themeId: z$1.ZodString;\n faviconColor: z$1.ZodEnum<{\n default: \"default\";\n red: \"red\";\n orange: \"orange\";\n yellow: \"yellow\";\n green: \"green\";\n teal: \"teal\";\n blue: \"blue\";\n purple: \"purple\";\n pink: \"pink\";\n }>;\n}, z$1.core.$strip>;\ntype AppThemeSelection = z$1.infer;\n\ndeclare const changedMessageSchema: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{\n type: z$1.ZodLiteral<\"changed\">;\n entity: z$1.ZodLiteral<\"thread\">;\n id: z$1.ZodOptional;\n metadata: z$1.ZodOptional;\n eventTypes: z$1.ZodOptional>>>>;\n hasPendingInteraction: z$1.ZodOptional;\n projectId: z$1.ZodOptional;\n }, z$1.core.$strict>>;\n changes: z$1.ZodReadonly>>;\n}, z$1.core.$strict>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"changed\">;\n entity: z$1.ZodLiteral<\"project\">;\n id: z$1.ZodOptional;\n changes: z$1.ZodReadonly>>;\n}, z$1.core.$strict>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"changed\">;\n entity: z$1.ZodLiteral<\"environment\">;\n id: z$1.ZodOptional;\n changes: z$1.ZodReadonly>>;\n}, z$1.core.$strict>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"changed\">;\n entity: z$1.ZodLiteral<\"host\">;\n id: z$1.ZodOptional;\n changes: z$1.ZodReadonly>>;\n}, z$1.core.$strict>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"changed\">;\n entity: z$1.ZodLiteral<\"system\">;\n changes: z$1.ZodReadonly>>;\n}, z$1.core.$strict>], \"entity\">;\ntype ChangedMessage = z$1.infer;\n\ndeclare const environmentSchema: z$1.ZodObject<{\n id: z$1.ZodString;\n name: z$1.ZodNullable;\n projectId: z$1.ZodString;\n hostId: z$1.ZodString;\n path: z$1.ZodNullable;\n managed: z$1.ZodBoolean;\n isGitRepo: z$1.ZodBoolean;\n isWorktree: z$1.ZodBoolean;\n workspaceProvisionType: z$1.ZodEnum<{\n unmanaged: \"unmanaged\";\n \"managed-worktree\": \"managed-worktree\";\n personal: \"personal\";\n }>;\n branchName: z$1.ZodNullable;\n baseBranch: z$1.ZodNullable;\n defaultBranch: z$1.ZodNullable;\n mergeBaseBranch: z$1.ZodNullable;\n status: z$1.ZodEnum<{\n error: \"error\";\n provisioning: \"provisioning\";\n ready: \"ready\";\n retiring: \"retiring\";\n destroying: \"destroying\";\n destroyed: \"destroyed\";\n }>;\n createdAt: z$1.ZodNumber;\n updatedAt: z$1.ZodNumber;\n}, z$1.core.$strip>;\ntype Environment = z$1.infer;\n\n/**\n * User-opt-in experiments (the Settings → Experiments toggles). Distinct from\n * `FeatureFlags`: flags are operator-set via env at server start, experiments\n * are user-toggled at runtime and persisted server-side so server-owned\n * policy (e.g. skill injection) can honor them.\n *\n * Every experiment defaults to off — opting in is the point.\n */\ndeclare const experimentsSchema: z$1.ZodObject<{\n claudeCodeMockCliTraffic: z$1.ZodBoolean;\n toolsHub: z$1.ZodBoolean;\n}, z$1.core.$strip>;\ntype Experiments = z$1.infer;\n\ndeclare const hostSchema: z$1.ZodObject<{\n id: z$1.ZodString;\n name: z$1.ZodString;\n type: z$1.ZodEnum<{\n persistent: \"persistent\";\n }>;\n status: z$1.ZodEnum<{\n connected: \"connected\";\n disconnected: \"disconnected\";\n }>;\n maxPermissionMode: z$1.ZodEnum<{\n full: \"full\";\n auto: \"auto\";\n \"accept-edits\": \"accept-edits\";\n }>;\n lastSeenAt: z$1.ZodNullable;\n lastRejectedProtocolVersion: z$1.ZodNullable;\n createdAt: z$1.ZodNumber;\n updatedAt: z$1.ZodNumber;\n}, z$1.core.$strip>;\ntype Host = z$1.infer;\n\ninterface JsonObject {\n [key: string]: JsonValue$1;\n}\ntype JsonValue$1 = string | number | boolean | null | JsonValue$1[] | JsonObject;\n\ndeclare const pendingInteractionResolutionSchema: z$1.ZodUnion;\n grantedPermissions: z$1.ZodNullable;\n }, z$1.core.$strip>>;\n fileSystem: z$1.ZodNullable;\n write: z$1.ZodArray;\n }, z$1.core.$strip>>;\n }, z$1.core.$strict>>;\n}, z$1.core.$strip>, z$1.ZodObject<{\n decision: z$1.ZodLiteral<\"allow_for_session\">;\n grantedPermissions: z$1.ZodNullable;\n }, z$1.core.$strip>>;\n fileSystem: z$1.ZodNullable;\n write: z$1.ZodArray;\n }, z$1.core.$strip>>;\n }, z$1.core.$strict>>;\n}, z$1.core.$strip>, z$1.ZodObject<{\n decision: z$1.ZodLiteral<\"deny\">;\n}, z$1.core.$strip>], \"decision\">, z$1.ZodObject<{\n kind: z$1.ZodLiteral<\"user_answer\">;\n answers: z$1.ZodRecord;\n freeText: z$1.ZodOptional;\n }, z$1.core.$strip>>;\n}, z$1.core.$strip>, z$1.ZodObject<{\n kind: z$1.ZodLiteral<\"plugin_submitted\">;\n}, z$1.core.$strip>]>;\ntype PendingInteractionResolution = z$1.infer;\ndeclare const providerPendingInteractionSchema: z$1.ZodObject<{\n id: z$1.ZodString;\n threadId: z$1.ZodString;\n status: z$1.ZodEnum<{\n pending: \"pending\";\n interrupted: \"interrupted\";\n resolving: \"resolving\";\n resolved: \"resolved\";\n }>;\n statusReason: z$1.ZodNullable;\n createdAt: z$1.ZodNumber;\n expiresAt: z$1.ZodOptional>;\n resolvedAt: z$1.ZodNullable;\n turnId: z$1.ZodString;\n providerId: z$1.ZodString;\n providerThreadId: z$1.ZodString;\n providerRequestId: z$1.ZodString;\n origin: z$1.ZodOptional;\n providerId: z$1.ZodString;\n providerThreadId: z$1.ZodString;\n providerRequestId: z$1.ZodString;\n }, z$1.core.$strip>>;\n payload: z$1.ZodUnion;\n subject: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{\n kind: z$1.ZodLiteral<\"command\">;\n itemId: z$1.ZodString;\n command: z$1.ZodString;\n cwd: z$1.ZodNullable;\n actions: z$1.ZodArray;\n command: z$1.ZodString;\n name: z$1.ZodString;\n path: z$1.ZodString;\n }, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"listFiles\">;\n command: z$1.ZodString;\n path: z$1.ZodNullable;\n }, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"search\">;\n command: z$1.ZodString;\n query: z$1.ZodNullable;\n path: z$1.ZodNullable;\n }, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"unknown\">;\n command: z$1.ZodString;\n }, z$1.core.$strip>], \"type\">>;\n sessionGrant: z$1.ZodNullable;\n }, z$1.core.$strip>>;\n fileSystem: z$1.ZodNullable;\n write: z$1.ZodArray;\n }, z$1.core.$strip>>;\n }, z$1.core.$strict>>;\n }, z$1.core.$strip>, z$1.ZodObject<{\n kind: z$1.ZodLiteral<\"file_change\">;\n itemId: z$1.ZodString;\n writeScope: z$1.ZodNullable;\n sessionGrant: z$1.ZodNullable;\n }, z$1.core.$strip>>;\n fileSystem: z$1.ZodNullable;\n write: z$1.ZodArray;\n }, z$1.core.$strip>>;\n }, z$1.core.$strict>>;\n }, z$1.core.$strip>, z$1.ZodObject<{\n kind: z$1.ZodLiteral<\"permission_grant\">;\n itemId: z$1.ZodString;\n toolName: z$1.ZodNullable;\n permissions: z$1.ZodObject<{\n network: z$1.ZodNullable;\n }, z$1.core.$strip>>;\n fileSystem: z$1.ZodNullable;\n write: z$1.ZodArray;\n }, z$1.core.$strip>>;\n }, z$1.core.$strict>;\n }, z$1.core.$strip>], \"kind\">;\n reason: z$1.ZodNullable;\n availableDecisions: z$1.ZodArray>;\n }, z$1.core.$strip>, z$1.ZodObject<{\n kind: z$1.ZodLiteral<\"user_question\">;\n questions: z$1.ZodArray;\n multiSelect: z$1.ZodBoolean;\n options: z$1.ZodOptional;\n }, z$1.core.$strip>>>;\n allowFreeText: z$1.ZodBoolean;\n }, z$1.core.$strip>>;\n }, z$1.core.$strip>]>;\n resolution: z$1.ZodNullable;\n grantedPermissions: z$1.ZodNullable;\n }, z$1.core.$strip>>;\n fileSystem: z$1.ZodNullable;\n write: z$1.ZodArray;\n }, z$1.core.$strip>>;\n }, z$1.core.$strict>>;\n }, z$1.core.$strip>, z$1.ZodObject<{\n decision: z$1.ZodLiteral<\"allow_for_session\">;\n grantedPermissions: z$1.ZodNullable;\n }, z$1.core.$strip>>;\n fileSystem: z$1.ZodNullable;\n write: z$1.ZodArray;\n }, z$1.core.$strip>>;\n }, z$1.core.$strict>>;\n }, z$1.core.$strip>, z$1.ZodObject<{\n decision: z$1.ZodLiteral<\"deny\">;\n }, z$1.core.$strip>], \"decision\">, z$1.ZodObject<{\n kind: z$1.ZodLiteral<\"user_answer\">;\n answers: z$1.ZodRecord;\n freeText: z$1.ZodOptional;\n }, z$1.core.$strip>>;\n }, z$1.core.$strip>]>>;\n}, z$1.core.$strip>;\ntype ProviderPendingInteraction = z$1.infer;\ndeclare const pluginPendingInteractionSchema: z$1.ZodObject<{\n id: z$1.ZodString;\n threadId: z$1.ZodString;\n status: z$1.ZodEnum<{\n pending: \"pending\";\n interrupted: \"interrupted\";\n resolving: \"resolving\";\n resolved: \"resolved\";\n }>;\n statusReason: z$1.ZodNullable;\n createdAt: z$1.ZodNumber;\n expiresAt: z$1.ZodOptional>;\n resolvedAt: z$1.ZodNullable;\n turnId: z$1.ZodNullable;\n origin: z$1.ZodObject<{\n kind: z$1.ZodLiteral<\"plugin\">;\n pluginId: z$1.ZodString;\n rendererId: z$1.ZodString;\n }, z$1.core.$strip>;\n payload: z$1.ZodObject<{\n kind: z$1.ZodLiteral<\"plugin\">;\n title: z$1.ZodString;\n data: z$1.ZodType>;\n }, z$1.core.$strip>;\n resolution: z$1.ZodNullable;\n }, z$1.core.$strip>>;\n}, z$1.core.$strip>;\ntype PluginPendingInteraction = z$1.infer;\ntype PendingInteraction = ProviderPendingInteraction | PluginPendingInteraction;\n\ndeclare const projectSourceSchema: z$1.ZodObject<{\n id: z$1.ZodString;\n projectId: z$1.ZodString;\n isDefault: z$1.ZodBoolean;\n createdAt: z$1.ZodNumber;\n updatedAt: z$1.ZodNumber;\n type: z$1.ZodLiteral<\"local_path\">;\n hostId: z$1.ZodString;\n path: z$1.ZodString;\n}, z$1.core.$strip>;\ntype ProjectSource = z$1.infer;\n\ndeclare const reasoningLevelSchema: z$1.ZodEnum<{\n none: \"none\";\n low: \"low\";\n medium: \"medium\";\n high: \"high\";\n xhigh: \"xhigh\";\n ultracode: \"ultracode\";\n max: \"max\";\n ultra: \"ultra\";\n}>;\ntype ReasoningLevel = z$1.infer;\ndeclare const serviceTierSchema: z$1.ZodEnum<{\n default: \"default\";\n fast: \"fast\";\n}>;\ntype ServiceTier = z$1.infer;\ndeclare const permissionModeSchema: z$1.ZodEnum<{\n full: \"full\";\n auto: \"auto\";\n \"accept-edits\": \"accept-edits\";\n}>;\ntype PermissionMode = z$1.infer;\ndeclare const promptInputSchema: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{\n visibility: z$1.ZodOptional>;\n type: z$1.ZodLiteral<\"text\">;\n text: z$1.ZodString;\n mentions: z$1.ZodDefault, z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{\n kind: z$1.ZodLiteral<\"thread\">;\n threadId: z$1.ZodString;\n projectId: z$1.ZodOptional;\n label: z$1.ZodString;\n }, z$1.core.$strip>, z$1.ZodObject<{\n kind: z$1.ZodLiteral<\"project\">;\n projectId: z$1.ZodString;\n label: z$1.ZodString;\n }, z$1.core.$strip>, z$1.ZodObject<{\n kind: z$1.ZodLiteral<\"section\">;\n sectionId: z$1.ZodString;\n label: z$1.ZodString;\n }, z$1.core.$strip>, z$1.ZodObject<{\n kind: z$1.ZodLiteral<\"path\">;\n source: z$1.ZodEnum<{\n workspace: \"workspace\";\n \"thread-storage\": \"thread-storage\";\n }>;\n entryKind: z$1.ZodEnum<{\n file: \"file\";\n directory: \"directory\";\n }>;\n path: z$1.ZodString;\n label: z$1.ZodString;\n }, z$1.core.$strip>, z$1.ZodObject<{\n kind: z$1.ZodLiteral<\"command\">;\n trigger: z$1.ZodEnum<{\n \"/\": \"/\";\n }>;\n name: z$1.ZodString;\n source: z$1.ZodEnum<{\n command: \"command\";\n skill: \"skill\";\n }>;\n origin: z$1.ZodEnum<{\n user: \"user\";\n project: \"project\";\n builtin: \"builtin\";\n }>;\n label: z$1.ZodString;\n argumentHint: z$1.ZodNullable;\n }, z$1.core.$strip>, z$1.ZodObject<{\n kind: z$1.ZodLiteral<\"plugin\">;\n pluginId: z$1.ZodString;\n icon: z$1.ZodOptional>;\n itemId: z$1.ZodString;\n label: z$1.ZodString;\n }, z$1.core.$strip>], \"kind\">>;\n }, z$1.core.$strip>>>;\n}, z$1.core.$strip>, z$1.ZodObject<{\n visibility: z$1.ZodOptional>;\n type: z$1.ZodLiteral<\"image\">;\n url: z$1.ZodString;\n}, z$1.core.$strip>, z$1.ZodObject<{\n visibility: z$1.ZodOptional>;\n type: z$1.ZodLiteral<\"localImage\">;\n path: z$1.ZodString;\n}, z$1.core.$strip>, z$1.ZodObject<{\n visibility: z$1.ZodOptional>;\n type: z$1.ZodLiteral<\"localFile\">;\n path: z$1.ZodString;\n name: z$1.ZodOptional;\n sizeBytes: z$1.ZodOptional;\n mimeType: z$1.ZodOptional;\n}, z$1.core.$strip>], \"type\">;\ntype PromptInput = z$1.infer;\ndeclare const resolvedThreadExecutionOptionsSchema: z$1.ZodObject<{\n seq: z$1.ZodOptional;\n model: z$1.ZodString;\n serviceTier: z$1.ZodEnum<{\n default: \"default\";\n fast: \"fast\";\n }>;\n reasoningLevel: z$1.ZodEnum<{\n none: \"none\";\n low: \"low\";\n medium: \"medium\";\n high: \"high\";\n xhigh: \"xhigh\";\n ultracode: \"ultracode\";\n max: \"max\";\n ultra: \"ultra\";\n }>;\n permissionMode: z$1.ZodEnum<{\n full: \"full\";\n auto: \"auto\";\n \"accept-edits\": \"accept-edits\";\n }>;\n source: z$1.ZodEnum<{\n \"client/thread/start\": \"client/thread/start\";\n \"client/turn/requested\": \"client/turn/requested\";\n \"client/turn/start\": \"client/turn/start\";\n }>;\n}, z$1.core.$strip>;\ntype ResolvedThreadExecutionOptions = z$1.infer;\ndeclare const projectExecutionDefaultsSchema: z$1.ZodObject<{\n providerId: z$1.ZodString;\n model: z$1.ZodString;\n serviceTier: z$1.ZodEnum<{\n default: \"default\";\n fast: \"fast\";\n }>;\n reasoningLevel: z$1.ZodEnum<{\n none: \"none\";\n low: \"low\";\n medium: \"medium\";\n high: \"high\";\n xhigh: \"xhigh\";\n ultracode: \"ultracode\";\n max: \"max\";\n ultra: \"ultra\";\n }>;\n permissionMode: z$1.ZodEnum<{\n full: \"full\";\n auto: \"auto\";\n \"accept-edits\": \"accept-edits\";\n }>;\n}, z$1.core.$strip>;\ntype ProjectExecutionDefaults = z$1.infer;\n\n/** All thread events — provider-originated or system-originated. */\ndeclare const threadEventSchema: z$1.ZodPipe;\n threadId: z$1.ZodString;\n}, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"thread/identity\">;\n threadId: z$1.ZodString;\n providerThreadId: z$1.ZodString;\n}, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"turn/started\">;\n threadId: z$1.ZodString;\n providerThreadId: z$1.ZodString;\n parentToolCallId: z$1.ZodOptional;\n}, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"turn/completed\">;\n threadId: z$1.ZodString;\n providerThreadId: z$1.ZodNullable;\n status: z$1.ZodEnum<{\n completed: \"completed\";\n failed: \"failed\";\n interrupted: \"interrupted\";\n }>;\n error: z$1.ZodOptional>;\n}, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"turn/input/accepted\">;\n threadId: z$1.ZodString;\n providerThreadId: z$1.ZodString;\n clientRequestId: z$1.ZodString;\n scope: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{\n kind: z$1.ZodLiteral<\"thread\">;\n }, z$1.core.$strip>, z$1.ZodObject<{\n kind: z$1.ZodLiteral<\"turn\">;\n turnId: z$1.ZodString;\n }, z$1.core.$strip>], \"kind\">;\n}, z$1.core.$strict>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"thread/name/updated\">;\n threadId: z$1.ZodString;\n providerThreadId: z$1.ZodString;\n threadName: z$1.ZodString;\n}, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"thread/compacted\">;\n threadId: z$1.ZodString;\n providerThreadId: z$1.ZodString;\n}, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"thread/goal/updated\">;\n threadId: z$1.ZodString;\n providerThreadId: z$1.ZodString;\n objective: z$1.ZodString;\n status: z$1.ZodEnum<{\n paused: \"paused\";\n active: \"active\";\n budgetLimited: \"budgetLimited\";\n complete: \"complete\";\n }>;\n tokenBudget: z$1.ZodNullable;\n tokensUsed: z$1.ZodNumber;\n timeUsedSeconds: z$1.ZodNumber;\n}, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"thread/goal/cleared\">;\n threadId: z$1.ZodString;\n providerThreadId: z$1.ZodString;\n}, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"item/started\">;\n threadId: z$1.ZodString;\n providerThreadId: z$1.ZodString;\n item: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{\n type: z$1.ZodLiteral<\"userMessage\">;\n id: z$1.ZodString;\n content: z$1.ZodArray;\n text: z$1.ZodString;\n }, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"image\">;\n url: z$1.ZodString;\n }, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"localImage\">;\n path: z$1.ZodString;\n }, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"localFile\">;\n path: z$1.ZodString;\n }, z$1.core.$strip>], \"type\">>;\n clientRequestId: z$1.ZodOptional;\n parentToolCallId: z$1.ZodOptional;\n }, z$1.core.$strict>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"agentMessage\">;\n id: z$1.ZodString;\n text: z$1.ZodString;\n parentToolCallId: z$1.ZodOptional;\n }, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"commandExecution\">;\n id: z$1.ZodString;\n command: z$1.ZodString;\n cwd: z$1.ZodString;\n status: z$1.ZodEnum<{\n pending: \"pending\";\n completed: \"completed\";\n failed: \"failed\";\n interrupted: \"interrupted\";\n }>;\n approvalStatus: z$1.ZodNullable>;\n aggregatedOutput: z$1.ZodOptional;\n exitCode: z$1.ZodOptional;\n durationMs: z$1.ZodOptional;\n truncation: z$1.ZodOptional>;\n result: z$1.ZodOptional>;\n resultText: z$1.ZodOptional>;\n }, z$1.core.$strip>>;\n parentToolCallId: z$1.ZodOptional;\n }, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"fileChange\">;\n id: z$1.ZodString;\n changes: z$1.ZodArray;\n movePath: z$1.ZodOptional;\n diff: z$1.ZodOptional;\n }, z$1.core.$strip>>;\n status: z$1.ZodEnum<{\n pending: \"pending\";\n completed: \"completed\";\n failed: \"failed\";\n interrupted: \"interrupted\";\n }>;\n approvalStatus: z$1.ZodNullable>;\n parentToolCallId: z$1.ZodOptional;\n }, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"webSearch\">;\n id: z$1.ZodString;\n queries: z$1.ZodArray;\n resultText: z$1.ZodNullable;\n parentToolCallId: z$1.ZodOptional;\n }, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"webFetch\">;\n id: z$1.ZodString;\n url: z$1.ZodString;\n prompt: z$1.ZodNullable;\n pattern: z$1.ZodNullable;\n resultText: z$1.ZodNullable;\n parentToolCallId: z$1.ZodOptional;\n }, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"imageView\">;\n id: z$1.ZodString;\n path: z$1.ZodString;\n parentToolCallId: z$1.ZodOptional;\n }, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"toolCall\">;\n id: z$1.ZodString;\n server: z$1.ZodOptional;\n tool: z$1.ZodString;\n arguments: z$1.ZodOptional>;\n statusLabels: z$1.ZodOptional>;\n status: z$1.ZodEnum<{\n pending: \"pending\";\n completed: \"completed\";\n failed: \"failed\";\n interrupted: \"interrupted\";\n }>;\n result: z$1.ZodOptional;\n error: z$1.ZodOptional;\n durationMs: z$1.ZodOptional;\n truncation: z$1.ZodOptional>;\n result: z$1.ZodOptional>;\n resultText: z$1.ZodOptional>;\n }, z$1.core.$strip>>;\n parentToolCallId: z$1.ZodOptional;\n }, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"reasoning\">;\n id: z$1.ZodString;\n summary: z$1.ZodArray;\n content: z$1.ZodArray;\n parentToolCallId: z$1.ZodOptional;\n }, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"plan\">;\n id: z$1.ZodString;\n text: z$1.ZodString;\n parentToolCallId: z$1.ZodOptional;\n }, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"contextCompaction\">;\n id: z$1.ZodString;\n parentToolCallId: z$1.ZodOptional;\n }, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"backgroundTask\">;\n id: z$1.ZodString;\n taskType: z$1.ZodString;\n description: z$1.ZodString;\n status: z$1.ZodEnum<{\n pending: \"pending\";\n completed: \"completed\";\n failed: \"failed\";\n interrupted: \"interrupted\";\n }>;\n taskStatus: z$1.ZodEnum<{\n pending: \"pending\";\n running: \"running\";\n paused: \"paused\";\n completed: \"completed\";\n failed: \"failed\";\n killed: \"killed\";\n stopped: \"stopped\";\n }>;\n skipTranscript: z$1.ZodBoolean;\n workflowName: z$1.ZodOptional;\n workflow: z$1.ZodOptional;\n }, z$1.core.$strip>>;\n agents: z$1.ZodArray;\n model: z$1.ZodString;\n attempt: z$1.ZodNumber;\n cached: z$1.ZodBoolean;\n lastProgressAt: z$1.ZodNumber;\n phaseIndex: z$1.ZodOptional;\n phaseTitle: z$1.ZodOptional;\n agentType: z$1.ZodOptional;\n isolation: z$1.ZodOptional;\n queuedAt: z$1.ZodOptional;\n startedAt: z$1.ZodOptional;\n lastToolName: z$1.ZodOptional;\n lastToolSummary: z$1.ZodOptional;\n promptPreview: z$1.ZodOptional;\n resultPreview: z$1.ZodOptional;\n error: z$1.ZodOptional;\n tokens: z$1.ZodOptional;\n toolCalls: z$1.ZodOptional;\n durationMs: z$1.ZodOptional;\n }, z$1.core.$strip>>;\n }, z$1.core.$strip>>;\n usage: z$1.ZodOptional>;\n summary: z$1.ZodOptional;\n error: z$1.ZodOptional;\n outputFile: z$1.ZodOptional;\n parentToolCallId: z$1.ZodOptional;\n }, z$1.core.$strip>], \"type\">;\n}, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"item/completed\">;\n threadId: z$1.ZodString;\n providerThreadId: z$1.ZodString;\n item: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{\n type: z$1.ZodLiteral<\"userMessage\">;\n id: z$1.ZodString;\n content: z$1.ZodArray;\n text: z$1.ZodString;\n }, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"image\">;\n url: z$1.ZodString;\n }, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"localImage\">;\n path: z$1.ZodString;\n }, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"localFile\">;\n path: z$1.ZodString;\n }, z$1.core.$strip>], \"type\">>;\n clientRequestId: z$1.ZodOptional;\n parentToolCallId: z$1.ZodOptional;\n }, z$1.core.$strict>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"agentMessage\">;\n id: z$1.ZodString;\n text: z$1.ZodString;\n parentToolCallId: z$1.ZodOptional;\n }, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"commandExecution\">;\n id: z$1.ZodString;\n command: z$1.ZodString;\n cwd: z$1.ZodString;\n status: z$1.ZodEnum<{\n pending: \"pending\";\n completed: \"completed\";\n failed: \"failed\";\n interrupted: \"interrupted\";\n }>;\n approvalStatus: z$1.ZodNullable>;\n aggregatedOutput: z$1.ZodOptional;\n exitCode: z$1.ZodOptional;\n durationMs: z$1.ZodOptional;\n truncation: z$1.ZodOptional>;\n result: z$1.ZodOptional>;\n resultText: z$1.ZodOptional>;\n }, z$1.core.$strip>>;\n parentToolCallId: z$1.ZodOptional;\n }, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"fileChange\">;\n id: z$1.ZodString;\n changes: z$1.ZodArray;\n movePath: z$1.ZodOptional;\n diff: z$1.ZodOptional;\n }, z$1.core.$strip>>;\n status: z$1.ZodEnum<{\n pending: \"pending\";\n completed: \"completed\";\n failed: \"failed\";\n interrupted: \"interrupted\";\n }>;\n approvalStatus: z$1.ZodNullable>;\n parentToolCallId: z$1.ZodOptional;\n }, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"webSearch\">;\n id: z$1.ZodString;\n queries: z$1.ZodArray;\n resultText: z$1.ZodNullable;\n parentToolCallId: z$1.ZodOptional;\n }, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"webFetch\">;\n id: z$1.ZodString;\n url: z$1.ZodString;\n prompt: z$1.ZodNullable;\n pattern: z$1.ZodNullable;\n resultText: z$1.ZodNullable;\n parentToolCallId: z$1.ZodOptional;\n }, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"imageView\">;\n id: z$1.ZodString;\n path: z$1.ZodString;\n parentToolCallId: z$1.ZodOptional;\n }, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"toolCall\">;\n id: z$1.ZodString;\n server: z$1.ZodOptional;\n tool: z$1.ZodString;\n arguments: z$1.ZodOptional>;\n statusLabels: z$1.ZodOptional>;\n status: z$1.ZodEnum<{\n pending: \"pending\";\n completed: \"completed\";\n failed: \"failed\";\n interrupted: \"interrupted\";\n }>;\n result: z$1.ZodOptional;\n error: z$1.ZodOptional;\n durationMs: z$1.ZodOptional;\n truncation: z$1.ZodOptional>;\n result: z$1.ZodOptional>;\n resultText: z$1.ZodOptional>;\n }, z$1.core.$strip>>;\n parentToolCallId: z$1.ZodOptional;\n }, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"reasoning\">;\n id: z$1.ZodString;\n summary: z$1.ZodArray;\n content: z$1.ZodArray;\n parentToolCallId: z$1.ZodOptional;\n }, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"plan\">;\n id: z$1.ZodString;\n text: z$1.ZodString;\n parentToolCallId: z$1.ZodOptional;\n }, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"contextCompaction\">;\n id: z$1.ZodString;\n parentToolCallId: z$1.ZodOptional;\n }, z$1.core.$strip>, z$1.ZodObject<{\n type: z$1.ZodLiteral<\"backgroundTask\">;\n id: z$1.ZodString;\n taskType: z$1.ZodString;\n description: z$1.ZodString;\n status: z$1.ZodEnum<{\n pending: \"pending\";\n completed: \"completed\";\n failed: \"failed\";\n interrupted: \"interrupted\";\n }>;\n taskStatus: z$1.ZodEnum<{\n pending: \"pending\";\n running: \"running\";\n paused: \"paused\";\n completed: \"completed\";\n failed: \"failed\";\n killed: \"killed\";\n stopped: \"stopped\";\n }>;\n skipTranscript: z$1.ZodBoolean;\n workflowName: z$1.ZodOptional;\n workflow: z$1.ZodOptional;\n }, z$1.core.$strip>>;\n agents: z$1.ZodArray;\n model: z$1.ZodString;\n attempt: z$1.ZodNumber;\n cached: z$1.ZodBoolean;\n lastProgressAt: z$1.ZodNumber;\n phaseIndex: z$1.ZodOptional;\n phaseTitle: z$1.ZodOptional;\n agentType: z$1.ZodOptional;\n isolation: z$1.ZodOptional;\n queuedAt: z$1.ZodOptional;\n startedAt: z$1.ZodOptional;\n lastToolName: z$1.ZodOptional;\n lastToolSummary: z$1.ZodOptional;\n promptPreview: z$1.ZodOptional;\n resultPreview: z$1.ZodOptional;\n error: z$1.ZodOptional;\n tokens: z$1.ZodOptional;\n toolCalls: z$1.ZodOptional