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
* 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 (
<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">
@@ -64,8 +64,7 @@ export function Workspace({ rail, children, className }) {
* height without measuring it in JavaScript. Content stays mounted, which is
* what keeps a section addressable by the rail while it is closed.
*/
/** @param {any} props */
export function Collapse({ open, children, className }) {
export function Collapse({ open, children, className }: any) {
return (
<div
className={cn(
@@ -100,8 +99,7 @@ export function Collapse({ open, children, className }) {
* Provides clear section navigation and leaf anchor shortcuts across the
* configuration workspace.
*/
/** @param {any} props */
export function Rail({ sections, activeId, onSelect }) {
export function Rail({ sections, activeId, onSelect }: any) {
const activeSection = sections.find((s) => s.id === activeId);
return (
@@ -181,11 +179,10 @@ export function Rail({ sections, activeId, onSelect }) {
/**
* One section of the document.
*/
/** @param {any} props */
export function DocSection({
id, icon: Icon, title, description, meta, open, onToggle, emphasis = false, last = false,
children,
}) {
}: any) {
return (
<section
id={id}
@@ -257,8 +254,7 @@ export function DocSection({
}
/** A labelled field in the document body. */
/** @param {any} props */
export function DocField({ id, label, hint, children, className }) {
export function DocField({ id, label, hint, children, className }: any) {
const reactId = React.useId();
const controlId = id ? `${id}-control` : reactId;
@@ -270,7 +266,7 @@ export function DocField({ id, label, hint, children, className }) {
>
{label}
</label>
{React.isValidElement(children)
{React.isValidElement<any>(children)
? React.cloneElement(children, { id: children.props.id || controlId })
: children}
{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
* exactly the level of the thing it names.
*/
/** @param {any} props */
export function GroupHead({ id, icon: Icon, title, count, action, className }) {
export function GroupHead({ id, icon: Icon, title, count, action, className }: any) {
return (
<div
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,
* which is what they are.
*/
/** @param {any} props */
export function ItemRow({ icon: Icon, title, detail, warning, onRemove, removeLabel }) {
export function ItemRow({ icon: Icon, title, detail, warning, onRemove, removeLabel }: any) {
return (
<li
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
* space gets a chevron and opens underneath.
*/
/** @param {any} props */
export function SettingRow({
id, icon: Icon, title, value, description, control, children, open, onToggle, last = false,
}) {
}: any) {
const expandable = Boolean(children);
const body = (
@@ -412,8 +405,7 @@ export function SettingRow({
}
/** A page an agent covers, or one it could. */
/** @param {any} props */
export function ScopeChip({ active, children, ...props }) {
export function ScopeChip({ active, children, ...props }: any) {
return (
<button
type="button"
@@ -443,8 +435,7 @@ export function ScopeChip({ active, children, ...props }) {
/* ── Identity ────────────────────────────────────────────────────────────── */
/** The icons an agent may wear, as a picker. */
/** @param {any} props */
export function IconPicker({ icons, value, onChange }) {
export function IconPicker({ icons, value, onChange }: any) {
return (
<div className="grid grid-cols-5 gap-1.5">
{icons.map((icon) => {
@@ -486,8 +477,7 @@ export function IconPicker({ icons, value, onChange }) {
* same three things the switcher renders — icon, name, description — and
* nothing it does not.
*/
/** @param {any} props */
export function AgentPreview({ name, description, icon }) {
export function AgentPreview({ name, description, icon }: any) {
const Icon = agentIconFor(icon);
return (

View File

@@ -94,7 +94,6 @@ function useActiveSection(ids) {
return [active, setActive];
}
/** @param {any} props */
export function AgentConfigure({
fields, agents, customSkills = [], onChange, onOpenSkills, scopeLocked = false,
/* 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
rather than fetched here so this component stays a form over `fields`. */
toolCatalogue = [],
}) {
}: any) {
/* 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
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.
*/
/** @param {any} props */
function MetricCard({ icon: Icon, label, value, detail, highlight }) {
function MetricCard({ icon: Icon, label, value, detail, highlight }: any) {
return (
<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">
@@ -45,8 +44,7 @@ function MetricCard({ icon: Icon, label, value, detail, highlight }) {
);
}
/** @param {any} props */
export function AgentInsightsPanel({ agentId, agentName }) {
export function AgentInsightsPanel({ agentId, agentName }: any) {
const preferences = usePreferences();
const [records] = React.useState(() => readHistory());

View File

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

View File

@@ -43,7 +43,6 @@ import { SkillDetails } from './SkillDetails';
const OWLIVER = 'owliver';
const BOARD = 'board';
/** @param {any} props */
export function AgentSkillWorkspace({
fields,
customSkills = [],
@@ -55,7 +54,7 @@ export function AgentSkillWorkspace({
busy = false,
pendingId = null,
dirty = false,
}) {
}: any) {
const [kind, setKind] = React.useState(OWLIVER);
const [query, setQuery] = 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({
agentName,
agentIcon,
@@ -191,7 +190,7 @@ export function AgentTree({
onToggleBranch,
busy = false,
className,
}) {
}: any) {
const Glyph = agentIconFor(agentIcon);
return (

View File

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

View File

@@ -25,9 +25,9 @@ import { glyphForGroup } from './glyphs';
*/
/** @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,
}) => {
}: any) => {
const Glyph = glyphForGroup(entry.group);
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.
*/
/** @param {any} props */
function CategoryFilter({ groups, value, onChange, total }) {
function CategoryFilter({ groups, value, onChange, total }: any) {
return (
<div
role="group"
@@ -60,7 +59,6 @@ function CategoryFilter({ groups, value, onChange, total }) {
);
}
/** @param {any} props */
export function SkillCatalog({
entries,
groups,
@@ -76,7 +74,7 @@ export function SkillCatalog({
testableIds,
busy = false,
pendingId = null,
}) {
}: any) {
const searchId = React.useId();
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
* two answers to "what am I about to enable".
*/
/** @param {any} props */
export function CapabilityBrief({
entry, attached, surface, onSurfaceChange, question, onQuestionChange,
canTest, target, onTest, test, busy = false, showTest = true,
}) {
}: any) {
const { shapes, described } = entry.capabilities;
return (
@@ -252,11 +251,10 @@ export function CapabilityBrief({
);
}
/** @param {any} props */
export function SkillDetails({
entry, open, onOpenChange, isEnabledOn, onToggle, busy = false,
fields = null, onRunTest = null, test = null,
}) {
}: any) {
/**
* The surface this would run on.
*

View File

@@ -45,8 +45,7 @@ const OPTIONS = [
export const supportedSurfaces = (type) =>
OPTIONS.filter((o) => o.supported(type)).map((o) => o.id);
/** @param {any} props */
export function SurfaceSelect({ type, value, onChange, name }) {
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) => {

View File

@@ -133,7 +133,7 @@ function ListSection({ data }) {
function TimelineSection({ data }) {
const items = [...(data.items || [])]
.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>;
return (
@@ -308,7 +308,7 @@ function WeightsSection({ data, section, onApply, compact = false }) {
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 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 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.
* Called with no placement it returns every section on the page.
*/
export function useSkillSections(page, placement) {
export function useSkillSections(page, placement?) {
const preferences = usePreferences();
/**
* Who is answering here.
@@ -217,7 +217,6 @@ function SkillSection({ skill, section, context }) {
);
}
/** @param {any} props */
export function SkillSurface({
page = '',
placement = '',
@@ -231,7 +230,7 @@ export function SkillSurface({
_customSources = [],
_email = '',
_onOpenTraining = null,
}) {
}: any) {
const sections = useSkillSections(page, placement);
const resolved = useSkillDataContext(context);