diff --git a/Cargo.toml b/Cargo.toml index a73910f4..9be11692 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,4 +23,15 @@ hex = "0.4" thiserror = "2" uuid = { version = "1", features = ["v4"] } regex = "1" -ts-rs = "12" +# `no-serde-warnings` silences one diagnostic class: ts-rs warns and skips when +# it meets a serde attribute it cannot model, and `McpParameter::options` uses +# `deserialize_with` (see `mcp::model::de_options`) which ts-rs has no concept +# of. Skipping it is correct -- the field always SERIALIZES back as a plain +# list, so the emitted `Array` is right -- but the warning printed on +# every `cargo clippy` run regardless. +# +# Nothing is lost by silencing it. The generated bindings are committed and CI's +# "Fail on stale generated artifacts" step diffs them, so if ts-rs ever emits a +# different shape for this field the build fails with an error instead of a +# warning nobody reads. +ts-rs = { version = "12", features = ["no-serde-warnings"] } diff --git a/apps/desktop/src/renderer/shared/ui/DescriptionText/DescriptionText.tsx b/apps/desktop/src/renderer/shared/ui/DescriptionText/DescriptionText.tsx index 7944f100..13591178 100644 --- a/apps/desktop/src/renderer/shared/ui/DescriptionText/DescriptionText.tsx +++ b/apps/desktop/src/renderer/shared/ui/DescriptionText/DescriptionText.tsx @@ -20,13 +20,10 @@ * `DescriptionSpan[]` value satisfies it structurally. */ import { cx } from '../../lib'; +import { spansToKeyedParts } from './spansToKeyedParts'; +import type { DescriptionSpan } from './spansToKeyedParts'; import './DescriptionText.scss'; -/** One piece of a parsed description: plain text, or a link with its own - * display text and target url. Structurally identical to the backend's - * generated `DescriptionSpan`. */ -export type DescriptionSpan = { kind: 'text'; text: string } | { kind: 'link'; text: string; url: string }; - export interface DescriptionTextProps { readonly spans: readonly DescriptionSpan[]; /** Called with a link span's own `url` when its button is clicked. Never @@ -40,15 +37,6 @@ export interface DescriptionTextProps { readonly 'data-testid'?: string; } -/** One span plus a stable React key. Keyed by position: spans never reorder - * once parsed, so a position-based key stays distinct even when two link - * spans repeat the same text and url. */ -export type KeyedDescriptionSpan = DescriptionSpan & { readonly key: string }; - -export function spansToKeyedParts(spans: readonly DescriptionSpan[]): KeyedDescriptionSpan[] { - return spans.map((span, index) => ({ ...span, key: String(index) })); -} - export function DescriptionText({ spans, onOpenLink, className, 'data-testid': testId }: DescriptionTextProps) { return ( diff --git a/apps/desktop/src/renderer/shared/ui/DescriptionText/index.ts b/apps/desktop/src/renderer/shared/ui/DescriptionText/index.ts index 68bbeb1b..1e7d09a8 100644 --- a/apps/desktop/src/renderer/shared/ui/DescriptionText/index.ts +++ b/apps/desktop/src/renderer/shared/ui/DescriptionText/index.ts @@ -1,2 +1,3 @@ export { DescriptionText } from './DescriptionText'; -export type { DescriptionTextProps, DescriptionSpan } from './DescriptionText'; +export type { DescriptionTextProps } from './DescriptionText'; +export type { DescriptionSpan } from './spansToKeyedParts'; diff --git a/apps/desktop/src/renderer/shared/ui/DescriptionText/DescriptionText.test.ts b/apps/desktop/src/renderer/shared/ui/DescriptionText/spansToKeyedParts.test.ts similarity index 91% rename from apps/desktop/src/renderer/shared/ui/DescriptionText/DescriptionText.test.ts rename to apps/desktop/src/renderer/shared/ui/DescriptionText/spansToKeyedParts.test.ts index 6c1790a7..1d28c6af 100644 --- a/apps/desktop/src/renderer/shared/ui/DescriptionText/DescriptionText.test.ts +++ b/apps/desktop/src/renderer/shared/ui/DescriptionText/spansToKeyedParts.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { spansToKeyedParts } from './DescriptionText'; -import type { DescriptionSpan } from './DescriptionText'; +import { spansToKeyedParts } from './spansToKeyedParts'; +import type { DescriptionSpan } from './spansToKeyedParts'; describe('spansToKeyedParts', () => { it('keeps text and link spans in order with stable keys', () => { diff --git a/apps/desktop/src/renderer/shared/ui/DescriptionText/spansToKeyedParts.ts b/apps/desktop/src/renderer/shared/ui/DescriptionText/spansToKeyedParts.ts new file mode 100644 index 00000000..7627c063 --- /dev/null +++ b/apps/desktop/src/renderer/shared/ui/DescriptionText/spansToKeyedParts.ts @@ -0,0 +1,29 @@ +/** + * The description span model and its React keying. + * + * Kept out of `DescriptionText.tsx` for two reasons that point the same way. + * Renderer tests here are node-only -- no jsdom, no testing-library -- so a + * component cannot be unit tested and pure logic has to live where a test can + * reach it. And exporting a function from a file that also exports a component + * breaks fast refresh, which is what `react-refresh/only-export-components` + * reported for as long as this lived there. + * + * Same split, for the same reason, as + * `features/skillInstall/lib/installSelection.ts`. + */ + +/** One piece of a parsed description: plain text, or a link with its own + * display text and target url. Structurally identical to the backend's + * generated `DescriptionSpan`, declared locally so this generic component has + * no dependency on the `services` layer -- any concrete `DescriptionSpan[]` + * value satisfies it. */ +export type DescriptionSpan = { kind: 'text'; text: string } | { kind: 'link'; text: string; url: string }; + +/** One span plus a stable React key. Keyed by position: spans never reorder + * once parsed, so a position-based key stays distinct even when two link + * spans repeat the same text and url. */ +export type KeyedDescriptionSpan = DescriptionSpan & { readonly key: string }; + +export function spansToKeyedParts(spans: readonly DescriptionSpan[]): KeyedDescriptionSpan[] { + return spans.map((span, index) => ({ ...span, key: String(index) })); +}