Files
krow_talent_app/src/components/agents/skills/SurfaceSelect.tsx
Aravind dde4ba62c6 refactor(ts-migration): Phase 11 batch 6 — agent and skill components
The fifteen files under `src/components/agents/` (including `skills/`)
and `src/components/skills/`. 15/15 erase byte-identically; bundle hash
unchanged.

Renaming raised 43 errors and 29 of them came from one thing: these
files carry 24 `/** @param {any} props */` annotations, one per
component, and JSDoc stops applying at the extension boundary. Without
them TypeScript infers every destructured prop as required, so a call
site passing seven of nine props is an error — which is how a file that
declared its props `any` ended up with fourteen complaints about missing
`className`. Restoring the author's own declaration as `: any` is not
blanket typing; it is the annotation that was already there, in the only
form that still works.

`Workspace` in `AgentCanvas` was the one component in that file its
author left without the hatch. It now matches its siblings.

The rest were five separate things:

  - `React.isValidElement(children)` no longer narrows enough to read
    `children.props.id`: React 19 types `ReactElement`'s props as
    `unknown`. `isValidElement<any>` says what the `cloneElement` call
    beneath it has always assumed. The migration plan predicted this
    site by name.
  - `useSkillSections(page, placement)` is called with one argument by
    `UiEditingProvider`, which its doc comment explicitly permits —
    "called with no placement it returns every section on the page". The
    parameter simply lacked its optional marker. The marker erases, so
    the emitted signature is unchanged.
  - `new Date(b.at) - new Date(a.at)` is valueOf coercion, which
    JavaScript performs and TypeScript refuses to describe. Cast rather
    than rewritten to `.getTime()`: that would change the emitted code,
    and this comparison orders the list.
  - `Object.values<any>` on a tally, the same inference gap as earlier
    batches, which also fixed a `ReactNode` complaint downstream of it.

All fifteen are linted under their new extensions, checked by name.

Measured against `446df7b`:

  typecheck   6 errors, down from 9; no new error anywhere
  lint        exit 0, 0 errors, 289 warnings, 15/15 linted by name
  npm test    1684/1691, the same 7 failures verbatim
  build       exit 0, identical bundle hash 74d17e2d…
  type erasure  72/72 byte-identical across Phase 11 so far

No baseline artifact touched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8
2026-09-18 15:32:14 +05:30

109 lines
4.0 KiB
TypeScript

import * as React from 'react';
import { Check, Layers, LayoutTemplate, Sparkles } from 'lucide-react';
import { cn } from '@/lib/utils';
/**
* Which surface this capability should run on.
*
* This used to be a read-out — two rows saying "✓ Owliver / — Board". It is now
* the choice it always described, and the constraint is the important part:
* **what can be picked is derived from the definition, never from a name.** A
* skill's facets say whether it teaches Owliver, draws on a page, or both, and
* an option the definition does not support is offered disabled with the reason
* rather than hidden — a reader deciding between surfaces should be able to see
* that the other one exists and why it is not available here.
*
* `Both` appears only when the definition genuinely supports both. Selecting it
* forks nothing: there is one logical skill, and this is a mode on it.
*/
const OPTIONS = [
{
id: 'owliver',
icon: Sparkles,
label: 'Owliver',
hint: 'Can be asked for in the assistant.',
supported: (type) => type === 'owliver' || type === 'both',
},
{
id: 'board',
icon: LayoutTemplate,
label: 'Board',
hint: 'Draws a section on the page.',
supported: (type) => type === 'board' || type === 'both',
},
{
id: 'both',
icon: Layers,
label: 'Both',
hint: 'Assistant and page.',
supported: (type) => type === 'both',
},
];
/** The surfaces this definition can be run on, in order. */
export const supportedSurfaces = (type) =>
OPTIONS.filter((o) => o.supported(type)).map((o) => o.id);
export function SurfaceSelect({ type, value, onChange, name }: any) {
return (
<div role="radiogroup" aria-label={`Run ${name} as`} className="space-y-1.5">
{OPTIONS.map((option) => {
const available = option.supported(type);
const selected = available && value === option.id;
const Icon = option.icon;
return (
<button
key={option.id}
type="button"
role="radio"
aria-checked={selected}
disabled={!available}
onClick={() => available && onChange(option.id)}
className={cn(
`flex w-full items-start gap-2 rounded-xl border px-2.5 py-2 text-left
transition-colors duration-base focus-visible:outline-none
focus-visible:ring-2 focus-visible:ring-krow-blue/40`,
!available && 'cursor-not-allowed border-dashed border-border bg-surface-subtle/40',
available && !selected && 'border-border bg-surface hover:border-krow-blue/30',
selected && 'border-krow-blue bg-krow-blue-tint'
)}
>
{/* The mark carries the state for anyone not reading colour; the
`aria-checked` above carries it for anyone not reading at all. */}
<span
className={cn(
'mt-px grid h-4 w-4 shrink-0 place-items-center rounded-full border',
selected ? 'border-krow-blue bg-krow-blue text-white' : 'border-border bg-surface'
)}
aria-hidden="true"
>
{selected && <Check className="h-2.5 w-2.5" />}
</span>
<span className="min-w-0 flex-1">
<span
className={cn(
'flex items-center gap-1.5 text-body-sm font-medium',
!available ? 'text-ink-4' : selected ? 'text-krow-blue' : 'text-ink-1'
)}
>
<Icon className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
{option.label}
</span>
<span className="mt-0.5 block text-caption leading-snug text-ink-3">
{available
? option.hint
: `This capability does not declare ${option.id === 'both' ? 'both surfaces' : `a ${option.label} surface`}.`}
</span>
</span>
</button>
);
})}
</div>
);
}
export default SurfaceSelect;