diff --git a/package-lock.json b/package-lock.json index ff0d34f..f7d8ffa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.2.4", "license": "MIT", "dependencies": { - "@elgato/streamdeck": "2.1.2" + "@elgato/streamdeck": "3.0.0" }, "devDependencies": { "@elgato/cli": "1.10.0", @@ -57,26 +57,19 @@ "node": ">=20.1.0" } }, - "node_modules/@elgato/cli/node_modules/@elgato/schemas": { + "node_modules/@elgato/schemas": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/@elgato/schemas/-/schemas-0.5.0.tgz", "integrity": "sha512-Mltoh5L8M7cQuLIlDAsBq4IfdPrt0KMcwKCpl75OcgqILti7XBb/DlgxtULSnze/RXrH4iiwMbKdmMYeKn4RaQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@elgato/schemas": { - "version": "0.4.15", - "resolved": "https://registry.npmjs.org/@elgato/schemas/-/schemas-0.4.15.tgz", - "integrity": "sha512-UpELAqazf52PFer+hqg/hvEhYoPHCH3R0af6P7Ih3Hk6xCCJENn7cA5TIpXAIGEWe6dIteHuo/e/iPGLUP7yXA==", "license": "MIT" }, "node_modules/@elgato/streamdeck": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@elgato/streamdeck/-/streamdeck-2.1.2.tgz", - "integrity": "sha512-qj6NuDyYj8bE+XFnr+O5ynB1zojwugfWiK3oQYwweaOzOFs7fEE21v9z75EPC8wQr3cuuzI7m557TQLn5k/4mA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@elgato/streamdeck/-/streamdeck-3.0.0.tgz", + "integrity": "sha512-Y7vfZH8YB442+iYleey/lNF4WRY8y5OKlY3e0FxKlDDl9Ztc9NLSwiwIy6PZRK87NrJVeq7IXoFxH6PkcZQLkA==", "license": "MIT", "dependencies": { - "@elgato/schemas": "^0.4.14", + "@elgato/schemas": "^0.5.0", "@elgato/utils": "0.6.0", "ws": "^8.19.0" }, diff --git a/package.json b/package.json index 41d3587..e1ec96e 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "test:fast": "vitest run --project unit" }, "dependencies": { - "@elgato/streamdeck": "2.1.2" + "@elgato/streamdeck": "3.0.0" }, "devDependencies": { "@elgato/cli": "1.10.0", diff --git a/src/actions/agent-navigator.ts b/src/actions/agent-navigator.ts index 99c76de..e3ed84a 100644 --- a/src/actions/agent-navigator.ts +++ b/src/actions/agent-navigator.ts @@ -10,6 +10,7 @@ import { openNewChat, openThread } from "../lib/automation.js"; import { codexStore } from "../lib/codex-store.js"; import { statusIndicator } from "../lib/visuals.js"; import { renderFeedback } from "../lib/render-cache.js"; +import { showActionAlert } from "../lib/streamdeck-alert.js"; type NavigatorSettings = { index?: number; @@ -70,7 +71,7 @@ export class AgentNavigatorAction extends SingletonAction { } if (actionInstance.isDial()) await this.draw(actionInstance); } catch { - await actionInstance.showAlert(); + await showActionAlert(actionInstance); } } diff --git a/src/actions/agent-status.ts b/src/actions/agent-status.ts index 20fbcbe..79fb8da 100644 --- a/src/actions/agent-status.ts +++ b/src/actions/agent-status.ts @@ -59,8 +59,7 @@ export class AgentStatusAction extends SingletonAction { receivedSettings?: AgentSettings, ): Promise { if (!actionInstance.isKey()) return; - const settings = - receivedSettings ?? (await actionInstance.getSettings()); + const settings = receivedSettings ?? (await actionInstance.getSettings()); const slot = this.slotFrom( settings, (actionInstance.coordinates?.column ?? 0) + 1, diff --git a/src/actions/command.ts b/src/actions/command.ts index ff77b0c..9619191 100644 --- a/src/actions/command.ts +++ b/src/actions/command.ts @@ -31,6 +31,7 @@ import { import { codexStore } from "../lib/codex-store.js"; import { pickerFailureLabel } from "../lib/codex-ui-control.js"; import { renderFeedback, renderKey } from "../lib/render-cache.js"; +import { showActionAlert } from "../lib/streamdeck-alert.js"; type CommandSettings = { commandId?: string; @@ -179,7 +180,7 @@ export class CommandAction extends SingletonAction { title: pickerFailureLabel(error), }); } - await actionInstance.showAlert(); + await showActionAlert(actionInstance); } } diff --git a/src/actions/context.ts b/src/actions/context.ts index 0ba39b9..ae2a710 100644 --- a/src/actions/context.ts +++ b/src/actions/context.ts @@ -10,17 +10,19 @@ import { toggleContextView, type ContextView } from "../lib/context.js"; import { contextKeySvg, svgDataUrl } from "../lib/visuals.js"; import { renderKey } from "../lib/render-cache.js"; +type ContextSettings = Record; + @action({ UUID: "com.todd.streamdeckcodex.context" }) -export class ContextAction extends SingletonAction { +export class ContextAction extends SingletonAction { readonly #mode = new Map(); - async onWillAppear(event: WillAppearEvent): Promise { + async onWillAppear(event: WillAppearEvent): Promise { if (!event.action.isKey()) return; this.#mode.set(event.action.id, "remaining"); await this.draw(event.action); } - async onKeyDown(event: KeyDownEvent): Promise { + async onKeyDown(event: KeyDownEvent): Promise { const current = this.#mode.get(event.action.id) ?? "remaining"; this.#mode.set(event.action.id, toggleContextView(current)); await this.draw(event.action); @@ -34,7 +36,7 @@ export class ContextAction extends SingletonAction { ); } - private async draw(actionInstance: Action): Promise { + private async draw(actionInstance: Action): Promise { if (!actionInstance.isKey()) return; await renderKey( actionInstance, diff --git a/src/actions/health.ts b/src/actions/health.ts index d7a0a55..072a3b3 100644 --- a/src/actions/health.ts +++ b/src/actions/health.ts @@ -15,17 +15,19 @@ import { import { renderKey } from "../lib/render-cache.js"; import { healthKeySvg, svgDataUrl } from "../lib/visuals.js"; +type HealthSettings = Record; + @action({ UUID: "com.todd.streamdeckcodex.health" }) -export class HealthAction extends SingletonAction { +export class HealthAction extends SingletonAction { readonly #selection = new Map(); - async onWillAppear(event: WillAppearEvent): Promise { + async onWillAppear(event: WillAppearEvent): Promise { if (!event.action.isKey()) return; this.#selection.set(event.action.id, 0); await this.draw(event.action); } - async onKeyDown(event: KeyDownEvent): Promise { + async onKeyDown(event: KeyDownEvent): Promise { const next = ((this.#selection.get(event.action.id) ?? 0) + 1) % HEALTH_COMPONENTS.length; @@ -41,7 +43,7 @@ export class HealthAction extends SingletonAction { ); } - private async draw(actionInstance: Action): Promise { + private async draw(actionInstance: Action): Promise { if (!actionInstance.isKey()) return; const snapshot = collectHealth(codexStore); const component = HEALTH_COMPONENTS[ diff --git a/src/actions/keycap.ts b/src/actions/keycap.ts index 5e6339a..a2d819e 100644 --- a/src/actions/keycap.ts +++ b/src/actions/keycap.ts @@ -38,7 +38,7 @@ export class KeycapAction extends SingletonAction { } async onKeyDown(event: KeyDownEvent): Promise { - const settings = await event.action.getSettings(); + const settings = await event.action.getSettings(); const actionKind = settings.action ?? "info"; try { if (actionKind === "new-chat") await openNewChat(); @@ -83,7 +83,7 @@ export class KeycapAction extends SingletonAction { private async draw(actionInstance: Action): Promise { if (!actionInstance.isKey()) return; - const settings = await actionInstance.getSettings(); + const settings = await actionInstance.getSettings(); await renderKey( actionInstance, svgDataUrl( diff --git a/src/actions/model.ts b/src/actions/model.ts index 9aa99ba..eb739e2 100644 --- a/src/actions/model.ts +++ b/src/actions/model.ts @@ -16,6 +16,7 @@ import { type ModelDialState, } from "../lib/model.js"; import { renderFeedback } from "../lib/render-cache.js"; +import { showActionAlert } from "../lib/streamdeck-alert.js"; import { dialFailureFeedback } from "../lib/visuals.js"; type ModelSettings = { @@ -115,7 +116,7 @@ export class ModelAction extends SingletonAction { streamDeck.logger.warn( `Ignored unavailable model ${current.selected || "(none)"}`, ); - await actionInstance.showAlert(); + await showActionAlert(actionInstance); return; } @@ -158,7 +159,7 @@ export class ModelAction extends SingletonAction { dialFailureFeedback(pickerFailureLabel(error)), ); } - await actionInstance.showAlert(); + await showActionAlert(actionInstance); } } diff --git a/src/actions/reasoning.ts b/src/actions/reasoning.ts index 3a88d77..7c2db20 100644 --- a/src/actions/reasoning.ts +++ b/src/actions/reasoning.ts @@ -17,6 +17,7 @@ import { type ReasoningDialState, } from "../lib/reasoning.js"; import { renderFeedback } from "../lib/render-cache.js"; +import { showActionAlert } from "../lib/streamdeck-alert.js"; import { dialFailureFeedback } from "../lib/visuals.js"; type ReasoningSettings = { @@ -121,7 +122,7 @@ export class ReasoningAction extends SingletonAction { streamDeck.logger.warn( `Ignored unsupported reasoning level ${current.selected} for ${snapshot.model ?? "current model"}`, ); - await actionInstance.showAlert(); + await showActionAlert(actionInstance); return; } await this.apply(context, actionInstance, current.selected); @@ -176,7 +177,7 @@ export class ReasoningAction extends SingletonAction { dialFailureFeedback(pickerFailureLabel(error)), ); } - await actionInstance.showAlert(); + await showActionAlert(actionInstance); } } diff --git a/src/actions/usage.ts b/src/actions/usage.ts index c934002..daa627c 100644 --- a/src/actions/usage.ts +++ b/src/actions/usage.ts @@ -10,17 +10,19 @@ import { toggleUsageView, type UsageView } from "../lib/usage.js"; import { svgDataUrl, usageKeySvg } from "../lib/visuals.js"; import { renderKey } from "../lib/render-cache.js"; +type UsageSettings = Record; + @action({ UUID: "com.todd.streamdeckcodex.usage" }) -export class UsageAction extends SingletonAction { +export class UsageAction extends SingletonAction { readonly #mode = new Map(); - async onWillAppear(event: WillAppearEvent): Promise { + async onWillAppear(event: WillAppearEvent): Promise { if (!event.action.isKey()) return; this.#mode.set(event.action.id, "weekly"); await this.draw(event.action); } - async onKeyDown(event: KeyDownEvent): Promise { + async onKeyDown(event: KeyDownEvent): Promise { const current = this.#mode.get(event.action.id) ?? "weekly"; this.#mode.set(event.action.id, toggleUsageView(current)); await this.draw(event.action); @@ -34,7 +36,7 @@ export class UsageAction extends SingletonAction { ); } - private async draw(actionInstance: Action): Promise { + private async draw(actionInstance: Action): Promise { if (!actionInstance.isKey()) return; const snapshot = codexStore.usageSnapshotCached(); await renderKey( diff --git a/src/actions/workflow.ts b/src/actions/workflow.ts index d0334c0..bd06388 100644 --- a/src/actions/workflow.ts +++ b/src/actions/workflow.ts @@ -12,6 +12,7 @@ import { launchWorkflow } from "../lib/automation.js"; import { codexStore } from "../lib/codex-store.js"; import { commandKeySvg, svgDataUrl } from "../lib/visuals.js"; import { renderFeedback, renderKey } from "../lib/render-cache.js"; +import { showActionAlert } from "../lib/streamdeck-alert.js"; import { workflowAt, WORKFLOWS, @@ -106,7 +107,7 @@ export class WorkflowAction extends SingletonAction { ); if (actionInstance.isKey()) await actionInstance.showOk(); } catch { - await actionInstance.showAlert(); + await showActionAlert(actionInstance); } } diff --git a/src/lib/streamdeck-alert.ts b/src/lib/streamdeck-alert.ts new file mode 100644 index 0000000..1593d31 --- /dev/null +++ b/src/lib/streamdeck-alert.ts @@ -0,0 +1,16 @@ +import type { Action } from "@elgato/streamdeck"; + +type JsonObject = { + [key: string]: JsonValue; +}; +type JsonValue = + boolean | number | string | null | undefined | JsonObject | JsonValue[]; + +/** + * SDK v3 keeps `showAlert` on keys and dials only; Neo Infobar actions omit it. + */ +export async function showActionAlert( + action: Action, +): Promise { + if (action.isKey() || action.isDial()) await action.showAlert(); +} diff --git a/test/helpers/fake-streamdeck-action.ts b/test/helpers/fake-streamdeck-action.ts index 35473b4..f0adea4 100644 --- a/test/helpers/fake-streamdeck-action.ts +++ b/test/helpers/fake-streamdeck-action.ts @@ -17,9 +17,12 @@ export class FakeStreamDeckAction< isDial(): boolean { return this.kind === "dial"; } - async getSettings(): Promise { + isNeoInfobar(): boolean { + return false; + } + async getSettings(): Promise { this.calls.push({ method: "getSettings" }); - return this.settings as unknown as U; + return this.settings; } async setSettings(value: T): Promise { this.settings = value;