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
This commit is contained in:
108
src/components/agents/skills/SurfaceSelect.tsx
Normal file
108
src/components/agents/skills/SurfaceSelect.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user