Compare commits

..

8 Commits

Author SHA1 Message Date
Aidan Timson 69fa1b1963 Use new test structure 2026-07-22 10:26:42 +01:00
Aidan Timson 29a548d610 Fix 2026-07-22 10:14:33 +01:00
Aidan Timson b92d81e510 Test Ask Assist from quick search 2026-07-22 10:10:33 +01:00
Aidan Timson e3d5f796a1 Hide Ask Assist when native Assist is active 2026-07-22 10:09:52 +01:00
Aidan Timson 8f020afba6 Group Ask Assist with quick search commands 2026-07-22 10:09:52 +01:00
Aidan Timson b48c044573 Support native android 2026-07-22 10:09:52 +01:00
Aidan Timson b1622a8273 Add ask assist action to quick search and support params in dialog 2026-07-22 10:09:52 +01:00
WazoAkaRapace 4be7a32148 Fix energy graph off-by-one colors with export-only grid source (#53145)
Fix energy usage graph off-by-one colors with export-only grid source

When a grid source configured without an import sensor (export-only) was
listed before other grid sources, the energy usage graph card assigned
color indices based on the filtered `statIds.from_grid` array — skipping
that source — instead of the positional index in the user's energy
sources config. This shifted every subsequent grid source's color by one
relative to the energy sources table card, which uses positional indices.

Populate `colorIndices.from_grid` and `colorIndices.to_grid` during the
source loop using a positional `gridIdx` counter, so import-only and
export-only grid sources no longer shift the colors of sources that
follow them.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-22 11:43:04 +03:00
14 changed files with 331 additions and 120 deletions
@@ -1,36 +0,0 @@
name: Prepare dependencies
description: Install and cache the complete dependency tree
runs:
using: composite
steps:
- name: Check for complete dependency tree
id: dependencies
continue-on-error: true
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
node_modules
.yarn/install-state.gz
key: >-
node-modules-v1-${{ runner.os }}-${{ runner.arch }}-${{
hashFiles('.nvmrc', 'package.json', 'yarn.lock', '.yarnrc.yml', '.yarn/releases/**', '.yarn/patches/**') }}
lookup-only: true
- name: Setup Node and install
if: steps.dependencies.outputs.cache-hit != 'true'
uses: ./.github/actions/setup
with:
cache: false
- name: Save complete dependency tree
if: steps.dependencies.outputs.cache-hit != 'true'
continue-on-error: true
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
node_modules
.yarn/install-state.gz
key: >-
node-modules-v1-${{ runner.os }}-${{ runner.arch }}-${{
hashFiles('.nvmrc', 'package.json', 'yarn.lock', '.yarnrc.yml', '.yarn/releases/**', '.yarn/patches/**') }}
+28 -2
View File
@@ -30,8 +30,34 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Prepare dependencies
uses: ./.github/actions/prepare-dependencies
- name: Check for complete dependency tree
id: dependencies
continue-on-error: true
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
node_modules
.yarn/install-state.gz
key: >-
node-modules-v1-${{ runner.os }}-${{ runner.arch }}-${{
hashFiles('.nvmrc', 'package.json', 'yarn.lock', '.yarnrc.yml', '.yarn/releases/**', '.yarn/patches/**') }}
lookup-only: true
- name: Setup Node and install
if: steps.dependencies.outputs.cache-hit != 'true'
uses: ./.github/actions/setup
with:
cache: false
- name: Save complete dependency tree
if: steps.dependencies.outputs.cache-hit != 'true'
continue-on-error: true
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
node_modules
.yarn/install-state.gz
key: >-
node-modules-v1-${{ runner.os }}-${{ runner.arch }}-${{
hashFiles('.nvmrc', 'package.json', 'yarn.lock', '.yarnrc.yml', '.yarn/releases/**', '.yarn/patches/**') }}
lint:
name: Lint and check format
+7 -57
View File
@@ -22,40 +22,9 @@ permissions:
contents: read
jobs:
prepare-dependencies:
name: Prepare dependencies
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Prepare dependencies
uses: ./.github/actions/prepare-dependencies
prepare-container-dependencies:
name: Prepare container dependencies
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.61.1-noble
options: --user 1001
defaults:
run:
shell: bash
steps:
- name: Check out files from GitHub
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Prepare dependencies
uses: ./.github/actions/prepare-dependencies
# ── Build the demo once and share it across test jobs via artifact ──────────
build-demo:
name: Build demo
needs: prepare-dependencies
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
@@ -63,10 +32,8 @@ jobs:
with:
persist-credentials: false
- name: Setup Node with shared dependencies
- name: Setup Node and install
uses: ./.github/actions/setup
with:
node-modules-cache: true
- name: Build demo
uses: ./.github/actions/build
@@ -86,7 +53,6 @@ jobs:
# ── Build the e2e test app and share it via artifact ────────────────────────
build-e2e-test-app:
name: Build e2e test app
needs: prepare-dependencies
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
@@ -94,10 +60,8 @@ jobs:
with:
persist-credentials: false
- name: Setup Node with shared dependencies
- name: Setup Node and install
uses: ./.github/actions/setup
with:
node-modules-cache: true
- name: Build e2e test app
uses: ./.github/actions/build
@@ -117,7 +81,6 @@ jobs:
# ── Build the gallery and share it via artifact ─────────────────────────────
build-gallery:
name: Build gallery
needs: prepare-dependencies
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
@@ -125,10 +88,8 @@ jobs:
with:
persist-credentials: false
- name: Setup Node with shared dependencies
- name: Setup Node and install
uses: ./.github/actions/setup
with:
node-modules-cache: true
- name: Build gallery
uses: ./.github/actions/build
@@ -149,7 +110,6 @@ jobs:
name: E2E demo (${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
needs:
- build-demo
- prepare-container-dependencies
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.61.1-noble
@@ -172,10 +132,8 @@ jobs:
with:
persist-credentials: false
- name: Setup Node with shared dependencies
- name: Setup Node and install
uses: ./.github/actions/setup
with:
node-modules-cache: true
- name: Download demo build
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
@@ -200,7 +158,6 @@ jobs:
name: E2E app (${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
needs:
- build-e2e-test-app
- prepare-container-dependencies
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.61.1-noble
@@ -225,10 +182,8 @@ jobs:
with:
persist-credentials: false
- name: Setup Node with shared dependencies
- name: Setup Node and install
uses: ./.github/actions/setup
with:
node-modules-cache: true
- name: Download e2e test app build
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
@@ -253,7 +208,6 @@ jobs:
name: E2E gallery (${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
needs:
- build-gallery
- prepare-container-dependencies
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.61.1-noble
@@ -278,10 +232,8 @@ jobs:
with:
persist-credentials: false
- name: Setup Node with shared dependencies
- name: Setup Node and install
uses: ./.github/actions/setup
with:
node-modules-cache: true
- name: Download gallery build
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
@@ -319,10 +271,8 @@ jobs:
with:
persist-credentials: false
- name: Setup Node with shared dependencies
- name: Setup Node and install
uses: ./.github/actions/setup
with:
node-modules-cache: true
- name: Download demo blob reports
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+37 -1
View File
@@ -57,6 +57,16 @@ interface AssistMessage {
error?: boolean;
}
export const initialPromptToSubmit = (
prompt: string | undefined,
submit: boolean
): string | undefined => (submit ? prompt?.trim() || undefined : undefined);
export const assistPipelineChanged = (
previous: AssistPipeline | undefined,
current: AssistPipeline | undefined
): boolean => previous?.id !== current?.id;
@customElement("ha-assist-chat")
export class HaAssistChat extends LitElement {
@property({ attribute: false }) public pipeline?: AssistPipeline;
@@ -67,6 +77,12 @@ export class HaAssistChat extends LitElement {
@property({ attribute: false })
public startListening?: boolean;
@property({ attribute: false })
public initialPrompt?: string;
@property({ attribute: false })
public submitInitialPrompt = false;
@query("#message-input") private _messageInput!: HaInput;
@query(".message:last-child")
@@ -99,6 +115,8 @@ export class HaAssistChat extends LitElement {
private _conversationId: string | null = null;
private _initialPromptSubmitted = false;
private _audioRecorder?: AudioRecorder;
private _audioBuffer?: Int16Array[];
@@ -108,7 +126,11 @@ export class HaAssistChat extends LitElement {
private _stt_binary_handler_id?: number | null;
protected willUpdate(changedProperties: PropertyValues<this>): void {
if (!this.hasUpdated || changedProperties.has("pipeline")) {
if (
!this.hasUpdated ||
(changedProperties.has("pipeline") &&
assistPipelineChanged(changedProperties.get("pipeline"), this.pipeline))
) {
this._conversation = [
{
who: "hass",
@@ -138,6 +160,20 @@ export class HaAssistChat extends LitElement {
if (changedProps.has("_conversation")) {
this._scrollMessagesBottom();
}
if (
!this._initialPromptSubmitted &&
(changedProps.has("initialPrompt") ||
changedProps.has("submitInitialPrompt"))
) {
const prompt = initialPromptToSubmit(
this.initialPrompt,
this.submitInitialPrompt
);
if (prompt) {
this._initialPromptSubmitted = true;
this._processText(prompt);
}
}
}
public disconnectedCallback() {
+56 -17
View File
@@ -1,4 +1,4 @@
import { mdiDevices } from "@mdi/js";
import { mdiCommentProcessingOutline, mdiDevices } from "@mdi/js";
import { consume } from "@lit/context";
import Fuse from "fuse.js";
import type { CSSResultGroup, PropertyValues } from "lit";
@@ -61,6 +61,7 @@ import { isIosApp } from "../../util/is_ios";
import { isMac } from "../../util/is_mac";
import { showConfirmationDialog } from "../generic/show-dialog-box";
import { showShortcutsDialog } from "../shortcuts/show-shortcuts-dialog";
import { showVoiceCommandDialog } from "../voice-command-dialog/show-ha-voice-command-dialog";
import {
effectiveQuickBarMode,
type QuickBarParams,
@@ -69,6 +70,18 @@ import {
const SEPARATOR = "________";
interface AssistComboBoxItem extends PickerComboBoxItem {
action: "assist";
assistPrompt: string;
}
type QuickBarComboBoxItem =
| NavigationComboBoxItem
| ActionCommandComboBoxItem
| EntityComboBoxItem
| DevicePickerItem
| AssistComboBoxItem;
@customElement("ha-quick-bar")
export class QuickBar extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -290,13 +303,7 @@ export class QuickBar extends LitElement {
`;
}
private _renderRow = (
item:
| NavigationComboBoxItem
| ActionCommandComboBoxItem
| EntityComboBoxItem
| DevicePickerItem
) => {
private _renderRow = (item: QuickBarComboBoxItem) => {
if (!item) {
return nothing;
}
@@ -412,8 +419,8 @@ export class QuickBar extends LitElement {
}: {
firstIndex: number;
lastIndex: number;
firstItem: PickerComboBoxItem | string;
secondItem: PickerComboBoxItem | string;
firstItem: QuickBarComboBoxItem | string;
secondItem: QuickBarComboBoxItem | string;
itemsCount: number;
}) => {
if (
@@ -461,7 +468,23 @@ export class QuickBar extends LitElement {
filter?: string,
section?: QuickBarSection
) => {
const items: (string | PickerComboBoxItem)[] = [];
const items: (string | QuickBarComboBoxItem)[] = [];
const prompt = filter?.trim();
const assistItem =
prompt &&
(!section || section === "command") &&
isComponentLoaded(this.hass.config, "conversation") &&
!this.hass.auth.external?.config.hasAssist
? ({
id: "ask-assist",
action: "assist",
primary: this.hass.localize("ui.dialogs.quick-bar.ask_assist", {
query: prompt,
}),
icon_path: mdiCommentProcessingOutline,
assistPrompt: prompt,
} satisfies AssistComboBoxItem)
: undefined;
if (!section || section === "navigate") {
let navigateItems = this._generateNavigationCommandsMemoized(
@@ -486,10 +509,12 @@ export class QuickBar extends LitElement {
items.push(...navigateItems);
}
if (this.hass.user?.is_admin && (!section || section === "command")) {
let commandItems = this._generateActionCommandsMemoized(this.hass).sort(
this._sortBySortingLabel
);
if (!section || section === "command") {
let commandItems = this.hass.user?.is_admin
? this._generateActionCommandsMemoized(this.hass).sort(
this._sortBySortingLabel
)
: [];
if (filter) {
commandItems = this._filterGroup(
@@ -499,12 +524,15 @@ export class QuickBar extends LitElement {
) as ActionCommandComboBoxItem[];
}
if (!section && commandItems.length) {
if (!section && (commandItems.length || assistItem)) {
// show group title
items.push(this.hass.localize("ui.dialogs.quick-bar.commands_title"));
}
items.push(...commandItems);
if (assistItem) {
items.push(assistItem);
}
}
if (!section || section === "entity") {
@@ -723,10 +751,21 @@ export class QuickBar extends LitElement {
const { index, newTab } = ev.detail;
const item = this._comboBox.virtualizerElement.items[
index
] as PickerComboBoxItem;
] as QuickBarComboBoxItem;
this._itemSelected = true;
if (item && "assistPrompt" in item) {
this.closeDialog();
showVoiceCommandDialog(this, this.hass, {
pipeline_id: "last_used",
start_listening: false,
prompt: item.assistPrompt,
submit: true,
});
return;
}
// entity selected
if (item && "stateObj" in item) {
this.closeDialog();
@@ -58,9 +58,11 @@ export class HaVoiceCommandDialog extends LitElement {
private _startListening = false;
public async showDialog(
params: Required<VoiceCommandDialogParams>
): Promise<void> {
private _prompt?: string;
private _submitPrompt = false;
public async showDialog(params: VoiceCommandDialogParams): Promise<void> {
await this._loadPipelines();
const pipelinesIds = this._pipelines?.map((pipeline) => pipeline.id) || [];
if (
@@ -77,7 +79,9 @@ export class HaVoiceCommandDialog extends LitElement {
this._pipelineId = this._preferredPipeline;
}
this._startListening = params.start_listening;
this._startListening = params.start_listening ?? false;
this._prompt = params.prompt;
this._submitPrompt = params.submit ?? false;
this._dialogOpen = true;
this._open = true;
}
@@ -189,6 +193,8 @@ export class HaVoiceCommandDialog extends LitElement {
.hass=${this.hass}
.pipeline=${this._pipeline}
.startListening=${this._startListening}
.initialPrompt=${this._prompt}
.submitInitialPrompt=${this._submitPrompt}
>
</ha-assist-chat>
`
@@ -6,6 +6,8 @@ const loadVoiceCommandDialog = () => import("./ha-voice-command-dialog");
export interface VoiceCommandDialogParams {
pipeline_id: "last_used" | "preferred" | string;
start_listening?: boolean;
prompt?: string;
submit?: boolean;
}
export const showVoiceCommandDialog = (
@@ -31,6 +33,8 @@ export const showVoiceCommandDialog = (
pipeline_id: dialogParams.pipeline_id,
// Don't start listening by default for web
start_listening: dialogParams.start_listening ?? false,
prompt: dialogParams.prompt,
submit: dialogParams.submit ?? false,
},
});
};
@@ -297,6 +297,15 @@ export class HuiEnergyUsageGraphCard
to_battery: {},
};
// Grid sources can be import-only or export-only; assign color indices by
// position in the user's grid sources config so this card matches the
// energy sources table, which uses positional indices.
const colorIndices: Record<string, Record<string, number>> = {};
Object.keys(colorPropertyMap).forEach((key) => {
colorIndices[key] = {};
});
let gridIdx = 0;
for (const source of energyData.prefs.energy_sources) {
if (source.type === "solar") {
if (statIds.solar) {
@@ -335,6 +344,7 @@ export class HuiEnergyUsageGraphCard
} else {
statIds.from_grid = [gridSource.stat_energy_from];
}
colorIndices.from_grid[gridSource.stat_energy_from] = gridIdx;
if (gridSource.name) {
statLabels.from_grid[gridSource.stat_energy_from] =
gridSource.stat_energy_to
@@ -351,6 +361,7 @@ export class HuiEnergyUsageGraphCard
} else {
statIds.to_grid = [gridSource.stat_energy_to];
}
colorIndices.to_grid[gridSource.stat_energy_to] = gridIdx;
if (gridSource.name) {
statLabels.to_grid[gridSource.stat_energy_to] =
gridSource.stat_energy_from
@@ -361,17 +372,18 @@ export class HuiEnergyUsageGraphCard
: gridSource.name;
}
}
gridIdx++;
}
const computedStyles = getComputedStyle(this);
const colorIndices: Record<string, Record<string, number>> = {};
Object.keys(colorPropertyMap).forEach((key) => {
colorIndices[key] = {};
if (
key === "used_grid" ||
key === "used_solar" ||
key === "used_battery"
key === "used_battery" ||
key === "from_grid" ||
key === "to_grid"
) {
return;
}
+1
View File
@@ -1495,6 +1495,7 @@
"revert_error": "Failed to revert the configuration: {error}"
},
"quick-bar": {
"ask_assist": "Ask Assist: {query}",
"commands_title": "Commands",
"navigate_title": "Navigate",
"commands": {
+38
View File
@@ -0,0 +1,38 @@
import { describe, expect, it } from "vitest";
import type { AssistPipeline } from "../../src/data/assist_pipeline";
import {
assistPipelineChanged,
initialPromptToSubmit,
} from "../../src/components/ha-assist-chat";
describe("initialPromptToSubmit", () => {
it("returns a trimmed prompt when submission is requested", () => {
expect(initialPromptToSubmit(" Turn on the lights ", true)).toBe(
"Turn on the lights"
);
});
it("does not return a prompt when submission is not requested", () => {
expect(initialPromptToSubmit("Turn on the lights", false)).toBeUndefined();
});
it("does not return an empty prompt", () => {
expect(initialPromptToSubmit(" ", true)).toBeUndefined();
});
});
describe("ha-assist-chat pipeline updates", () => {
it("preserves the conversation when the same pipeline is reloaded", () => {
const pipeline = { id: "pipeline-id" } as AssistPipeline;
expect(assistPipelineChanged(pipeline, { ...pipeline })).toBe(false);
});
it("resets the conversation when the pipeline changes", () => {
expect(
assistPipelineChanged(
{ id: "first" } as AssistPipeline,
{ id: "second" } as AssistPipeline
)
).toBe(true);
});
});
@@ -0,0 +1,59 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { showVoiceCommandDialog } from "../../../src/dialogs/voice-command-dialog/show-ha-voice-command-dialog";
import type { HomeAssistant } from "../../../src/types";
describe("showVoiceCommandDialog", () => {
let element: HTMLElement;
beforeEach(() => {
element = document.createElement("div");
});
it("passes an initial prompt to the web dialog", () => {
const hass = {
auth: { external: undefined },
} as HomeAssistant;
const listener = vi.fn();
element.addEventListener("show-dialog", listener);
showVoiceCommandDialog(element, hass, {
pipeline_id: "last_used",
prompt: "Turn on the lights",
submit: true,
});
expect(listener).toHaveBeenCalledOnce();
expect(listener.mock.calls[0][0].detail.dialogParams).toEqual({
pipeline_id: "last_used",
start_listening: false,
prompt: "Turn on the lights",
submit: true,
});
});
it("keeps the native Assist payload compatible", () => {
const fireMessage = vi.fn();
const hass = {
auth: {
external: {
config: { hasAssist: true },
fireMessage,
},
},
} as unknown as HomeAssistant;
showVoiceCommandDialog(element, hass, {
pipeline_id: "last_used",
prompt: "Turn on the lights",
submit: true,
});
expect(fireMessage).toHaveBeenCalledWith({
type: "assist/show",
payload: {
pipeline_id: "last_used",
start_listening: true,
},
});
});
});
+39
View File
@@ -120,6 +120,45 @@ test.describe("App shell", () => {
});
});
test.describe("Quick search", () => {
test("starts an Assist conversation with the search query", async ({
page,
}) => {
await goToPanel(page, "/?scenario=quick-search-assist#/lovelace");
await page.keyboard.press("Control+K");
const quickBar = page.locator("ha-quick-bar");
await expect(quickBar).toBeAttached({ timeout: QUICK_TIMEOUT });
await quickBar
.locator("ha-input-search >> input")
.fill("Turn on the lights");
const assistItem = quickBar
.locator(".combo-box-row")
.filter({ hasText: "Ask Assist: Turn on the lights" });
await expect(assistItem).toBeVisible({ timeout: QUICK_TIMEOUT });
await assistItem.click();
const voiceCommandDialog = page.locator("ha-voice-command-dialog");
await expect(voiceCommandDialog).toBeAttached({ timeout: QUICK_TIMEOUT });
await expect(voiceCommandDialog.locator("ha-assist-chat")).toBeAttached({
timeout: QUICK_TIMEOUT,
});
await expect
.poll(() => page.evaluate(() => window.__assistRun))
.toMatchObject({
type: "assist_pipeline/run",
start_stage: "intent",
input: { text: "Turn on the lights" },
end_stage: "intent",
pipeline: "test-pipeline",
conversation_id: null,
});
});
});
defineRouteSmokeTests(appRouteSmokeGroups);
// ---------------------------------------------------------------------------
+1
View File
@@ -90,6 +90,7 @@ const MEDIA_BROWSER_ROOT = {
declare global {
interface Window {
__assistRun?: unknown;
__mockHass: MockHomeAssistant;
}
}
+36
View File
@@ -1,4 +1,5 @@
import type { ExtEntityRegistryEntry } from "../../../../../src/data/entity/entity_registry";
import type { AssistPipeline } from "../../../../../src/data/assist_pipeline";
import type { MockHomeAssistant } from "../../../../../src/fake_data/provide_hass";
export type Scenario = (hass: MockHomeAssistant) => Promise<void> | void;
@@ -89,6 +90,40 @@ const lightMoreInfoScenario: Scenario = async (hass) => {
hass.mockWS("config/entity_registry/get", () => registryEntry);
};
const quickSearchAssistScenario: Scenario = async (hass) => {
const pipeline: AssistPipeline = {
id: "test-pipeline",
name: "Test Assist",
language: "en",
conversation_engine: "conversation.home_assistant",
conversation_language: "en",
stt_engine: null,
stt_language: null,
tts_engine: null,
tts_language: null,
tts_voice: null,
wake_word_entity: null,
wake_word_id: null,
};
hass.updateHass({
config: {
...hass.config,
components: [...hass.config.components, "conversation"],
},
enableShortcuts: true,
});
hass.mockWS("assist_pipeline/pipeline/list", () => ({
pipelines: [pipeline],
preferred_pipeline: pipeline.id,
}));
hass.mockWS("assist_pipeline/pipeline/get", () => pipeline);
hass.mockWS("assist_pipeline/run", (message) => {
window.__assistRun = message;
return () => undefined;
});
};
// ── Registry ──────────────────────────────────────────────────────────────
export const scenarios: Record<string, Scenario> = {
@@ -97,4 +132,5 @@ export const scenarios: Record<string, Scenario> = {
"dark-theme": darkThemeScenario,
"custom-theme": customThemeScenario,
"light-more-info": lightMoreInfoScenario,
"quick-search-assist": quickSearchAssistScenario,
};