Archived
fix(ui): keep warning toggle visible
This commit is contained in:
@@ -2,4 +2,4 @@
|
|||||||
"@jmfederico/pi-web": patch
|
"@jmfederico/pi-web": patch
|
||||||
---
|
---
|
||||||
|
|
||||||
Let users minimise session warnings into an accessible status-bar count that stays minimised when they revisit the session, and replace warning emoji with SVG icons.
|
Let users minimise session warnings with an accessible status-bar count that remains available as an expand/collapse toggle, remembers its state per session, and uses SVG warning icons.
|
||||||
|
|||||||
@@ -141,22 +141,6 @@ describe("ChatView session-warning dismiss wiring", () => {
|
|||||||
expect(onDismissWarning).toHaveBeenCalledExactlyOnceWith("anthropicExtraUsage");
|
expect(onDismissWarning).toHaveBeenCalledExactlyOnceWith("anthropicExtraUsage");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Escape hatch: this verifies the collapse button's Lit callback wiring in
|
|
||||||
// the node test environment, anchored to its stable semantic class marker.
|
|
||||||
it("invokes onCollapseWarnings from the visible warning area", () => {
|
|
||||||
const view = withStatus(new ChatView(), warningStatus([
|
|
||||||
{ severity: "warning", message: "subscription auth is active" },
|
|
||||||
]));
|
|
||||||
const onCollapseWarnings = vi.fn();
|
|
||||||
view.onCollapseWarnings = onCollapseWarnings;
|
|
||||||
|
|
||||||
const rendered = renderWarnings(view);
|
|
||||||
if (rendered === null) throw new Error("expected a warnings banner");
|
|
||||||
templateEventHandlerAfterMarker(rendered, "session-warnings-collapse")(new Event("click"));
|
|
||||||
|
|
||||||
expect(onCollapseWarnings).toHaveBeenCalledOnce();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("removes the warning area while presentation is collapsed or there are no warnings", () => {
|
it("removes the warning area while presentation is collapsed or there are no warnings", () => {
|
||||||
const view = withStatus(new ChatView(), warningStatus([
|
const view = withStatus(new ChatView(), warningStatus([
|
||||||
{ severity: "warning", message: "subscription auth is active" },
|
{ severity: "warning", message: "subscription auth is active" },
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { LitElement, html, svg } from "lit";
|
import { LitElement, html } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators.js";
|
import { customElement, property, query, state } from "lit/decorators.js";
|
||||||
import { repeat } from "lit/directives/repeat.js";
|
import { repeat } from "lit/directives/repeat.js";
|
||||||
import { ChatDisclosureController } from "../chatDisclosure";
|
import { ChatDisclosureController } from "../chatDisclosure";
|
||||||
@@ -199,7 +199,6 @@ export class ChatView extends LitElement {
|
|||||||
@property({ attribute: false }) onDismissNotification?: (notificationId: string) => void;
|
@property({ attribute: false }) onDismissNotification?: (notificationId: string) => void;
|
||||||
@property({ attribute: false }) onDismissAllNotifications?: () => void;
|
@property({ attribute: false }) onDismissAllNotifications?: () => void;
|
||||||
@property({ type: Boolean }) warningsVisible = true;
|
@property({ type: Boolean }) warningsVisible = true;
|
||||||
@property({ attribute: false }) onCollapseWarnings?: () => void;
|
|
||||||
@property({ attribute: false }) onLoadMore?: () => void;
|
@property({ attribute: false }) onLoadMore?: () => void;
|
||||||
@query(".chat") private chat?: HTMLDivElement;
|
@query(".chat") private chat?: HTMLDivElement;
|
||||||
@query("dialog.image-zoom") private imageZoomDialog?: HTMLDialogElement;
|
@query("dialog.image-zoom") private imageZoomDialog?: HTMLDialogElement;
|
||||||
@@ -253,9 +252,6 @@ export class ChatView extends LitElement {
|
|||||||
private readonly handleClearServerQueue = (): void => {
|
private readonly handleClearServerQueue = (): void => {
|
||||||
this.onClearServerQueue?.();
|
this.onClearServerQueue?.();
|
||||||
};
|
};
|
||||||
private readonly handleCollapseWarnings = (): void => {
|
|
||||||
this.onCollapseWarnings?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
override connectedCallback(): void {
|
override connectedCallback(): void {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
@@ -515,24 +511,6 @@ export class ChatView extends LitElement {
|
|||||||
if (!this.warningsVisible || rows.length === 0) return null;
|
if (!this.warningsVisible || rows.length === 0) return null;
|
||||||
return html`
|
return html`
|
||||||
<aside class="session-warnings" role="alert" aria-live="polite">
|
<aside class="session-warnings" role="alert" aria-live="polite">
|
||||||
${this.onCollapseWarnings === undefined ? null : html`
|
|
||||||
<div class="session-warnings-controls">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="session-warnings-collapse"
|
|
||||||
title="Minimise warnings"
|
|
||||||
aria-label="Minimise warnings"
|
|
||||||
@click=${this.handleCollapseWarnings}
|
|
||||||
>
|
|
||||||
${svg`
|
|
||||||
<svg class="session-warnings-collapse-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
|
||||||
<path d="m18 15-6-6-6 6"></path>
|
|
||||||
</svg>
|
|
||||||
`}
|
|
||||||
<span>Minimise</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
`}
|
|
||||||
${rows.map((row) => {
|
${rows.map((row) => {
|
||||||
const dismissId = row.dismissId;
|
const dismissId = row.dismissId;
|
||||||
return html`
|
return html`
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import { sessionCleanupRequestKey, sessionCleanupUnavailableMessage } from "../s
|
|||||||
import { selectedNotificationView } from "../sessionNotifications";
|
import { selectedNotificationView } from "../sessionNotifications";
|
||||||
import { hasAuthoritativeSessionPersistence as runtimeHasAuthoritativeSessionPersistence } from "../sessionPersistence";
|
import { hasAuthoritativeSessionPersistence as runtimeHasAuthoritativeSessionPersistence } from "../sessionPersistence";
|
||||||
import { SessionUnreadController } from "../sessionUnread";
|
import { SessionUnreadController } from "../sessionUnread";
|
||||||
import { collapseSessionWarnings, initialSessionWarningVisibilityState, reconcileSessionWarningVisibility, restoreSessionWarnings } from "../sessionWarningVisibility";
|
import { initialSessionWarningVisibilityState, reconcileSessionWarningVisibility, toggleSessionWarnings } from "../sessionWarningVisibility";
|
||||||
import { RealtimeSocket, type BrowserRealtimeEvent } from "../sessionSocket";
|
import { RealtimeSocket, type BrowserRealtimeEvent } from "../sessionSocket";
|
||||||
import type { PiWebPluginRegistration, PluginMachine, PluginPromptEditor, QualifiedContributionId, QualifiedThemeContribution, QualifiedThemePairContribution, QualifiedWorkspacePanelContribution, PluginRuntimeContext, TerminalCommandRunsInternalRuntime, WorkspaceFiles, WorkspaceHost, WorkspaceLabelContext, WorkspaceLabelItem, WorkspacePanelContext } from "../plugins/types";
|
import type { PiWebPluginRegistration, PluginMachine, PluginPromptEditor, QualifiedContributionId, QualifiedThemeContribution, QualifiedThemePairContribution, QualifiedWorkspacePanelContribution, PluginRuntimeContext, TerminalCommandRunsInternalRuntime, WorkspaceFiles, WorkspaceHost, WorkspaceLabelContext, WorkspaceLabelItem, WorkspacePanelContext } from "../plugins/types";
|
||||||
import { CLASSIC_THEME_ID, DEFAULT_THEME_PREFERENCE, applyPiWebTheme, findThemePairForTheme, readStoredThemePreference, resolveThemePreference, writeStoredThemePreference, type ThemePreference, type ThemePreferenceResolution } from "../theme";
|
import { CLASSIC_THEME_ID, DEFAULT_THEME_PREFERENCE, applyPiWebTheme, findThemePairForTheme, readStoredThemePreference, resolveThemePreference, writeStoredThemePreference, type ThemePreference, type ThemePreferenceResolution } from "../theme";
|
||||||
@@ -2108,15 +2108,8 @@ export class PiWebApp extends LitElement {
|
|||||||
void this.notifications.dismissAll();
|
void this.notifications.dismissAll();
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly handleCollapseWarnings = (): void => {
|
private readonly handleToggleWarnings = (): void => {
|
||||||
const next = collapseSessionWarnings(this.sessionWarningVisibility);
|
const next = toggleSessionWarnings(this.sessionWarningVisibility);
|
||||||
if (next === this.sessionWarningVisibility) return;
|
|
||||||
this.sessionWarningVisibility = next;
|
|
||||||
this.requestUpdate();
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly handleRestoreWarnings = (): void => {
|
|
||||||
const next = restoreSessionWarnings(this.sessionWarningVisibility);
|
|
||||||
if (next === this.sessionWarningVisibility) return;
|
if (next === this.sessionWarningVisibility) return;
|
||||||
this.sessionWarningVisibility = next;
|
this.sessionWarningVisibility = next;
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
@@ -2132,16 +2125,14 @@ export class PiWebApp extends LitElement {
|
|||||||
|
|
||||||
private renderChatView(state: AppState, session: SessionInfo) {
|
private renderChatView(state: AppState, session: SessionInfo) {
|
||||||
return html`
|
return html`
|
||||||
<chat-view .sessionId=${session.id} .messages=${state.messages} .messageStart=${state.messagePageStart} .messageEnd=${state.messagePageEnd} .messageTotal=${state.messagePageTotal} .hasMore=${state.messagePageStart > 0} .loadingMore=${state.isLoadingEarlierMessages} .isSendingPrompt=${state.sendingPrompts[session.id] === true} .isCompacting=${state.status?.isCompacting === true} .pendingMessageCount=${state.status?.pendingMessageCount ?? 0} .clientQueuedMessages=${state.clientQueuedSessionMessages[session.id] ?? []} .status=${state.status} .activity=${state.activity} .notificationInbox=${selectedNotificationView(state.selectedNotificationInbox)} .canClearServerQueue=${this.canClearServerQueue()} .onClearServerQueue=${this.handleClearServerQueue} .onDismissWarning=${this.handleDismissWarning} .onDismissNotification=${this.handleDismissNotification} .onDismissAllNotifications=${this.handleDismissAllNotifications} .warningsVisible=${!this.sessionWarningVisibility.collapsed} .onCollapseWarnings=${this.handleCollapseWarnings} .onLoadMore=${() => this.withChatPrependTransition(() => this.sessions.loadEarlierMessages())}></chat-view>
|
<chat-view .sessionId=${session.id} .messages=${state.messages} .messageStart=${state.messagePageStart} .messageEnd=${state.messagePageEnd} .messageTotal=${state.messagePageTotal} .hasMore=${state.messagePageStart > 0} .loadingMore=${state.isLoadingEarlierMessages} .isSendingPrompt=${state.sendingPrompts[session.id] === true} .isCompacting=${state.status?.isCompacting === true} .pendingMessageCount=${state.status?.pendingMessageCount ?? 0} .clientQueuedMessages=${state.clientQueuedSessionMessages[session.id] ?? []} .status=${state.status} .activity=${state.activity} .notificationInbox=${selectedNotificationView(state.selectedNotificationInbox)} .canClearServerQueue=${this.canClearServerQueue()} .onClearServerQueue=${this.handleClearServerQueue} .onDismissWarning=${this.handleDismissWarning} .onDismissNotification=${this.handleDismissNotification} .onDismissAllNotifications=${this.handleDismissAllNotifications} .warningsVisible=${!this.sessionWarningVisibility.collapsed} .onLoadMore=${() => this.withChatPrependTransition(() => this.sessions.loadEarlierMessages())}></chat-view>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderStatusBar(state: AppState) {
|
private renderStatusBar(state: AppState) {
|
||||||
const collapsedWarningCount = this.sessionWarningVisibility.collapsed
|
const warningCount = this.sessionWarningVisibility.warningCount;
|
||||||
? this.sessionWarningVisibility.warningCount
|
|
||||||
: 0;
|
|
||||||
return html`
|
return html`
|
||||||
<status-bar .status=${state.status} .collapsedWarningCount=${collapsedWarningCount} .onRestoreWarnings=${this.handleRestoreWarnings}></status-bar>
|
<status-bar .status=${state.status} .warningCount=${warningCount} .warningsExpanded=${warningCount > 0 && !this.sessionWarningVisibility.collapsed} .onToggleWarnings=${this.handleToggleWarnings}></status-bar>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,21 +13,24 @@ afterEach(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("PiWebApp session-warning visibility wiring", () => {
|
describe("PiWebApp session-warning visibility wiring", () => {
|
||||||
it("collapses the existing warning area and restores it from the status bar", () => {
|
it("keeps the warning control present and toggles the warning area from the status bar", () => {
|
||||||
const app = createApp();
|
const app = createApp();
|
||||||
const state = stateWithWarnings();
|
const state = stateWithWarnings();
|
||||||
setAppState(app, state);
|
setAppState(app, state);
|
||||||
syncWarningVisibility(app);
|
syncWarningVisibility(app);
|
||||||
|
|
||||||
const visibleChat = renderChatView(app, state);
|
const visibleStatusBar = renderStatusBar(app, state);
|
||||||
expect(templateValueAfterMarker(visibleChat, ".warningsVisible=")).toBe(true);
|
expect(templateValueAfterMarker(renderChatView(app, state), ".warningsVisible=")).toBe(true);
|
||||||
|
expect(templateValueAfterMarker(visibleStatusBar, ".warningCount=")).toBe(2);
|
||||||
|
expect(templateValueAfterMarker(visibleStatusBar, ".warningsExpanded=")).toBe(true);
|
||||||
|
|
||||||
const collapse = templateCallbackAfterMarker(visibleChat, ".onCollapseWarnings=");
|
const toggle = templateCallbackAfterMarker(visibleStatusBar, ".onToggleWarnings=");
|
||||||
collapse();
|
toggle();
|
||||||
|
|
||||||
const collapsedChat = renderChatView(app, state);
|
const collapsedStatusBar = renderStatusBar(app, state);
|
||||||
expect(templateValueAfterMarker(collapsedChat, ".warningsVisible=")).toBe(false);
|
expect(templateValueAfterMarker(renderChatView(app, state), ".warningsVisible=")).toBe(false);
|
||||||
expect(templateValueAfterMarker(renderStatusBar(app, state), ".collapsedWarningCount=")).toBe(2);
|
expect(templateValueAfterMarker(collapsedStatusBar, ".warningCount=")).toBe(2);
|
||||||
|
expect(templateValueAfterMarker(collapsedStatusBar, ".warningsExpanded=")).toBe(false);
|
||||||
|
|
||||||
const otherState = stateWithWarnings("session-2");
|
const otherState = stateWithWarnings("session-2");
|
||||||
setAppState(app, otherState);
|
setAppState(app, otherState);
|
||||||
@@ -38,18 +41,21 @@ describe("PiWebApp session-warning visibility wiring", () => {
|
|||||||
setAppState(app, returningState);
|
setAppState(app, returningState);
|
||||||
syncWarningVisibility(app);
|
syncWarningVisibility(app);
|
||||||
expect(templateValueAfterMarker(renderChatView(app, returningState), ".warningsVisible=")).toBe(true);
|
expect(templateValueAfterMarker(renderChatView(app, returningState), ".warningsVisible=")).toBe(true);
|
||||||
|
expect(templateValueAfterMarker(renderStatusBar(app, returningState), ".warningCount=")).toBe(0);
|
||||||
|
|
||||||
setAppState(app, state);
|
setAppState(app, state);
|
||||||
syncWarningVisibility(app);
|
syncWarningVisibility(app);
|
||||||
const returnedStatusBar = renderStatusBar(app, state);
|
const returnedStatusBar = renderStatusBar(app, state);
|
||||||
expect(templateValueAfterMarker(renderChatView(app, state), ".warningsVisible=")).toBe(false);
|
expect(templateValueAfterMarker(renderChatView(app, state), ".warningsVisible=")).toBe(false);
|
||||||
expect(templateValueAfterMarker(returnedStatusBar, ".collapsedWarningCount=")).toBe(2);
|
expect(templateValueAfterMarker(returnedStatusBar, ".warningCount=")).toBe(2);
|
||||||
|
expect(templateValueAfterMarker(returnedStatusBar, ".warningsExpanded=")).toBe(false);
|
||||||
|
|
||||||
const restore = templateCallbackAfterMarker(returnedStatusBar, ".onRestoreWarnings=");
|
templateCallbackAfterMarker(returnedStatusBar, ".onToggleWarnings=")();
|
||||||
restore();
|
|
||||||
|
|
||||||
|
const restoredStatusBar = renderStatusBar(app, state);
|
||||||
expect(templateValueAfterMarker(renderChatView(app, state), ".warningsVisible=")).toBe(true);
|
expect(templateValueAfterMarker(renderChatView(app, state), ".warningsVisible=")).toBe(true);
|
||||||
expect(templateValueAfterMarker(renderStatusBar(app, state), ".collapsedWarningCount=")).toBe(0);
|
expect(templateValueAfterMarker(restoredStatusBar, ".warningCount=")).toBe(2);
|
||||||
|
expect(templateValueAfterMarker(restoredStatusBar, ".warningsExpanded=")).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,35 +5,36 @@ import { templateEventHandlerAfterMarker } from "../templateInspection.testSuppo
|
|||||||
import { StatusBar, statusBarWarningControlContent } from "./StatusBar";
|
import { StatusBar, statusBarWarningControlContent } from "./StatusBar";
|
||||||
|
|
||||||
describe("statusBarWarningControlContent", () => {
|
describe("statusBarWarningControlContent", () => {
|
||||||
it("provides only the visible numeric count while keeping a descriptive accessible label", () => {
|
it("provides an action label for both states while keeping only the count visible", () => {
|
||||||
expect(statusBarWarningControlContent(1)).toEqual({
|
expect(statusBarWarningControlContent(1, true)).toEqual({
|
||||||
countText: "1",
|
countText: "1",
|
||||||
accessibleLabel: "Show 1 warning in the warning area",
|
accessibleLabel: "Minimise 1 warning",
|
||||||
});
|
});
|
||||||
expect(statusBarWarningControlContent(3)).toEqual({
|
expect(statusBarWarningControlContent(3, false)).toEqual({
|
||||||
countText: "3",
|
countText: "3",
|
||||||
accessibleLabel: "Show 3 warnings in the warning area",
|
accessibleLabel: "Show 3 warnings in the warning area",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("omits the control content when there are no collapsed warnings", () => {
|
it("omits the control content when there are no warnings", () => {
|
||||||
expect(statusBarWarningControlContent(0)).toBeUndefined();
|
expect(statusBarWarningControlContent(0, false)).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("StatusBar warning restore wiring", () => {
|
describe("StatusBar warning toggle wiring", () => {
|
||||||
// Escape hatch: this specifically verifies the compact status-bar button's
|
// Escape hatch: this specifically verifies the compact status-bar button's
|
||||||
// Lit callback wiring in the node environment, anchored to its semantic class.
|
// Lit callback wiring in the node environment, anchored to its semantic class.
|
||||||
it("invokes onRestoreWarnings when the warning-count control is activated", () => {
|
it("invokes onToggleWarnings when the warning-count control is activated", () => {
|
||||||
const statusBar = new StatusBar();
|
const statusBar = new StatusBar();
|
||||||
const onRestoreWarnings = vi.fn();
|
const onToggleWarnings = vi.fn();
|
||||||
statusBar.status = status();
|
statusBar.status = status();
|
||||||
statusBar.collapsedWarningCount = 2;
|
statusBar.warningCount = 2;
|
||||||
statusBar.onRestoreWarnings = onRestoreWarnings;
|
statusBar.warningsExpanded = true;
|
||||||
|
statusBar.onToggleWarnings = onToggleWarnings;
|
||||||
|
|
||||||
templateEventHandlerAfterMarker(renderStatusBar(statusBar), "warning-restore")(new Event("click"));
|
templateEventHandlerAfterMarker(renderStatusBar(statusBar), "warning-toggle")(new Event("click"));
|
||||||
|
|
||||||
expect(onRestoreWarnings).toHaveBeenCalledOnce();
|
expect(onToggleWarnings).toHaveBeenCalledOnce();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -9,23 +9,24 @@ export interface StatusBarWarningControlContent {
|
|||||||
accessibleLabel: string;
|
accessibleLabel: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function statusBarWarningControlContent(count: number): StatusBarWarningControlContent | undefined {
|
export function statusBarWarningControlContent(count: number, expanded: boolean): StatusBarWarningControlContent | undefined {
|
||||||
if (!Number.isInteger(count) || count <= 0) return undefined;
|
if (!Number.isInteger(count) || count <= 0) return undefined;
|
||||||
const warningText = `${String(count)} ${count === 1 ? "warning" : "warnings"}`;
|
const warningText = `${String(count)} ${count === 1 ? "warning" : "warnings"}`;
|
||||||
return {
|
return {
|
||||||
countText: String(count),
|
countText: String(count),
|
||||||
accessibleLabel: `Show ${warningText} in the warning area`,
|
accessibleLabel: expanded ? `Minimise ${warningText}` : `Show ${warningText} in the warning area`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@customElement("status-bar")
|
@customElement("status-bar")
|
||||||
export class StatusBar extends LitElement {
|
export class StatusBar extends LitElement {
|
||||||
@property({ attribute: false }) status?: SessionStatus;
|
@property({ attribute: false }) status?: SessionStatus;
|
||||||
@property({ type: Number }) collapsedWarningCount = 0;
|
@property({ type: Number }) warningCount = 0;
|
||||||
@property({ attribute: false }) onRestoreWarnings?: () => void;
|
@property({ type: Boolean }) warningsExpanded = false;
|
||||||
|
@property({ attribute: false }) onToggleWarnings?: () => void;
|
||||||
|
|
||||||
private readonly handleRestoreWarnings = (): void => {
|
private readonly handleToggleWarnings = (): void => {
|
||||||
this.onRestoreWarnings?.();
|
this.onToggleWarnings?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
@@ -38,18 +39,19 @@ export class StatusBar extends LitElement {
|
|||||||
: `${context.percent.toFixed(1)}%/${formatTokenCount(context.contextWindow)}`
|
: `${context.percent.toFixed(1)}%/${formatTokenCount(context.contextWindow)}`
|
||||||
: "context unknown";
|
: "context unknown";
|
||||||
const tokens = status.tokens;
|
const tokens = status.tokens;
|
||||||
const warningControl = statusBarWarningControlContent(this.collapsedWarningCount);
|
const warningControl = statusBarWarningControlContent(this.warningCount, this.warningsExpanded);
|
||||||
return html`
|
return html`
|
||||||
<div class="bar">
|
<div class="bar">
|
||||||
${warningControl === undefined || this.onRestoreWarnings === undefined ? null : html`
|
${warningControl === undefined || this.onToggleWarnings === undefined ? null : html`
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="warning-restore"
|
class="warning-toggle"
|
||||||
title=${warningControl.accessibleLabel}
|
title=${warningControl.accessibleLabel}
|
||||||
aria-label=${warningControl.accessibleLabel}
|
aria-label=${warningControl.accessibleLabel}
|
||||||
@click=${this.handleRestoreWarnings}
|
aria-expanded=${String(this.warningsExpanded)}
|
||||||
|
@click=${this.handleToggleWarnings}
|
||||||
>
|
>
|
||||||
${renderSessionWarningIcon("warning", "warning-restore-icon")}
|
${renderSessionWarningIcon("warning", "warning-toggle-icon")}
|
||||||
<span>${warningControl.countText}</span>
|
<span>${warningControl.countText}</span>
|
||||||
</button>
|
</button>
|
||||||
`}
|
`}
|
||||||
|
|||||||
@@ -307,11 +307,6 @@ export const chatStyles = css`
|
|||||||
.top-notices { box-sizing: border-box; flex: 0 0 auto; max-height: 40%; min-height: 0; display: flex; flex-direction: column; overflow: hidden; border-bottom: 1px solid var(--pi-border); background: var(--pi-bg-overlay); }
|
.top-notices { box-sizing: border-box; flex: 0 0 auto; max-height: 40%; min-height: 0; display: flex; flex-direction: column; overflow: hidden; border-bottom: 1px solid var(--pi-border); background: var(--pi-bg-overlay); }
|
||||||
.session-warnings { flex: 0 1 auto; display: grid; gap: 8px; max-height: 50%; min-height: 0; overflow-y: auto; box-sizing: border-box; padding: 10px 16px; border-bottom: 1px solid var(--pi-border-muted); }
|
.session-warnings { flex: 0 1 auto; display: grid; gap: 8px; max-height: 50%; min-height: 0; overflow-y: auto; box-sizing: border-box; padding: 10px 16px; border-bottom: 1px solid var(--pi-border-muted); }
|
||||||
.session-warnings:only-child { flex: 1 1 auto; max-height: 100%; border-bottom: 0; }
|
.session-warnings:only-child { flex: 1 1 auto; max-height: 100%; border-bottom: 0; }
|
||||||
.session-warnings-controls { display: flex; justify-content: flex-end; }
|
|
||||||
.session-warnings-collapse { display: inline-flex; align-items: center; gap: 5px; border: 1px solid var(--pi-border); border-radius: 6px; background: var(--pi-surface); color: var(--pi-muted); padding: 4px 7px; font: 12px system-ui, sans-serif; cursor: pointer; }
|
|
||||||
.session-warnings-collapse:hover, .session-warnings-collapse:focus-visible { color: var(--pi-text-bright); border-color: var(--pi-accent); background: var(--pi-bg-overlay); }
|
|
||||||
.session-warnings-collapse:focus-visible { outline: 1px solid var(--pi-border); outline-offset: 2px; }
|
|
||||||
.session-warnings-collapse-icon { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; pointer-events: none; }
|
|
||||||
.session-warning { position: relative; display: grid; gap: 4px; box-sizing: border-box; padding: 10px 34px 10px 12px; border: 1px solid var(--pi-warning-border); border-radius: 10px; background: var(--pi-warning-surface); color: var(--pi-text); }
|
.session-warning { position: relative; display: grid; gap: 4px; box-sizing: border-box; padding: 10px 34px 10px 12px; border: 1px solid var(--pi-warning-border); border-radius: 10px; background: var(--pi-warning-surface); color: var(--pi-text); }
|
||||||
.session-warning.error { border-color: var(--pi-danger); background: color-mix(in srgb, var(--pi-danger) 12%, var(--pi-surface)); }
|
.session-warning.error { border-color: var(--pi-danger); background: color-mix(in srgb, var(--pi-danger) 12%, var(--pi-surface)); }
|
||||||
.session-warning.info { border-color: var(--pi-accent-border); background: var(--pi-selection-bg); }
|
.session-warning.info { border-color: var(--pi-accent-border); background: var(--pi-selection-bg); }
|
||||||
@@ -493,9 +488,9 @@ export const statusBarStyles = css`
|
|||||||
:host { display: block; color: var(--pi-muted); font: 12px system-ui, sans-serif; }
|
:host { display: block; color: var(--pi-muted); font: 12px system-ui, sans-serif; }
|
||||||
.bar { display: flex; justify-content: flex-end; gap: 12px; align-items: center; min-width: 0; padding: 7px 12px; border-top: 1px solid var(--pi-border); background: var(--pi-bg); white-space: nowrap; overflow: hidden; }
|
.bar { display: flex; justify-content: flex-end; gap: 12px; align-items: center; min-width: 0; padding: 7px 12px; border-top: 1px solid var(--pi-border); background: var(--pi-bg); white-space: nowrap; overflow: hidden; }
|
||||||
span { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
span { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
||||||
.warning-restore { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 4px; margin-right: auto; border: 0; background: transparent; color: inherit; padding: 0; font: inherit; line-height: 1; white-space: nowrap; cursor: pointer; }
|
.warning-toggle { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 4px; margin-right: auto; border: 0; background: transparent; color: inherit; padding: 0; font: inherit; line-height: 1; white-space: nowrap; cursor: pointer; }
|
||||||
.warning-restore:focus-visible { outline: 1px solid currentColor; outline-offset: 2px; }
|
.warning-toggle:focus-visible { outline: 1px solid currentColor; outline-offset: 2px; }
|
||||||
.warning-restore-icon { flex: 0 0 auto; width: 12px; height: 12px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
|
.warning-toggle-icon { flex: 0 0 auto; width: 12px; height: 12px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
|
||||||
.activity { display: inline-flex; align-items: center; gap: 6px; color: var(--pi-muted); }
|
.activity { display: inline-flex; align-items: center; gap: 6px; color: var(--pi-muted); }
|
||||||
.activity.active { color: var(--pi-success); }
|
.activity.active { color: var(--pi-success); }
|
||||||
.dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; opacity: .45; flex: 0 0 auto; }
|
.dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; opacity: .45; flex: 0 0 auto; }
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
reconcileSessionWarningVisibility,
|
reconcileSessionWarningVisibility,
|
||||||
restoreSessionWarnings,
|
restoreSessionWarnings,
|
||||||
sessionWarningSetSignature,
|
sessionWarningSetSignature,
|
||||||
|
toggleSessionWarnings,
|
||||||
} from "./sessionWarningVisibility";
|
} from "./sessionWarningVisibility";
|
||||||
|
|
||||||
const subscriptionWarning: SessionWarning = { severity: "warning", message: "subscription auth is active", source: "anthropic", dismiss: { id: "anthropicExtraUsage" } };
|
const subscriptionWarning: SessionWarning = { severity: "warning", message: "subscription auth is active", source: "anthropic", dismiss: { id: "anthropicExtraUsage" } };
|
||||||
@@ -95,4 +96,14 @@ describe("session warning visibility transitions", () => {
|
|||||||
expect(restored.collapsed).toBe(false);
|
expect(restored.collapsed).toBe(false);
|
||||||
expect(restored.collapsedWarningSets.size).toBe(0);
|
expect(restored.collapsedWarningSets.size).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uses one toggle transition for both warning-area states", () => {
|
||||||
|
const visible = reconcileSessionWarningVisibility(initialSessionWarningVisibilityState(), "session-1", warnings);
|
||||||
|
const collapsed = toggleSessionWarnings(visible);
|
||||||
|
const restored = toggleSessionWarnings(collapsed);
|
||||||
|
|
||||||
|
expect(collapsed.collapsed).toBe(true);
|
||||||
|
expect(restored.collapsed).toBe(false);
|
||||||
|
expect(restored.collapsedWarningSets.size).toBe(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -79,3 +79,7 @@ export function restoreSessionWarnings(current: SessionWarningVisibilityState):
|
|||||||
if (current.selectedSessionKey !== undefined) collapsedWarningSets.delete(current.selectedSessionKey);
|
if (current.selectedSessionKey !== undefined) collapsedWarningSets.delete(current.selectedSessionKey);
|
||||||
return { ...current, collapsed: false, collapsedWarningSets };
|
return { ...current, collapsed: false, collapsedWarningSets };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function toggleSessionWarnings(current: SessionWarningVisibilityState): SessionWarningVisibilityState {
|
||||||
|
return current.collapsed ? restoreSessionWarnings(current) : collapseSessionWarnings(current);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user