diff --git a/main/handlers/providers.ts b/main/handlers/providers.ts index 94e1e1bc..9dd9ef94 100644 --- a/main/handlers/providers.ts +++ b/main/handlers/providers.ts @@ -58,8 +58,10 @@ import { normalizeAppearanceConfig, parseAppearanceConfig, } from "../../renderer/shared/appearance.js"; -import { normalizeProviderArtwork } from "../../renderer/shared/provider-artwork.js"; -import { normalizeProviderArtworkInput } from "../services/provider-artwork.js"; +import { + normalizeProviderArtworkInput, + persistableProviderArtwork, +} from "../services/provider-artwork.js"; import { isGenerationThinkingLevel } from "../../renderer/shared/generation-thinking.js"; import { isGeminiUsageScope } from "../../renderer/shared/gemini-usage-scope.js"; import { isGeminiTranscriptionModel } from "../../renderer/shared/voice-models.js"; @@ -158,7 +160,7 @@ function parseProvider(value: unknown): StoredProvider { id: asProviderId(p.id), kind, label: asString(p.label, "label"), - artwork: normalizeProviderArtwork(p.artwork), + artwork: persistableProviderArtwork(p.artwork), baseUrl, models, modelMetadata, diff --git a/main/services/provider-artwork-core.test.ts b/main/services/provider-artwork-core.test.ts index 290a12ed..482bbac4 100644 --- a/main/services/provider-artwork-core.test.ts +++ b/main/services/provider-artwork-core.test.ts @@ -1,6 +1,13 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { decodeProviderArtworkSource } from "./provider-artwork-core.js"; +import { + decodeProviderArtworkSource, + persistStoredProviderArtwork, +} from "./provider-artwork-core.js"; +import { normalizeProviderArtwork } from "../../renderer/shared/provider-artwork.js"; + +const VALID_PNG = + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="; test("provider artwork accepts PNG and inert SVG sources", () => { assert.equal( @@ -51,3 +58,59 @@ test("provider artwork rejects malformed base64 and oversized PNG dimensions bef /dimensions/u, ); }); + +test("already-normalized artwork persists without another decode", () => { + let reencodeCalls = 0; + const artwork = persistStoredProviderArtwork( + { mimeType: "image/png", dataBase64: VALID_PNG }, + () => { + reencodeCalls += 1; + throw new Error("should not re-encode valid artwork"); + }, + ); + assert.equal(reencodeCalls, 0); + assert.deepEqual( + artwork, + normalizeProviderArtwork({ mimeType: "image/png", dataBase64: VALID_PNG }), + ); +}); + +test("invalid artwork is dropped, and oversized PNG bytes are re-encoded", () => { + assert.equal(persistStoredProviderArtwork(undefined, () => { + throw new Error("unused"); + }), undefined); + assert.equal( + persistStoredProviderArtwork({ mimeType: "image/svg+xml" }, () => { + throw new Error("unused"); + }), + undefined, + ); + + const oversizedPng = Buffer.alloc(24); + Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]).copy(oversizedPng); + Buffer.from("IHDR", "ascii").copy(oversizedPng, 12); + oversizedPng.writeUInt32BE(65, 16); + oversizedPng.writeUInt32BE(65, 20); + const oversizedBase64 = oversizedPng.toString("base64"); + assert.equal( + normalizeProviderArtwork({ mimeType: "image/png", dataBase64: oversizedBase64 }), + undefined, + ); + + const recovered = persistStoredProviderArtwork( + { mimeType: "image/png", dataBase64: oversizedBase64 }, + (input) => { + assert.equal(input.name, "icon.png"); + assert.equal(input.dataBase64, oversizedBase64); + return { mimeType: "image/png", dataBase64: VALID_PNG }; + }, + ); + assert.deepEqual(recovered, { mimeType: "image/png", dataBase64: VALID_PNG }); + + assert.equal( + persistStoredProviderArtwork({ mimeType: "image/png", dataBase64: "not-png" }, () => { + throw new Error("decode failed"); + }), + undefined, + ); +}); diff --git a/main/services/provider-artwork-core.ts b/main/services/provider-artwork-core.ts index 3a6ef8df..ec6a892d 100644 --- a/main/services/provider-artwork-core.ts +++ b/main/services/provider-artwork-core.ts @@ -1,5 +1,27 @@ +import { + normalizeProviderArtwork, + type ProviderArtwork, +} from "../../renderer/shared/provider-artwork.js"; + export const PROVIDER_ARTWORK_MAX_SOURCE_BYTES = 512 * 1024; +/** Keep artwork that already matches the display contract, or re-encode PNG bytes. */ +export function persistStoredProviderArtwork( + value: unknown, + reencode: (input: { name: string; dataBase64: string }) => ProviderArtwork, +): ProviderArtwork | undefined { + const validated = normalizeProviderArtwork(value); + if (validated) return validated; + if (!value || typeof value !== "object" || Array.isArray(value)) return undefined; + const dataBase64 = (value as { dataBase64?: unknown }).dataBase64; + if (typeof dataBase64 !== "string" || dataBase64.length === 0) return undefined; + try { + return reencode({ name: "icon.png", dataBase64 }); + } catch { + return undefined; + } +} + export function decodeProviderArtworkSource(value: unknown): { bytes: Buffer; kind: "png" | "svg"; diff --git a/main/services/provider-artwork.test.ts b/main/services/provider-artwork.test.ts new file mode 100644 index 00000000..37cd35ce --- /dev/null +++ b/main/services/provider-artwork.test.ts @@ -0,0 +1,26 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import test from "node:test"; + +test("normalized provider icons are encoded at 1x so the display validator keeps them", () => { + const source = readFileSync(new URL("./provider-artwork.ts", import.meta.url), "utf8"); + assert.match(source, /const PIXEL_SCALE = 1 as const/u); + assert.match(source, /nativeImage\.createFromBuffer\(source\.bytes, \{ scaleFactor: PIXEL_SCALE \}\)/u); + assert.match( + source, + /nativeImage\.createFromDataURL\(\s*`data:image\/svg\+xml;base64,\$\{Buffer\.from\(source\.safeSvg!, "utf8"\)\.toString\("base64"\)\}`,?\s*\)/u, + ); + assert.match(source, /image\.getSize\(PIXEL_SCALE\)/u); + assert.match(source, /normalized\.toPNG\(\{ scaleFactor: PIXEL_SCALE \}\)/u); + assert.match(source, /if \(!normalizeProviderArtwork\(artwork\)\)/u); + assert.match( + source, + /persistStoredProviderArtwork\(value, \(input\) => normalizeProviderArtworkInput\(input\)\)/u, + ); +}); + +test("provider saves persist recovered artwork instead of dropping oversize icons", () => { + const source = readFileSync(new URL("../handlers/providers.ts", import.meta.url), "utf8"); + assert.match(source, /artwork: persistableProviderArtwork\(p\.artwork\)/u); + assert.doesNotMatch(source, /artwork:\s*normalizeProviderArtwork\(p\.artwork\)/u); +}); diff --git a/main/services/provider-artwork.ts b/main/services/provider-artwork.ts index fc557c1d..3609903d 100644 --- a/main/services/provider-artwork.ts +++ b/main/services/provider-artwork.ts @@ -1,10 +1,16 @@ import { nativeImage } from "../platform.js"; import { PROVIDER_ARTWORK_MAX_PNG_BYTES, + normalizeProviderArtwork, type ProviderArtwork, } from "../../renderer/shared/provider-artwork.js"; -import { decodeProviderArtworkSource } from "./provider-artwork-core.js"; +import { + decodeProviderArtworkSource, + persistStoredProviderArtwork, +} from "./provider-artwork-core.js"; + const TARGET_EDGE = 64; +const PIXEL_SCALE = 1 as const; export function normalizeProviderArtworkInput(value: unknown): ProviderArtwork { const source = decodeProviderArtworkSource(value); @@ -15,12 +21,12 @@ export function normalizeProviderArtworkInput(value: unknown): ProviderArtwork { throw new Error("Provider artwork dimensions are invalid."); } const image = source.kind === "png" - ? nativeImage.createFromBuffer(source.bytes, { scaleFactor: 1 }) + ? nativeImage.createFromBuffer(source.bytes, { scaleFactor: PIXEL_SCALE }) : nativeImage.createFromDataURL( `data:image/svg+xml;base64,${Buffer.from(source.safeSvg!, "utf8").toString("base64")}`, ); if (image.isEmpty()) throw new Error("Aiden could not decode that provider icon."); - const size = image.getSize(); + const size = image.getSize(PIXEL_SCALE); if (size.width <= 0 || size.height <= 0 || size.width > 8_192 || size.height > 8_192) { throw new Error("Provider artwork dimensions are invalid."); } @@ -32,9 +38,18 @@ export function normalizeProviderArtworkInput(value: unknown): ProviderArtwork { quality: "best", }) : image; - const png = normalized.toPNG(); + const png = normalized.toPNG({ scaleFactor: PIXEL_SCALE }); if (png.length === 0 || png.length > PROVIDER_ARTWORK_MAX_PNG_BYTES) { throw new Error("The normalized provider icon is too complex. Choose a simpler image."); } - return { mimeType: "image/png", dataBase64: png.toString("base64") }; + const artwork = { mimeType: "image/png" as const, dataBase64: png.toString("base64") }; + if (!normalizeProviderArtwork(artwork)) { + throw new Error("The normalized provider icon is too complex. Choose a simpler image."); + } + return artwork; +} + +/** Persist only artwork that already matches the display contract, or re-encode it. */ +export function persistableProviderArtwork(value: unknown): ProviderArtwork | undefined { + return persistStoredProviderArtwork(value, (input) => normalizeProviderArtworkInput(input)); } diff --git a/package.json b/package.json index c461348d..f78f4d94 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "test:model-insights": "tsx --test main/services/openrouter-benchmark.test.ts main/services/models.test.ts main/services/provider-model-info-core.test.ts main/handlers/ipc-contract.test.ts", "test:model-pad": "tsx --test renderer/components/settings/model-pad-settings.test.tsx renderer/lib/google-provider-migration.test.ts renderer/lib/model-pad-layout.test.ts renderer/lib/model-picker-data.test.ts renderer/lib/pi-provider-display.test.ts", "test:command-system": "tsx --test main/services/native-menu-command-contract.test.ts main/services/renderer-readiness-core.test.ts main/services/shortcut-registration-core.test.ts main/services/shortcut-transaction-core.test.ts main/services/superseding-task-core.test.ts renderer/lib/appearance-intent.test.ts renderer/lib/command-palette-contract.test.ts renderer/lib/command-palette-recent.test.ts renderer/lib/command-system-core.test.ts renderer/lib/shortcut-settings-contract.test.ts renderer/lib/use-model-selection.test.ts renderer/shared/keybindings.test.ts", - "test:slash-commands": "tsx --test main/services/generation-initialization-terminal.test.ts main/handlers/attachments.contract.test.ts main/handlers/chat.parse.test.ts main/handlers/chat-create-params.test.ts main/handlers/chat-session-params.test.ts main/handlers/worktree-create-params.test.ts main/services/chat-workspace-authority.test.ts main/handlers/chats.append.contract.test.ts main/services/attachment-contract.test.ts main/services/attachments.test.ts main/services/chat-append-commit.test.ts main/services/chat-export.test.ts main/services/chat-message-contract.test.ts main/services/chat-session-copy.test.ts main/services/chat-store-core.test.ts main/services/chat-turn-admission.test.ts main/services/generation-messages.test.ts main/services/provider-auth-flow-core.test.ts main/services/provider-list-core.test.ts main/services/provider-artwork-core.test.ts main/services/scheduled-chat-creation.test.ts main/services/skill-invocation-flow.integration.test.ts main/services/skill-invocation-turn.test.ts main/services/skill-registry-core.test.ts main/services/skill-registry.test.ts main/services/skill-tools.test.ts main/services/skills-discovery.test.ts renderer/components/assistant/assistant-ui.test.tsx renderer/components/assistant/use-assistant-chat.test.ts renderer/components/composer.test.tsx renderer/lib/chat-message-queue.test.ts renderer/components/message-bubble.test.tsx renderer/lib/chat-copy-view.test.ts renderer/lib/codex-provider-cache.test.ts renderer/lib/computer-use-control.test.ts renderer/lib/ipc-stream.test.ts renderer/lib/skill-catalog-workspace.test.ts renderer/lib/slash-command-actions.test.ts renderer/lib/slash-command-core.test.ts renderer/lib/slash-command-performance.test.ts renderer/main/chat-transition.test.tsx renderer/shared/attachment-contract.test.ts renderer/shared/chat-message-contract.test.ts renderer/shared/slash-commands.test.ts renderer/components/settings/skills-settings.test.tsx", + "test:slash-commands": "tsx --test main/services/generation-initialization-terminal.test.ts main/handlers/attachments.contract.test.ts main/handlers/chat.parse.test.ts main/handlers/chat-create-params.test.ts main/handlers/chat-session-params.test.ts main/handlers/worktree-create-params.test.ts main/services/chat-workspace-authority.test.ts main/handlers/chats.append.contract.test.ts main/services/attachment-contract.test.ts main/services/attachments.test.ts main/services/chat-append-commit.test.ts main/services/chat-export.test.ts main/services/chat-message-contract.test.ts main/services/chat-session-copy.test.ts main/services/chat-store-core.test.ts main/services/chat-turn-admission.test.ts main/services/generation-messages.test.ts main/services/provider-auth-flow-core.test.ts main/services/provider-list-core.test.ts main/services/provider-artwork-core.test.ts main/services/provider-artwork.test.ts main/services/scheduled-chat-creation.test.ts main/services/skill-invocation-flow.integration.test.ts main/services/skill-invocation-turn.test.ts main/services/skill-registry-core.test.ts main/services/skill-registry.test.ts main/services/skill-tools.test.ts main/services/skills-discovery.test.ts renderer/components/assistant/assistant-ui.test.tsx renderer/components/assistant/use-assistant-chat.test.ts renderer/components/composer.test.tsx renderer/lib/chat-message-queue.test.ts renderer/components/message-bubble.test.tsx renderer/lib/chat-copy-view.test.ts renderer/lib/codex-provider-cache.test.ts renderer/lib/computer-use-control.test.ts renderer/lib/ipc-stream.test.ts renderer/lib/skill-catalog-workspace.test.ts renderer/lib/slash-command-actions.test.ts renderer/lib/slash-command-core.test.ts renderer/lib/slash-command-performance.test.ts renderer/main/chat-transition.test.tsx renderer/shared/attachment-contract.test.ts renderer/shared/chat-message-contract.test.ts renderer/shared/slash-commands.test.ts renderer/components/settings/skills-settings.test.tsx", "test:display-image": "tsx --test main/services/display-image-artifact-store.test.ts main/services/display-image-extension.test.ts main/services/generation-timeline.test.ts renderer/components/message-bubble.test.tsx renderer/lib/ipc-stream.test.ts", "test:ask-user-question": "tsx --test renderer/shared/ask-user-question.test.ts main/services/ask-user-question-coordinator.test.ts main/services/ask-user-question-extension.test.ts renderer/components/ask-user-question-composer.test.ts", "test:todo": "tsx --test main/services/rpiv-todo/*.test.ts renderer/shared/todo.test.ts renderer/components/todo-panel.test.tsx main/services/generation-timeline.test.ts main/handlers/ipc-contract.test.ts renderer/lib/ipc-stream.test.ts", @@ -109,8 +109,8 @@ "test:voice": "tsx --test main/services/transcription-core.test.ts main/services/gemini-live-transcription-core.test.ts main/services/dictation-coordinator.test.ts main/services/dictation-paste.test.ts main/services/parakeet-protocol.test.ts main/services/parakeet-process-core.test.ts main/services/parakeet-transcription-lane.test.ts renderer/shared/voice-models.test.ts renderer/shared/gemini-usage-scope.test.ts renderer/components/settings/gemini-voice-setup.test.tsx renderer/lib/accessibility-permission-core.test.ts renderer/lib/accessibility-refresh.test.ts renderer/lib/dictation-operation-gate.test.ts renderer/lib/gemini-recorded-retry.test.ts renderer/lib/live-pcm-capture.test.ts renderer/lib/voice-recorder-core.test.ts renderer/lib/wav-audio.test.ts", "test:diagnostics": "tsx --test main/services/diagnostics-contract.test.ts main/services/diagnostic-health.test.ts main/services/diagnostic-journal.test.ts main/services/diagnostic-support.test.ts main/services/dev-log.test.ts main/services/process-diagnostics.test.ts main/services/renderer-crash-recovery.test.ts main/services/renderer-diagnostic-rate.test.ts main/services/subagents/subagent-runtime-diagnostics.test.ts renderer/components/settings/diagnostics-settings.test.tsx && node --test scripts/diagnostic-policy.test.mjs", "diagnostics:failure-receipt": "node scripts/write-diagnostic-failure-receipt.mjs", - "test": "tsx --test main/services/mcp-oauth-client-metadata.test.ts main/handlers/assistant-parse.test.ts main/services/assistant/system-prompt.test.ts main/services/chat-activity-core.test.ts main/services/chat-generation-start.test.ts main/services/chat-title-policy.test.ts main/services/chat-title-routing.test.ts main/services/chat-store-core.test.ts main/services/codex-provider.test.ts main/services/coding-tools.test.ts main/services/config-store-core.test.ts main/services/computer-use/computer-use-foundation.test.ts main/services/computer-use/computer-use-tool.test.ts main/services/computer-use/generation-gate.test.ts main/services/computer-use/safety.test.ts main/services/computer-use/settings-core.test.ts main/services/computer-use/status-core.test.ts main/services/data-store.test.ts main/services/data-store.resilience.test.ts main/services/terminal.test.ts main/services/terminal-history.test.ts main/services/aiden-config-dir.test.ts main/services/portable-config-core.test.ts main/services/portable-config-core.roundtrip.test.ts main/services/portable-config-watch-core.test.ts main/services/secret-map-core.test.ts main/services/dev-log.test.ts main/services/process-diagnostics.test.ts main/services/dictation-coordinator.test.ts main/services/dictation-paste.test.ts main/services/dictation-cleanup-core.test.ts main/services/dictation-hotkey.test.ts main/services/dictation-key-state.test.ts main/services/dictation-keycode.test.ts main/services/parakeet-protocol.test.ts main/services/parakeet-process-core.test.ts main/services/foundation-models-connection.test.ts main/services/foundation-models-connection-core.test.ts main/services/gemini-context-cache.test.ts main/services/generation-bound-connection-cache.test.ts main/services/generation-context.test.ts main/services/generation-messages.test.ts main/services/generation-runtime.test.ts main/services/external-editors.test.ts main/services/git.test.ts main/services/model-runtime-core.test.ts main/services/models.test.ts main/services/mcp-oauth-operation.test.ts main/services/mcp-oauth-session.test.ts main/services/mcp-presets.test.ts renderer/shared/plugin-catalog.test.ts main/services/pi-credential-store-core.test.ts main/services/pi-provider-contract.test.ts main/services/profile-share-core.test.ts main/services/profile-share-files.test.ts main/services/profile.test.ts main/services/provider-auth-flow-core.test.ts main/services/provider-auth-owner.test.ts main/services/provider-key-policy.test.ts main/services/provider-list-core.test.ts main/services/provider-artwork-core.test.ts main/services/quit-barrier.test.ts main/services/scratch-workspace.test.ts main/services/skills-discovery.test.ts main/services/tool-approval.test.ts main/services/local-runtime-status.test.ts main/services/usage-store-core.test.ts main/services/workspace-files.test.ts main/windows/pill-window-security.test.ts renderer/components/assistant/use-assistant-chat.test.ts renderer/components/assistant/assistant-ui.test.tsx renderer/components/activity-feed.test.tsx renderer/components/environment-subagents-contract.test.ts renderer/components/subagents-panel.test.tsx renderer/components/chat-sidebar.test.tsx renderer/components/composer.test.tsx renderer/lib/chat-message-queue.test.ts renderer/main/chat-transition.test.tsx renderer/components/usage/profile-share-card.test.tsx renderer/lib/accessibility-refresh.test.ts renderer/lib/agent-activity.test.ts renderer/lib/chat-activity.test.ts renderer/lib/assistant-dock.test.ts renderer/lib/assistant-motion-contract.test.ts renderer/lib/dialog-motion-contract.test.ts renderer/lib/scrollbar-gutter-contract.test.ts renderer/lib/text-entry-focus-contract.test.ts renderer/lib/chat-deletion-cache.test.ts renderer/lib/chat-terminal-sync.test.ts renderer/lib/ipc-stream.test.ts renderer/lib/chat-title-reveal.test.ts renderer/lib/codex-auth-session.test.ts renderer/lib/codex-provider-cache.test.ts renderer/lib/composer-placeholder.test.ts renderer/lib/computer-use-notice.test.ts renderer/lib/dictation-operation-gate.test.ts renderer/lib/media-recorder-stop.test.ts renderer/lib/dictation-vad.test.ts renderer/lib/dictation-sounds.test.ts renderer/lib/editor-preference.test.ts renderer/lib/environment-panel-layout.test.ts renderer/lib/subagent-view-state.test.ts renderer/lib/truncate-path.test.ts renderer/lib/mcp-preset-state.test.ts renderer/components/settings/mcp-settings.test.tsx renderer/components/settings/mcp-preset-icons.test.ts renderer/lib/model-display.test.ts renderer/lib/model-picker-data.test.ts renderer/lib/profile-share-data.test.ts renderer/lib/sidebar-chat-shortcuts.test.ts renderer/lib/usage-profile-data.test.ts renderer/shared/appearance.test.ts renderer/components/interface-polish.test.tsx renderer/shared/provider-deployment.test.ts main/handlers/ipc-contract.test.ts main/handlers/chat.parse.test.ts main/handlers/voice-codec.test.ts main/handlers/phase2-parse.test.ts scripts/apple-developer-tools.test.mjs scripts/check-macos-release.test.mjs scripts/computer-use-packaged-acceptance.test.mjs scripts/configure-electron-fuses.test.mjs scripts/model-snapshot-core.test.mjs scripts/prepare-macos-dev-runtime.test.mjs scripts/prepare-macos-package-output.test.mjs scripts/run-macos-distribution.test.mjs scripts/sign-macos.test.mjs scripts/hide-dmg-support-files.test.mjs scripts/update-model-capabilities.test.mjs scripts/vendor-cua-driver.test.mjs scripts/verify-macos-package.test.mjs && npm run test:telegram && npm run test:worktree-remover:native && npm run test:computer-use:native && npm run test:settings-design", - "test:coverage": "tsx --test --experimental-test-coverage main/services/mcp-oauth-client-metadata.test.ts main/handlers/assistant-parse.test.ts main/services/assistant/system-prompt.test.ts main/services/chat-activity-core.test.ts main/services/chat-generation-start.test.ts main/services/chat-title-policy.test.ts main/services/chat-title-routing.test.ts main/services/chat-store-core.test.ts main/services/codex-provider.test.ts main/services/coding-tools.test.ts main/services/config-store-core.test.ts main/services/computer-use/computer-use-foundation.test.ts main/services/computer-use/computer-use-tool.test.ts main/services/computer-use/generation-gate.test.ts main/services/computer-use/safety.test.ts main/services/computer-use/settings-core.test.ts main/services/computer-use/status-core.test.ts main/services/data-store.test.ts main/services/data-store.resilience.test.ts main/services/terminal.test.ts main/services/terminal-history.test.ts main/services/aiden-config-dir.test.ts main/services/portable-config-core.test.ts main/services/portable-config-core.roundtrip.test.ts main/services/portable-config-watch-core.test.ts main/services/dev-log.test.ts main/services/process-diagnostics.test.ts main/services/dictation-coordinator.test.ts main/services/dictation-paste.test.ts main/services/dictation-cleanup-core.test.ts main/services/dictation-hotkey.test.ts main/services/dictation-key-state.test.ts main/services/dictation-keycode.test.ts main/services/parakeet-protocol.test.ts main/services/parakeet-process-core.test.ts main/services/foundation-models-connection.test.ts main/services/foundation-models-connection-core.test.ts main/services/generation-bound-connection-cache.test.ts main/services/generation-context.test.ts main/services/generation-messages.test.ts main/services/generation-runtime.test.ts main/services/external-editors.test.ts main/services/git.test.ts main/services/model-runtime-core.test.ts main/services/pi-compaction-core.test.ts main/services/models.test.ts main/services/mcp-oauth-operation.test.ts main/services/mcp-oauth-session.test.ts main/services/mcp-presets.test.ts renderer/shared/plugin-catalog.test.ts main/services/pi-credential-store-core.test.ts main/services/pi-provider-contract.test.ts main/services/profile-share-core.test.ts main/services/profile-share-files.test.ts main/services/profile.test.ts main/services/provider-auth-flow-core.test.ts main/services/provider-auth-owner.test.ts main/services/provider-key-policy.test.ts main/services/provider-list-core.test.ts main/services/provider-artwork-core.test.ts main/services/quit-barrier.test.ts main/services/scratch-workspace.test.ts main/services/skills-discovery.test.ts main/services/tool-approval.test.ts main/services/local-runtime-status.test.ts main/services/usage-store-core.test.ts main/services/workspace-files.test.ts main/windows/pill-window-security.test.ts renderer/components/assistant/use-assistant-chat.test.ts renderer/components/assistant/assistant-ui.test.tsx renderer/components/environment-subagents-contract.test.ts renderer/components/subagents-panel.test.tsx renderer/components/chat-sidebar.test.tsx renderer/components/composer.test.tsx renderer/main/chat-transition.test.tsx renderer/components/usage/profile-share-card.test.tsx renderer/lib/accessibility-refresh.test.ts renderer/lib/agent-activity.test.ts renderer/lib/chat-activity.test.ts renderer/lib/assistant-dock.test.ts renderer/lib/assistant-motion-contract.test.ts renderer/lib/dialog-motion-contract.test.ts renderer/lib/chat-deletion-cache.test.ts renderer/lib/chat-terminal-sync.test.ts renderer/lib/ipc-stream.test.ts renderer/lib/chat-title-reveal.test.ts renderer/lib/codex-auth-session.test.ts renderer/lib/codex-auth-view-state.test.ts renderer/lib/codex-provider-cache.test.ts renderer/lib/composer-placeholder.test.ts renderer/lib/computer-use-notice.test.ts renderer/lib/dictation-operation-gate.test.ts renderer/lib/media-recorder-stop.test.ts renderer/lib/dictation-vad.test.ts renderer/lib/dictation-sounds.test.ts renderer/lib/editor-preference.test.ts renderer/lib/environment-panel-layout.test.ts renderer/lib/subagent-view-state.test.ts renderer/lib/truncate-path.test.ts renderer/lib/mcp-preset-state.test.ts renderer/components/settings/mcp-settings.test.tsx renderer/components/settings/mcp-preset-icons.test.ts renderer/lib/model-display.test.ts renderer/lib/profile-share-data.test.ts renderer/lib/sidebar-chat-shortcuts.test.ts renderer/lib/usage-profile-data.test.ts renderer/shared/appearance.test.ts renderer/shared/provider-deployment.test.ts main/handlers/ipc-contract.test.ts main/handlers/chat.parse.test.ts main/handlers/voice-codec.test.ts main/handlers/phase2-parse.test.ts scripts/apple-developer-tools.test.mjs scripts/check-macos-release.test.mjs scripts/computer-use-packaged-acceptance.test.mjs scripts/configure-electron-fuses.test.mjs scripts/model-snapshot-core.test.mjs scripts/prepare-macos-dev-runtime.test.mjs scripts/prepare-macos-package-output.test.mjs scripts/run-macos-distribution.test.mjs scripts/sign-macos.test.mjs scripts/update-model-capabilities.test.mjs scripts/vendor-cua-driver.test.mjs scripts/verify-macos-package.test.mjs", + "test": "tsx --test main/services/mcp-oauth-client-metadata.test.ts main/handlers/assistant-parse.test.ts main/services/assistant/system-prompt.test.ts main/services/chat-activity-core.test.ts main/services/chat-generation-start.test.ts main/services/chat-title-policy.test.ts main/services/chat-title-routing.test.ts main/services/chat-store-core.test.ts main/services/codex-provider.test.ts main/services/coding-tools.test.ts main/services/config-store-core.test.ts main/services/computer-use/computer-use-foundation.test.ts main/services/computer-use/computer-use-tool.test.ts main/services/computer-use/generation-gate.test.ts main/services/computer-use/safety.test.ts main/services/computer-use/settings-core.test.ts main/services/computer-use/status-core.test.ts main/services/data-store.test.ts main/services/data-store.resilience.test.ts main/services/terminal.test.ts main/services/terminal-history.test.ts main/services/aiden-config-dir.test.ts main/services/portable-config-core.test.ts main/services/portable-config-core.roundtrip.test.ts main/services/portable-config-watch-core.test.ts main/services/secret-map-core.test.ts main/services/dev-log.test.ts main/services/process-diagnostics.test.ts main/services/dictation-coordinator.test.ts main/services/dictation-paste.test.ts main/services/dictation-cleanup-core.test.ts main/services/dictation-hotkey.test.ts main/services/dictation-key-state.test.ts main/services/dictation-keycode.test.ts main/services/parakeet-protocol.test.ts main/services/parakeet-process-core.test.ts main/services/foundation-models-connection.test.ts main/services/foundation-models-connection-core.test.ts main/services/gemini-context-cache.test.ts main/services/generation-bound-connection-cache.test.ts main/services/generation-context.test.ts main/services/generation-messages.test.ts main/services/generation-runtime.test.ts main/services/external-editors.test.ts main/services/git.test.ts main/services/model-runtime-core.test.ts main/services/models.test.ts main/services/mcp-oauth-operation.test.ts main/services/mcp-oauth-session.test.ts main/services/mcp-presets.test.ts renderer/shared/plugin-catalog.test.ts main/services/pi-credential-store-core.test.ts main/services/pi-provider-contract.test.ts main/services/profile-share-core.test.ts main/services/profile-share-files.test.ts main/services/profile.test.ts main/services/provider-auth-flow-core.test.ts main/services/provider-auth-owner.test.ts main/services/provider-key-policy.test.ts main/services/provider-list-core.test.ts main/services/provider-artwork-core.test.ts main/services/provider-artwork.test.ts main/services/quit-barrier.test.ts main/services/scratch-workspace.test.ts main/services/skills-discovery.test.ts main/services/tool-approval.test.ts main/services/local-runtime-status.test.ts main/services/usage-store-core.test.ts main/services/workspace-files.test.ts main/windows/pill-window-security.test.ts renderer/components/assistant/use-assistant-chat.test.ts renderer/components/assistant/assistant-ui.test.tsx renderer/components/activity-feed.test.tsx renderer/components/environment-subagents-contract.test.ts renderer/components/subagents-panel.test.tsx renderer/components/chat-sidebar.test.tsx renderer/components/composer.test.tsx renderer/lib/chat-message-queue.test.ts renderer/main/chat-transition.test.tsx renderer/components/usage/profile-share-card.test.tsx renderer/lib/accessibility-refresh.test.ts renderer/lib/agent-activity.test.ts renderer/lib/chat-activity.test.ts renderer/lib/assistant-dock.test.ts renderer/lib/assistant-motion-contract.test.ts renderer/lib/dialog-motion-contract.test.ts renderer/lib/scrollbar-gutter-contract.test.ts renderer/lib/text-entry-focus-contract.test.ts renderer/lib/chat-deletion-cache.test.ts renderer/lib/chat-terminal-sync.test.ts renderer/lib/ipc-stream.test.ts renderer/lib/chat-title-reveal.test.ts renderer/lib/codex-auth-session.test.ts renderer/lib/codex-provider-cache.test.ts renderer/lib/composer-placeholder.test.ts renderer/lib/computer-use-notice.test.ts renderer/lib/dictation-operation-gate.test.ts renderer/lib/media-recorder-stop.test.ts renderer/lib/dictation-vad.test.ts renderer/lib/dictation-sounds.test.ts renderer/lib/editor-preference.test.ts renderer/lib/environment-panel-layout.test.ts renderer/lib/subagent-view-state.test.ts renderer/lib/truncate-path.test.ts renderer/lib/mcp-preset-state.test.ts renderer/components/settings/mcp-settings.test.tsx renderer/components/settings/mcp-preset-icons.test.ts renderer/lib/model-display.test.ts renderer/lib/model-picker-data.test.ts renderer/lib/profile-share-data.test.ts renderer/lib/sidebar-chat-shortcuts.test.ts renderer/lib/usage-profile-data.test.ts renderer/shared/appearance.test.ts renderer/components/interface-polish.test.tsx renderer/shared/provider-deployment.test.ts main/handlers/ipc-contract.test.ts main/handlers/chat.parse.test.ts main/handlers/voice-codec.test.ts main/handlers/phase2-parse.test.ts scripts/apple-developer-tools.test.mjs scripts/check-macos-release.test.mjs scripts/computer-use-packaged-acceptance.test.mjs scripts/configure-electron-fuses.test.mjs scripts/model-snapshot-core.test.mjs scripts/prepare-macos-dev-runtime.test.mjs scripts/prepare-macos-package-output.test.mjs scripts/run-macos-distribution.test.mjs scripts/sign-macos.test.mjs scripts/hide-dmg-support-files.test.mjs scripts/update-model-capabilities.test.mjs scripts/vendor-cua-driver.test.mjs scripts/verify-macos-package.test.mjs && npm run test:telegram && npm run test:worktree-remover:native && npm run test:computer-use:native && npm run test:settings-design", + "test:coverage": "tsx --test --experimental-test-coverage main/services/mcp-oauth-client-metadata.test.ts main/handlers/assistant-parse.test.ts main/services/assistant/system-prompt.test.ts main/services/chat-activity-core.test.ts main/services/chat-generation-start.test.ts main/services/chat-title-policy.test.ts main/services/chat-title-routing.test.ts main/services/chat-store-core.test.ts main/services/codex-provider.test.ts main/services/coding-tools.test.ts main/services/config-store-core.test.ts main/services/computer-use/computer-use-foundation.test.ts main/services/computer-use/computer-use-tool.test.ts main/services/computer-use/generation-gate.test.ts main/services/computer-use/safety.test.ts main/services/computer-use/settings-core.test.ts main/services/computer-use/status-core.test.ts main/services/data-store.test.ts main/services/data-store.resilience.test.ts main/services/terminal.test.ts main/services/terminal-history.test.ts main/services/aiden-config-dir.test.ts main/services/portable-config-core.test.ts main/services/portable-config-core.roundtrip.test.ts main/services/portable-config-watch-core.test.ts main/services/dev-log.test.ts main/services/process-diagnostics.test.ts main/services/dictation-coordinator.test.ts main/services/dictation-paste.test.ts main/services/dictation-cleanup-core.test.ts main/services/dictation-hotkey.test.ts main/services/dictation-key-state.test.ts main/services/dictation-keycode.test.ts main/services/parakeet-protocol.test.ts main/services/parakeet-process-core.test.ts main/services/foundation-models-connection.test.ts main/services/foundation-models-connection-core.test.ts main/services/generation-bound-connection-cache.test.ts main/services/generation-context.test.ts main/services/generation-messages.test.ts main/services/generation-runtime.test.ts main/services/external-editors.test.ts main/services/git.test.ts main/services/model-runtime-core.test.ts main/services/pi-compaction-core.test.ts main/services/models.test.ts main/services/mcp-oauth-operation.test.ts main/services/mcp-oauth-session.test.ts main/services/mcp-presets.test.ts renderer/shared/plugin-catalog.test.ts main/services/pi-credential-store-core.test.ts main/services/pi-provider-contract.test.ts main/services/profile-share-core.test.ts main/services/profile-share-files.test.ts main/services/profile.test.ts main/services/provider-auth-flow-core.test.ts main/services/provider-auth-owner.test.ts main/services/provider-key-policy.test.ts main/services/provider-list-core.test.ts main/services/provider-artwork-core.test.ts main/services/provider-artwork.test.ts main/services/quit-barrier.test.ts main/services/scratch-workspace.test.ts main/services/skills-discovery.test.ts main/services/tool-approval.test.ts main/services/local-runtime-status.test.ts main/services/usage-store-core.test.ts main/services/workspace-files.test.ts main/windows/pill-window-security.test.ts renderer/components/assistant/use-assistant-chat.test.ts renderer/components/assistant/assistant-ui.test.tsx renderer/components/environment-subagents-contract.test.ts renderer/components/subagents-panel.test.tsx renderer/components/chat-sidebar.test.tsx renderer/components/composer.test.tsx renderer/main/chat-transition.test.tsx renderer/components/usage/profile-share-card.test.tsx renderer/lib/accessibility-refresh.test.ts renderer/lib/agent-activity.test.ts renderer/lib/chat-activity.test.ts renderer/lib/assistant-dock.test.ts renderer/lib/assistant-motion-contract.test.ts renderer/lib/dialog-motion-contract.test.ts renderer/lib/chat-deletion-cache.test.ts renderer/lib/chat-terminal-sync.test.ts renderer/lib/ipc-stream.test.ts renderer/lib/chat-title-reveal.test.ts renderer/lib/codex-auth-session.test.ts renderer/lib/codex-auth-view-state.test.ts renderer/lib/codex-provider-cache.test.ts renderer/lib/composer-placeholder.test.ts renderer/lib/computer-use-notice.test.ts renderer/lib/dictation-operation-gate.test.ts renderer/lib/media-recorder-stop.test.ts renderer/lib/dictation-vad.test.ts renderer/lib/dictation-sounds.test.ts renderer/lib/editor-preference.test.ts renderer/lib/environment-panel-layout.test.ts renderer/lib/subagent-view-state.test.ts renderer/lib/truncate-path.test.ts renderer/lib/mcp-preset-state.test.ts renderer/components/settings/mcp-settings.test.tsx renderer/components/settings/mcp-preset-icons.test.ts renderer/lib/model-display.test.ts renderer/lib/profile-share-data.test.ts renderer/lib/sidebar-chat-shortcuts.test.ts renderer/lib/usage-profile-data.test.ts renderer/shared/appearance.test.ts renderer/shared/provider-deployment.test.ts main/handlers/ipc-contract.test.ts main/handlers/chat.parse.test.ts main/handlers/voice-codec.test.ts main/handlers/phase2-parse.test.ts scripts/apple-developer-tools.test.mjs scripts/check-macos-release.test.mjs scripts/computer-use-packaged-acceptance.test.mjs scripts/configure-electron-fuses.test.mjs scripts/model-snapshot-core.test.mjs scripts/prepare-macos-dev-runtime.test.mjs scripts/prepare-macos-package-output.test.mjs scripts/run-macos-distribution.test.mjs scripts/sign-macos.test.mjs scripts/update-model-capabilities.test.mjs scripts/vendor-cua-driver.test.mjs scripts/verify-macos-package.test.mjs", "test:computer-use": "tsx --test main/services/computer-use/computer-use-foundation.test.ts main/services/computer-use/computer-use-tool.test.ts main/services/computer-use/generation-gate.test.ts main/services/computer-use/safety.test.ts main/services/computer-use/settings-core.test.ts main/services/computer-use/status-core.test.ts main/services/data-store.test.ts main/services/generation-messages.test.ts main/services/generation-runtime.test.ts main/services/quit-barrier.test.ts main/services/tool-approval.test.ts scripts/check-macos-release.test.mjs scripts/computer-use-packaged-acceptance.test.mjs scripts/configure-electron-fuses.test.mjs scripts/prepare-macos-package-output.test.mjs scripts/run-macos-distribution.test.mjs scripts/sign-macos.test.mjs scripts/vendor-cua-driver.test.mjs scripts/verify-macos-package.test.mjs && npm run test:computer-use:native", "test:computer-use:packaged": "node scripts/computer-use-packaged-acceptance.mjs", "test:computer-use:native": "cd native/computer-use-broker && CARGO_TARGET_DIR=../../build/computer-use-broker-test cargo fmt -- --check && CARGO_TARGET_DIR=../../build/computer-use-broker-test cargo test --locked && CARGO_TARGET_DIR=../../build/computer-use-broker-test cargo clippy --locked --all-targets -- -D warnings", diff --git a/renderer/components/composer.test.tsx b/renderer/components/composer.test.tsx index fa3ed992..b9adf6f2 100644 --- a/renderer/components/composer.test.tsx +++ b/renderer/components/composer.test.tsx @@ -294,4 +294,11 @@ test("model picker details sit beside the menu without overlapping the pad", () styles, /\.model-pad:focus-visible\s*\{\s*outline: none !important;\s*box-shadow:\s*inset 0 0 0 2px var\(--focus-ring\)/u, ); + assert.match(pad, /import \{ ProviderIcon \} from "\.\/provider-icon"/u); + assert.match(pad, /artwork=\{puckPoint\.providerArtwork\}/u); + assert.match(styles, /\.model-pad-knob\s*\{[^}]*color: #111110/u); + assert.match( + styles, + /\.model-pad-knob\[data-confirmed="true"\]\s*\{[^}]*color: var\(--accent-foreground\)/u, + ); }); diff --git a/renderer/components/model-picker-pad.tsx b/renderer/components/model-picker-pad.tsx index 467419c2..bc752c79 100644 --- a/renderer/components/model-picker-pad.tsx +++ b/renderer/components/model-picker-pad.tsx @@ -14,6 +14,7 @@ import { modelPadTopPercent, } from "../lib/model-pad-layout"; import { cn } from "../lib/ui-utils"; +import { ProviderIcon } from "./provider-icon"; function pointFromPointer(event: React.PointerEvent, rect: DOMRect): ModelPoint { const inset = MODEL_PAD_INSET_PERCENT / 100; @@ -284,12 +285,20 @@ export function ModelPickerPad({ ) : null}

diff --git a/renderer/components/provider-icon.tsx b/renderer/components/provider-icon.tsx index 100c2d6c..e1401aa3 100644 --- a/renderer/components/provider-icon.tsx +++ b/renderer/components/provider-icon.tsx @@ -78,6 +78,39 @@ const MULTICOLOR_PROVIDER_ICON_SLUGS = new Set([ "zai-coding-cn", ]); +const THEMED_MARK_STYLE = { + backgroundColor: "currentColor", + WebkitMaskPosition: "center", + maskPosition: "center", + WebkitMaskRepeat: "no-repeat", + maskRepeat: "no-repeat", + WebkitMaskSize: "contain", + maskSize: "contain", +} as const; + +function ThemedProviderMark({ + iconUrl, + mark, + className, +}: { + iconUrl: string; + mark: string; + className?: string; +}) { + return ( +