Merge remote-tracking branch 'origin/v2/main' into 1579-wave-1

# Conflicts:
#	clients/cli/package-lock.json
#	clients/cli/package.json
#	clients/web/src/App.tsx
#	clients/web/src/lib/environmentFactory.test.ts
#	clients/web/src/lib/environmentFactory.ts
#	clients/web/src/lib/remoteOAuthStorage.test.ts
#	clients/web/src/lib/remoteOAuthStorage.ts
#	clients/web/src/test/core/auth/connection-state.test.ts
#	clients/web/src/test/core/auth/providers.test.ts
#	clients/web/src/test/core/auth/storage-browser.test.ts
#	clients/web/src/test/core/auth/storage-remote.test.ts
#	clients/web/src/test/core/mcp/oauthManager.test.ts
#	clients/web/src/test/integration/auth/node/storage.test.ts
#	clients/web/src/test/integration/storage/adapters.test.ts
#	clients/web/src/utils/clearServerOAuthState.test.ts
#	clients/web/src/utils/clearServerOAuthState.ts
#	core/auth/connection-state.ts
#	core/auth/oauth-storage.ts
#	core/auth/providers.ts
#	core/auth/storage.ts
#	core/auth/store.ts
#	core/storage/adapters/remote-storage.ts
This commit is contained in:
cliffhall 2026-07-09 09:41:08 -04:00
commit 32a3610737
6 changed files with 5046 additions and 3271 deletions

View file

@ -111,4 +111,48 @@ describe("HistoryScreen", () => {
expect.objectContaining({ methodFilter: undefined }),
);
});
it("renders a pin-as-column button when onPin is provided and invokes it", async () => {
const user = userEvent.setup();
const onPin = vi.fn();
renderWithMantine(<HistoryScreen {...baseProps} onPin={onPin} />);
await user.click(screen.getByRole("button", { name: "Pin as column" }));
expect(onPin).toHaveBeenCalledTimes(1);
});
it("drops the filter sidebar when embedded, keeping the request list", () => {
renderWithMantine(<HistoryScreen {...baseProps} embedded />);
expect(screen.getByText("Requests")).toBeInTheDocument();
// The sidebar (HistoryControls, with its Search box) is not rendered.
expect(screen.queryByPlaceholderText("Search...")).toBeNull();
});
it("applies the search text but ignores the method filter when embedded", () => {
renderWithMantine(
<HistoryScreen
{...baseProps}
ui={{
...EMPTY_HISTORY_UI,
// Method filter would exclude the tools/list entries on the full-size
// screen...
methodFilter: "resources/list",
// ...but the column search matches them.
search: "tools",
}}
embedded
/>,
);
expect(screen.queryByText("No request history")).toBeNull();
});
it("hides entries not matching the column search when embedded", () => {
renderWithMantine(
<HistoryScreen
{...baseProps}
ui={{ ...EMPTY_HISTORY_UI, search: "zzz-no-match" }}
embedded
/>,
);
expect(screen.getByText("No request history")).toBeInTheDocument();
});
});

View file

@ -25,6 +25,10 @@ export interface HistoryScreenProps {
onSortChange: (next: SortDirection) => void;
compact: boolean;
onToggleCompact: () => void;
/** See LoggingScreen: shows a "pin as column" button when set (#1616). */
onPin?: () => void;
/** See LoggingScreen: fills the parent height and drops the filter sidebar. */
embedded?: boolean;
}
// Search text, method filter, and per-direction visibility — controlled by the
@ -69,6 +73,8 @@ export function HistoryScreen({
onSortChange,
compact,
onToggleCompact,
onPin,
embedded = false,
}: HistoryScreenProps) {
const { search, methodFilter, visibleDirections } = ui;
@ -101,23 +107,25 @@ export function HistoryScreen({
}, [ui, visibleDirections, onUiChange]);
return (
<ScreenLayout>
<Sidebar>
<SidebarCard>
<HistoryControls
searchText={search}
methodFilter={methodFilter}
availableMethods={availableMethods}
visibleDirections={visibleDirections}
onSearchChange={(value) => onUiChange({ ...ui, search: value })}
onMethodFilterChange={(value) =>
onUiChange({ ...ui, methodFilter: value })
}
onToggleDirection={handleToggleDirection}
onToggleAllDirections={handleToggleAllDirections}
/>
</SidebarCard>
</Sidebar>
<ScreenLayout h={embedded ? "100%" : undefined}>
{embedded ? null : (
<Sidebar>
<SidebarCard>
<HistoryControls
searchText={search}
methodFilter={methodFilter}
availableMethods={availableMethods}
visibleDirections={visibleDirections}
onSearchChange={(value) => onUiChange({ ...ui, search: value })}
onMethodFilterChange={(value) =>
onUiChange({ ...ui, methodFilter: value })
}
onToggleDirection={handleToggleDirection}
onToggleAllDirections={handleToggleAllDirections}
/>
</SidebarCard>
</Sidebar>
)}
<HistoryListPanel
entries={entries}
pinnedIds={pinnedIds}
@ -134,6 +142,8 @@ export function HistoryScreen({
onSortChange={onSortChange}
compact={compact}
onToggleCompact={onToggleCompact}
onPin={onPin}
embedded={embedded}
/>
</ScreenLayout>
);

View file

@ -131,4 +131,62 @@ describe("LoggingScreen", () => {
await user.click(screen.getByRole("button", { name: "Set" }));
expect(onSetLevel).toHaveBeenCalledWith("info");
});
it("renders a pin-as-column button when onPin is provided and invokes it", async () => {
const user = userEvent.setup();
const onPin = vi.fn();
renderWithMantine(<LoggingScreen {...baseProps} onPin={onPin} />);
await user.click(screen.getByRole("button", { name: "Pin as column" }));
expect(onPin).toHaveBeenCalledTimes(1);
});
it("drops the filter sidebar when embedded, keeping the stream", () => {
renderWithMantine(<LoggingScreen {...baseProps} embedded />);
expect(screen.getByText("Log Stream")).toBeInTheDocument();
// The sidebar (LogControls, with its Set button) is not rendered.
expect(screen.queryByRole("button", { name: "Set" })).toBeNull();
});
it("applies the search text but ignores the level filter when embedded", () => {
const entries = [
{
receivedAt: new Date(),
params: { level: "info" as const, data: "hello" },
},
];
renderWithMantine(
<LoggingScreen
{...baseProps}
entries={entries}
ui={{
...EMPTY_LOGS_UI,
// The level filter would hide this entry on the full-size screen...
visibleLevels: { ...EMPTY_LOGS_UI.visibleLevels, info: false },
// ...but the column search matches it.
filterText: "hello",
}}
embedded
/>,
);
// Search matches + level filter is ignored → the entry shows.
expect(screen.getByText("hello")).toBeInTheDocument();
});
it("hides entries not matching the column search when embedded", () => {
const entries = [
{
receivedAt: new Date(),
params: { level: "info" as const, data: "hello" },
},
];
renderWithMantine(
<LoggingScreen
{...baseProps}
entries={entries}
ui={{ ...EMPTY_LOGS_UI, filterText: "zzz-no-match" }}
embedded
/>,
);
expect(screen.queryByText("hello")).toBeNull();
});
});

View file

@ -16,6 +16,17 @@ export interface LoggingScreenProps {
onExport: () => void;
sortDirection: SortDirection;
onSortChange: (next: SortDirection) => void;
/**
* When set, the stream panel shows a "pin as column" button (#1616). Omitted
* when already embedded, or when pinning isn't currently available.
*/
onPin?: () => void;
/**
* True when rendered inside the monitoring column: the screen fills its
* parent's height (instead of the viewport calc) and drops the filter
* sidebar so the narrow column is stream-only.
*/
embedded?: boolean;
}
// Filter text + visible-level set — controlled by the parent (App) as one
@ -52,6 +63,8 @@ export function LoggingScreen({
onExport,
sortDirection,
onSortChange,
onPin,
embedded = false,
}: LoggingScreenProps) {
const { filterText, visibleLevels } = ui;
@ -71,20 +84,24 @@ export function LoggingScreen({
}
return (
<ScreenLayout>
<Sidebar>
<SidebarCard>
<LogControls
currentLevel={currentLevel}
filterText={filterText}
visibleLevels={visibleLevels}
onSetLevel={onSetLevel}
onFilterChange={(value) => onUiChange({ ...ui, filterText: value })}
onToggleLevel={handleToggleLevel}
onToggleAllLevels={handleToggleAllLevels}
/>
</SidebarCard>
</Sidebar>
<ScreenLayout h={embedded ? "100%" : undefined}>
{embedded ? null : (
<Sidebar>
<SidebarCard>
<LogControls
currentLevel={currentLevel}
filterText={filterText}
visibleLevels={visibleLevels}
onSetLevel={onSetLevel}
onFilterChange={(value) =>
onUiChange({ ...ui, filterText: value })
}
onToggleLevel={handleToggleLevel}
onToggleAllLevels={handleToggleAllLevels}
/>
</SidebarCard>
</Sidebar>
)}
<LogStreamPanel
entries={entries}
filterText={filterText}
@ -93,6 +110,8 @@ export function LoggingScreen({
onExport={onExport}
sortDirection={sortDirection}
onSortChange={onSortChange}
onPin={onPin}
embedded={embedded}
/>
</ScreenLayout>
);

View file

@ -163,4 +163,49 @@ describe("NetworkScreen", () => {
expect(screen.getByRole("button", { name: "Clear" })).toBeDisabled();
expect(screen.getByRole("button", { name: "Export" })).toBeDisabled();
});
it("renders a pin-as-column button when onPin is provided and invokes it", async () => {
const user = userEvent.setup();
const onPin = vi.fn();
renderWithMantine(<NetworkScreen {...baseProps} onPin={onPin} />);
await user.click(screen.getByRole("button", { name: "Pin as column" }));
expect(onPin).toHaveBeenCalledTimes(1);
});
it("drops the filter sidebar when embedded, keeping the request list", () => {
renderWithMantine(<NetworkScreen {...baseProps} embedded />);
// The sidebar (NetworkControls, with its Search box) is not rendered.
expect(screen.queryByPlaceholderText("Search...")).toBeNull();
// The stream toolbar (Export) is still present.
expect(screen.getByRole("button", { name: "Export" })).toBeInTheDocument();
});
it("applies the search text but ignores the category filter when embedded", () => {
renderWithMantine(
<NetworkScreen
{...baseProps}
ui={{
...EMPTY_NETWORK_UI,
// Category filter would hide the transport entry on the full-size
// screen...
visibleCategories: { auth: true, transport: false },
// ...but the column search matches it.
filterText: "example.com",
}}
embedded
/>,
);
expect(screen.queryByText("No network requests")).toBeNull();
});
it("hides entries not matching the column search when embedded", () => {
renderWithMantine(
<NetworkScreen
{...baseProps}
ui={{ ...EMPTY_NETWORK_UI, filterText: "zzz-no-match" }}
embedded
/>,
);
expect(screen.getByText("No network requests")).toBeInTheDocument();
});
});

View file

@ -21,6 +21,10 @@ export interface NetworkScreenProps {
onSortChange: (next: SortDirection) => void;
compact: boolean;
onToggleCompact: () => void;
/** See LoggingScreen: shows a "pin as column" button when set (#1616). */
onPin?: () => void;
/** See LoggingScreen: fills the parent height and drops the filter sidebar. */
embedded?: boolean;
}
// Filter text + visible-category set — controlled by the parent (App) as one
@ -57,6 +61,8 @@ export function NetworkScreen({
onSortChange,
compact,
onToggleCompact,
onPin,
embedded = false,
}: NetworkScreenProps) {
const { filterText, visibleCategories } = ui;
@ -81,18 +87,22 @@ export function NetworkScreen({
}
return (
<ScreenLayout>
<Sidebar>
<SidebarCard>
<NetworkControls
filterText={filterText}
visibleCategories={visibleCategories}
onFilterChange={(value) => onUiChange({ ...ui, filterText: value })}
onToggleCategory={handleToggleCategory}
onToggleAllCategories={handleToggleAllCategories}
/>
</SidebarCard>
</Sidebar>
<ScreenLayout h={embedded ? "100%" : undefined}>
{embedded ? null : (
<Sidebar>
<SidebarCard>
<NetworkControls
filterText={filterText}
visibleCategories={visibleCategories}
onFilterChange={(value) =>
onUiChange({ ...ui, filterText: value })
}
onToggleCategory={handleToggleCategory}
onToggleAllCategories={handleToggleAllCategories}
/>
</SidebarCard>
</Sidebar>
)}
<NetworkStreamPanel
entries={entries}
filterText={filterText}
@ -103,6 +113,8 @@ export function NetworkScreen({
onSortChange={onSortChange}
compact={compact}
onToggleCompact={onToggleCompact}
onPin={onPin}
embedded={embedded}
/>
</ScreenLayout>
);