Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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<McpOption>` 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"] }
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 (
<span className={cx('sk-description', className)} data-testid={testId}>
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/src/renderer/shared/ui/DescriptionText/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { DescriptionText } from './DescriptionText';
export type { DescriptionTextProps, DescriptionSpan } from './DescriptionText';
export type { DescriptionTextProps } from './DescriptionText';
export type { DescriptionSpan } from './spansToKeyedParts';
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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) }));
}
Loading