Files
paperclip/ui/src/components/ManagedRoutinesList.tsx
T
Dotta 563413ecd4 Fix LLM wiki type contracts (#5758)
## Thinking Path

> - Paperclip is the control plane for autonomous AI companies, and
plugins extend that control plane without bloating core.
> - The LLM Wiki plugin adds a knowledge surface through the plugin
runtime and shared plugin UI components.
> - After the LLM Wiki work merged to `master`, CI exposed TypeScript
contract drift between plugin code, SDK component types, and update
settings types.
> - The ingestion settings update path intentionally accepts partial
source toggles, but its type intersected with the full settings shape
and required every source key.
> - The LLM Wiki UI also passes managed routine default-drift metadata
through the shared routine list item shape, but that metadata was
missing from the public item type.
> - This pull request narrows those type contracts to match the existing
runtime behavior.
> - The benefit is restoring typecheck on `master` with a small,
non-behavioral follow-up.

## What Changed

- Added a `WikiEventIngestionSettingsUpdate` type that permits partial
source updates without weakening normalized stored settings.
- Added managed routine default-drift metadata to the plugin SDK
`ManagedRoutinesListItem` type.
- Mirrored that managed routine default-drift type in the host UI
component item type.

## Verification

- `pnpm --filter @paperclipai/plugin-llm-wiki typecheck`
- `pnpm --filter @paperclipai/plugin-sdk typecheck`
- `pnpm --filter @paperclipai/ui typecheck`
- `git diff --check`

## Risks

- Low risk. This is a TypeScript type-contract fix only; no runtime
behavior or database schema changes.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- OpenAI Codex, GPT-5-based coding agent, tool-enabled local repository
editing and command execution.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

Notes on checklist applicability: no screenshots are included because
the UI change is a shared type-only contract update with no visual
behavior change; no docs were required because no behavior or commands
changed.

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-05-11 21:07:06 -05:00

188 lines
6.3 KiB
TypeScript

import { Button } from "@/components/ui/button";
import {
RoutineListRow,
type RoutineListAgentSummary,
type RoutineListProjectSummary,
type RoutineListRowItem,
} from "@/components/RoutineList";
export type ManagedRoutinesListAgent = {
id: string;
name: string;
icon?: string | null;
};
export type ManagedRoutinesListProject = {
id: string;
name: string;
color?: string | null;
};
export type ManagedRoutineMissingRef = {
resourceKind: string;
resourceKey: string;
};
export type ManagedRoutineDefaultDrift = {
changedFields: string[];
defaultTitle?: string | null;
defaultDescription?: string | null;
};
export type ManagedRoutinesListItem = {
key: string;
title: string;
status: string;
routineId?: string | null;
href?: string | null;
resourceKey?: string | null;
projectId?: string | null;
assigneeAgentId?: string | null;
cronExpression?: string | null;
lastRunAt?: Date | string | null;
lastRunStatus?: string | null;
managedByPluginDisplayName?: string | null;
missingRefs?: ManagedRoutineMissingRef[];
defaultDrift?: ManagedRoutineDefaultDrift | null;
};
export type ManagedRoutinesListProps = {
routines: ManagedRoutinesListItem[];
agents?: ManagedRoutinesListAgent[];
projects?: ManagedRoutinesListProject[];
pluginDisplayName?: string | null;
emptyMessage?: string;
runningRoutineKey?: string | null;
statusMutationRoutineKey?: string | null;
reconcilingRoutineKey?: string | null;
resettingRoutineKey?: string | null;
onRunNow?: (routine: ManagedRoutinesListItem) => void;
onToggleEnabled?: (routine: ManagedRoutinesListItem, enabled: boolean) => void;
onReconcile?: (routine: ManagedRoutinesListItem) => void;
onReset?: (routine: ManagedRoutinesListItem) => void;
};
function managedRoutineToRow(routine: ManagedRoutinesListItem): RoutineListRowItem {
return {
id: routine.key,
title: routine.title,
status: routine.status,
projectId: routine.projectId ?? null,
assigneeAgentId: routine.assigneeAgentId ?? null,
lastRun: routine.lastRunAt || routine.lastRunStatus
? {
triggeredAt: routine.lastRunAt ?? null,
status: routine.lastRunStatus ?? null,
}
: null,
};
}
export function ManagedRoutinesList({
routines,
agents = [],
projects = [],
pluginDisplayName = null,
emptyMessage = "No managed routines.",
runningRoutineKey = null,
statusMutationRoutineKey = null,
reconcilingRoutineKey = null,
resettingRoutineKey = null,
onRunNow,
onToggleEnabled,
onReconcile,
onReset,
}: ManagedRoutinesListProps) {
const agentById = new Map<string, RoutineListAgentSummary>(
agents.map((agent) => [agent.id, { name: agent.name, icon: agent.icon }]),
);
const projectById = new Map<string, RoutineListProjectSummary>(
projects.map((project) => [project.id, { name: project.name, color: project.color }]),
);
if (routines.length === 0) {
return (
<div className="rounded-lg border border-border px-3 py-8 text-center text-sm text-muted-foreground">
{emptyMessage}
</div>
);
}
return (
<div className="rounded-lg border border-border">
{routines.map((routine) => {
const row = managedRoutineToRow(routine);
const href = routine.href ?? (routine.routineId ? `/routines/${routine.routineId}` : "/routines");
const missingRefs = routine.missingRefs ?? [];
const canUseRoutine = Boolean(routine.routineId && routine.resourceKey && missingRefs.length === 0);
const managedBy = routine.managedByPluginDisplayName ?? pluginDisplayName;
const hasRepairActions = Boolean(onReconcile || onReset);
return (
<div key={routine.key} className="last:[&_a]:border-b-0">
<RoutineListRow
routine={row}
projectById={projectById}
agentById={agentById}
runningRoutineId={runningRoutineKey}
statusMutationRoutineId={statusMutationRoutineKey}
href={href}
configureLabel="Configure"
managedByLabel={managedBy ? `Managed by ${managedBy}` : null}
runNowButton
hideArchiveAction
disableRunNow={!canUseRoutine}
disableToggle={!canUseRoutine}
secondaryDetails={
<span className="flex flex-wrap items-center gap-x-3 gap-y-1">
{routine.resourceKey ? <span>{routine.resourceKey}</span> : null}
{routine.cronExpression ? <span>Schedule {routine.cronExpression}</span> : null}
</span>
}
onRunNow={() => onRunNow?.(routine)}
onToggleEnabled={() => onToggleEnabled?.(routine, row.status === "active")}
/>
{hasRepairActions ? (
<div
className="flex flex-wrap items-center justify-between gap-2 border-b border-border px-3 pb-3 text-xs text-muted-foreground last:border-b-0"
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
}}
>
<span>
{missingRefs.length
? `Missing ${missingRefs.map((ref) => `${ref.resourceKind}:${ref.resourceKey}`).join(", ")}`
: "Routine defaults can be repaired."}
</span>
<span className="flex items-center gap-2">
{onReconcile ? (
<Button
size="sm"
variant="ghost"
disabled={reconcilingRoutineKey === routine.key}
onClick={() => onReconcile(routine)}
>
{reconcilingRoutineKey === routine.key ? "Reconciling..." : "Reconcile"}
</Button>
) : null}
{onReset ? (
<Button
size="sm"
variant="ghost"
disabled={resettingRoutineKey === routine.key}
onClick={() => onReset(routine)}
>
{resettingRoutineKey === routine.key ? "Resetting..." : "Reset"}
</Button>
) : null}
</span>
</div>
) : null}
</div>
);
})}
</div>
);
}