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:
2026-09-18 15:32:14 +05:30
parent 446df7b37b
commit dde4ba62c6
15 changed files with 30 additions and 53 deletions

View File

@@ -44,7 +44,7 @@ import OwliverAvatar from '@/components/krow/OwliverAvatar';
* holds a ~380px track on the right of every Admin page, so below `xl` the * holds a ~380px track on the right of every Admin page, so below `xl` the
* remaining width belongs to the editor and the rail becomes a strip above it. * remaining width belongs to the editor and the rail becomes a strip above it.
*/ */
export function Workspace({ rail, children, className }) { export function Workspace({ rail, children, className }: any) {
return ( return (
<div className={cn('w-full rounded-2xl border border-border bg-surface shadow-xs overflow-hidden', className)}> <div className={cn('w-full rounded-2xl border border-border bg-surface shadow-xs overflow-hidden', className)}>
<div className="border-b border-border/80 bg-surface-subtle/40 px-4 py-3 sm:px-6"> <div className="border-b border-border/80 bg-surface-subtle/40 px-4 py-3 sm:px-6">
@@ -64,8 +64,7 @@ export function Workspace({ rail, children, className }) {
* height without measuring it in JavaScript. Content stays mounted, which is * height without measuring it in JavaScript. Content stays mounted, which is
* what keeps a section addressable by the rail while it is closed. * what keeps a section addressable by the rail while it is closed.
*/ */
/** @param {any} props */ export function Collapse({ open, children, className }: any) {
export function Collapse({ open, children, className }) {
return ( return (
<div <div
className={cn( className={cn(
@@ -100,8 +99,7 @@ export function Collapse({ open, children, className }) {
* Provides clear section navigation and leaf anchor shortcuts across the * Provides clear section navigation and leaf anchor shortcuts across the
* configuration workspace. * configuration workspace.
*/ */
/** @param {any} props */ export function Rail({ sections, activeId, onSelect }: any) {
export function Rail({ sections, activeId, onSelect }) {
const activeSection = sections.find((s) => s.id === activeId); const activeSection = sections.find((s) => s.id === activeId);
return ( return (
@@ -181,11 +179,10 @@ export function Rail({ sections, activeId, onSelect }) {
/** /**
* One section of the document. * One section of the document.
*/ */
/** @param {any} props */
export function DocSection({ export function DocSection({
id, icon: Icon, title, description, meta, open, onToggle, emphasis = false, last = false, id, icon: Icon, title, description, meta, open, onToggle, emphasis = false, last = false,
children, children,
}) { }: any) {
return ( return (
<section <section
id={id} id={id}
@@ -257,8 +254,7 @@ export function DocSection({
} }
/** A labelled field in the document body. */ /** A labelled field in the document body. */
/** @param {any} props */ export function DocField({ id, label, hint, children, className }: any) {
export function DocField({ id, label, hint, children, className }) {
const reactId = React.useId(); const reactId = React.useId();
const controlId = id ? `${id}-control` : reactId; const controlId = id ? `${id}-control` : reactId;
@@ -270,7 +266,7 @@ export function DocField({ id, label, hint, children, className }) {
> >
{label} {label}
</label> </label>
{React.isValidElement(children) {React.isValidElement<any>(children)
? React.cloneElement(children, { id: children.props.id || controlId }) ? React.cloneElement(children, { id: children.props.id || controlId })
: children} : children}
{hint && <p className="text-caption leading-relaxed text-ink-4">{hint}</p>} {hint && <p className="text-caption leading-relaxed text-ink-4">{hint}</p>}
@@ -284,8 +280,7 @@ export function DocField({ id, label, hint, children, className }) {
* One notch quieter than a section title and one louder than a row, which is * One notch quieter than a section title and one louder than a row, which is
* exactly the level of the thing it names. * exactly the level of the thing it names.
*/ */
/** @param {any} props */ export function GroupHead({ id, icon: Icon, title, count, action, className }: any) {
export function GroupHead({ id, icon: Icon, title, count, action, className }) {
return ( return (
<div <div
id={id} id={id}
@@ -310,8 +305,7 @@ export function GroupHead({ id, icon: Icon, title, count, action, className }) {
* competing with the section that holds them; as rows they read as a list, * competing with the section that holds them; as rows they read as a list,
* which is what they are. * which is what they are.
*/ */
/** @param {any} props */ export function ItemRow({ icon: Icon, title, detail, warning, onRemove, removeLabel }: any) {
export function ItemRow({ icon: Icon, title, detail, warning, onRemove, removeLabel }) {
return ( return (
<li <li
className={`group/item -mx-2 flex items-start gap-2.5 rounded-lg px-2 py-2 className={`group/item -mx-2 flex items-start gap-2.5 rounded-lg px-2 py-2
@@ -352,10 +346,9 @@ export function ItemRow({ icon: Icon, title, detail, warning, onRemove, removeLa
* a switch in a button makes one press mean two things. A setting that needs * a switch in a button makes one press mean two things. A setting that needs
* space gets a chevron and opens underneath. * space gets a chevron and opens underneath.
*/ */
/** @param {any} props */
export function SettingRow({ export function SettingRow({
id, icon: Icon, title, value, description, control, children, open, onToggle, last = false, id, icon: Icon, title, value, description, control, children, open, onToggle, last = false,
}) { }: any) {
const expandable = Boolean(children); const expandable = Boolean(children);
const body = ( const body = (
@@ -412,8 +405,7 @@ export function SettingRow({
} }
/** A page an agent covers, or one it could. */ /** A page an agent covers, or one it could. */
/** @param {any} props */ export function ScopeChip({ active, children, ...props }: any) {
export function ScopeChip({ active, children, ...props }) {
return ( return (
<button <button
type="button" type="button"
@@ -443,8 +435,7 @@ export function ScopeChip({ active, children, ...props }) {
/* ── Identity ────────────────────────────────────────────────────────────── */ /* ── Identity ────────────────────────────────────────────────────────────── */
/** The icons an agent may wear, as a picker. */ /** The icons an agent may wear, as a picker. */
/** @param {any} props */ export function IconPicker({ icons, value, onChange }: any) {
export function IconPicker({ icons, value, onChange }) {
return ( return (
<div className="grid grid-cols-5 gap-1.5"> <div className="grid grid-cols-5 gap-1.5">
{icons.map((icon) => { {icons.map((icon) => {
@@ -486,8 +477,7 @@ export function IconPicker({ icons, value, onChange }) {
* same three things the switcher renders — icon, name, description — and * same three things the switcher renders — icon, name, description — and
* nothing it does not. * nothing it does not.
*/ */
/** @param {any} props */ export function AgentPreview({ name, description, icon }: any) {
export function AgentPreview({ name, description, icon }) {
const Icon = agentIconFor(icon); const Icon = agentIconFor(icon);
return ( return (

View File

@@ -94,7 +94,6 @@ function useActiveSection(ids) {
return [active, setActive]; return [active, setActive];
} }
/** @param {any} props */
export function AgentConfigure({ export function AgentConfigure({
fields, agents, customSkills = [], onChange, onOpenSkills, scopeLocked = false, fields, agents, customSkills = [], onChange, onOpenSkills, scopeLocked = false,
/* Attaching and detaching a skill goes through the caller's existing write /* Attaching and detaching a skill goes through the caller's existing write
@@ -107,7 +106,7 @@ export function AgentConfigure({
/* The tools this deployment registers, from GET /api/v1/tools. Passed in /* The tools this deployment registers, from GET /api/v1/tools. Passed in
rather than fetched here so this component stays a form over `fields`. */ rather than fetched here so this component stays a form over `fields`. */
toolCatalogue = [], toolCatalogue = [],
}) { }: any) {
/* Sections are open by default: this is a document, and one that greets its /* Sections are open by default: this is a document, and one that greets its
author with four closed headers hides the thing they came to write. Closing author with four closed headers hides the thing they came to write. Closing
is for focus, not for the initial reading. */ is for focus, not for the initial reading. */

View File

@@ -22,8 +22,7 @@ import { cn } from '@/lib/utils';
* Real-time usage analytics computed directly from the local telemetry log. * Real-time usage analytics computed directly from the local telemetry log.
*/ */
/** @param {any} props */ function MetricCard({ icon: Icon, label, value, detail, highlight }: any) {
function MetricCard({ icon: Icon, label, value, detail, highlight }) {
return ( return (
<div className="rounded-2xl border border-border bg-surface p-5 shadow-xs space-y-2"> <div className="rounded-2xl border border-border bg-surface p-5 shadow-xs space-y-2">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
@@ -45,8 +44,7 @@ function MetricCard({ icon: Icon, label, value, detail, highlight }) {
); );
} }
/** @param {any} props */ export function AgentInsightsPanel({ agentId, agentName }: any) {
export function AgentInsightsPanel({ agentId, agentName }) {
const preferences = usePreferences(); const preferences = usePreferences();
const [records] = React.useState(() => readHistory()); const [records] = React.useState(() => readHistory());

View File

@@ -46,8 +46,7 @@ const CLASSIFICATION_COPY = {
combined: 'Requires both structured records and knowledge retrieval.', combined: 'Requires both structured records and knowledge retrieval.',
}; };
/** @param {any} props */ export function AgentTestPanel({ fields, dirty, customSkills = [], onRunLive }: any) {
export function AgentTestPanel({ fields, dirty, customSkills = [], onRunLive }) {
const [contextId, setContextId] = React.useState( const [contextId, setContextId] = React.useState(
() => CONTEXT_OPTIONS.find((o) => fields.pages.includes(pageKeyForContext(o.contextId)))?.contextId () => CONTEXT_OPTIONS.find((o) => fields.pages.includes(pageKeyForContext(o.contextId)))?.contextId
|| CONTEXT_OPTIONS[0].contextId || CONTEXT_OPTIONS[0].contextId

View File

@@ -43,7 +43,6 @@ import { SkillDetails } from './SkillDetails';
const OWLIVER = 'owliver'; const OWLIVER = 'owliver';
const BOARD = 'board'; const BOARD = 'board';
/** @param {any} props */
export function AgentSkillWorkspace({ export function AgentSkillWorkspace({
fields, fields,
customSkills = [], customSkills = [],
@@ -55,7 +54,7 @@ export function AgentSkillWorkspace({
busy = false, busy = false,
pendingId = null, pendingId = null,
dirty = false, dirty = false,
}) { }: any) {
const [kind, setKind] = React.useState(OWLIVER); const [kind, setKind] = React.useState(OWLIVER);
const [query, setQuery] = React.useState(''); const [query, setQuery] = React.useState('');
const [boardQuery, setBoardQuery] = React.useState(''); const [boardQuery, setBoardQuery] = React.useState('');

View File

@@ -174,7 +174,6 @@ function StaticLeaf({ icon: Icon, label, detail = null }) {
); );
} }
/** @param {any} props */
export function AgentTree({ export function AgentTree({
agentName, agentName,
agentIcon, agentIcon,
@@ -191,7 +190,7 @@ export function AgentTree({
onToggleBranch, onToggleBranch,
busy = false, busy = false,
className, className,
}) { }: any) {
const Glyph = agentIconFor(agentIcon); const Glyph = agentIconFor(agentIcon);
return ( return (

View File

@@ -159,11 +159,10 @@ function BoardCard({ entry, enabled, onToggle, onSurfaces, busy, attached = fals
); );
} }
/** @param {any} props */
export function BoardSkillList({ export function BoardSkillList({
entries, agentPages = [], disabledIds, onToggle, busy = false, query, onQueryChange, total, entries, agentPages = [], disabledIds, onToggle, busy = false, query, onQueryChange, total,
attachedIds = null, onAttach = null, attachedIds = null, onAttach = null,
}) { }: any) {
const searchId = React.useId(); const searchId = React.useId();
const onSurfaces = React.useMemo(() => new Set(agentPages), [agentPages]); const onSurfaces = React.useMemo(() => new Set(agentPages), [agentPages]);

View File

@@ -25,9 +25,9 @@ import { glyphForGroup } from './glyphs';
*/ */
/** @type {any} */ /** @type {any} */
export const SkillCard = React.memo(/** @param {any} props */ ({ export const SkillCard = React.memo(({
entry, enabled, onOpen, onAddAgent, onTest, canTest = false, busy = false, isPending = false, entry, enabled, onOpen, onAddAgent, onTest, canTest = false, busy = false, isPending = false,
}) => { }: any) => {
const Glyph = glyphForGroup(entry.group); const Glyph = glyphForGroup(entry.group);
const samplePrompt = entry.suggestions?.[0]?.label; const samplePrompt = entry.suggestions?.[0]?.label;

View File

@@ -23,8 +23,7 @@ import { SkillCard } from './SkillCard';
* has, at every width, with no media query to get wrong. * has, at every width, with no media query to get wrong.
*/ */
/** @param {any} props */ function CategoryFilter({ groups, value, onChange, total }: any) {
function CategoryFilter({ groups, value, onChange, total }) {
return ( return (
<div <div
role="group" role="group"
@@ -60,7 +59,6 @@ function CategoryFilter({ groups, value, onChange, total }) {
); );
} }
/** @param {any} props */
export function SkillCatalog({ export function SkillCatalog({
entries, entries,
groups, groups,
@@ -76,7 +74,7 @@ export function SkillCatalog({
testableIds, testableIds,
busy = false, busy = false,
pendingId = null, pendingId = null,
}) { }: any) {
const searchId = React.useId(); const searchId = React.useId();
return ( return (

View File

@@ -82,11 +82,10 @@ function TestState({ test, entryId }) {
* button needs the same `surface` this renders, and two copies of that would be * button needs the same `surface` this renders, and two copies of that would be
* two answers to "what am I about to enable". * two answers to "what am I about to enable".
*/ */
/** @param {any} props */
export function CapabilityBrief({ export function CapabilityBrief({
entry, attached, surface, onSurfaceChange, question, onQuestionChange, entry, attached, surface, onSurfaceChange, question, onQuestionChange,
canTest, target, onTest, test, busy = false, showTest = true, canTest, target, onTest, test, busy = false, showTest = true,
}) { }: any) {
const { shapes, described } = entry.capabilities; const { shapes, described } = entry.capabilities;
return ( return (
@@ -252,11 +251,10 @@ export function CapabilityBrief({
); );
} }
/** @param {any} props */
export function SkillDetails({ export function SkillDetails({
entry, open, onOpenChange, isEnabledOn, onToggle, busy = false, entry, open, onOpenChange, isEnabledOn, onToggle, busy = false,
fields = null, onRunTest = null, test = null, fields = null, onRunTest = null, test = null,
}) { }: any) {
/** /**
* The surface this would run on. * The surface this would run on.
* *

View File

@@ -45,8 +45,7 @@ const OPTIONS = [
export const supportedSurfaces = (type) => export const supportedSurfaces = (type) =>
OPTIONS.filter((o) => o.supported(type)).map((o) => o.id); OPTIONS.filter((o) => o.supported(type)).map((o) => o.id);
/** @param {any} props */ export function SurfaceSelect({ type, value, onChange, name }: any) {
export function SurfaceSelect({ type, value, onChange, name }) {
return ( return (
<div role="radiogroup" aria-label={`Run ${name} as`} className="space-y-1.5"> <div role="radiogroup" aria-label={`Run ${name} as`} className="space-y-1.5">
{OPTIONS.map((option) => { {OPTIONS.map((option) => {

View File

@@ -133,7 +133,7 @@ function ListSection({ data }) {
function TimelineSection({ data }) { function TimelineSection({ data }) {
const items = [...(data.items || [])] const items = [...(data.items || [])]
.filter((item) => item.at) .filter((item) => item.at)
.sort((a, b) => new Date(b.at) - new Date(a.at)); .sort((a, b) => (new Date(b.at) as any) - (new Date(a.at) as any));
if (data.empty || !items.length) return <EmptyNote>{data.emptyNote}</EmptyNote>; if (data.empty || !items.length) return <EmptyNote>{data.emptyNote}</EmptyNote>;
return ( return (
@@ -308,7 +308,7 @@ function WeightsSection({ data, section, onApply, compact = false }) {
if (data?.empty || !rows.length) return <EmptyNote>{data?.emptyNote}</EmptyNote>; if (data?.empty || !rows.length) return <EmptyNote>{data?.emptyNote}</EmptyNote>;
const current = draft || Object.fromEntries(rows.map((r) => [r.id, Number(r.value) || 0])); const current = draft || Object.fromEntries(rows.map((r) => [r.id, Number(r.value) || 0]));
const total = Object.values(current).reduce((a, b) => a + b, 0); const total = Object.values<any>(current).reduce((a, b) => a + b, 0);
const balanced = total === 100; const balanced = total === 100;
const dirty = draft !== null && rows.some((r) => (Number(r.value) || 0) !== current[r.id]); const dirty = draft !== null && rows.some((r) => (Number(r.value) || 0) !== current[r.id]);

View File

@@ -49,7 +49,7 @@ import { agentPermitsSkill } from '@/lib/agents/runtime';
* from one resolution, or the two would disagree about what a page carries. * from one resolution, or the two would disagree about what a page carries.
* Called with no placement it returns every section on the page. * Called with no placement it returns every section on the page.
*/ */
export function useSkillSections(page, placement) { export function useSkillSections(page, placement?) {
const preferences = usePreferences(); const preferences = usePreferences();
/** /**
* Who is answering here. * Who is answering here.
@@ -217,7 +217,6 @@ function SkillSection({ skill, section, context }) {
); );
} }
/** @param {any} props */
export function SkillSurface({ export function SkillSurface({
page = '', page = '',
placement = '', placement = '',
@@ -231,7 +230,7 @@ export function SkillSurface({
_customSources = [], _customSources = [],
_email = '', _email = '',
_onOpenTraining = null, _onOpenTraining = null,
}) { }: any) {
const sections = useSkillSections(page, placement); const sections = useSkillSections(page, placement);
const resolved = useSkillDataContext(context); const resolved = useSkillDataContext(context);