plugins: a plugin's tasks are read where they live
the last piece of docs/plugin-capabilities.md. skills, affordances and notification catalogues already loaded from each installed and enabled plugin's own directory; tasks did not, so the transcribe plugin shipped a task nothing could see — right-clicking an .m4a offered no Run Task at all. it turned out to be one function rather than several. every path that runs a task resolves it through getTaskByDirName and then reads implementation off the record, so the sources go in there and in listTasks and a plugin's task is runnable everywhere at once, identity unchanged: still a directory name, still what pipeline_jobs stores. the record carries `plugin` and that one field does the rest. updateTask and deleteTask throw on it, DELETE /api/tasks/:name answers 409 naming the plugin, and the capability screen shows whose it is instead of an edit and a delete that cannot work. without the delete guard it was quietly a no-op: rm -rf on a path in the owner's store that does not exist, then ok: true, and the task still in the menu. owner's store first, so a plugin cannot take over a name already in use. getTaskByDirName now refuses a name containing a separator. it always joined a caller-supplied string onto a path — from a websocket message and a job row — and searching several roots is the moment that stops being one directory nobody could escape. capabilityRootsFrom is gone; it was dead. capabilitySources replaces it and carries the provenance, with capabilityRoots the thin map over it that the loaders that only want paths still use. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
@@ -24,6 +24,8 @@ export type CapabilityDetail = CapabilitySummary & {
|
||||
rawFrontmatter: string;
|
||||
filePath: string;
|
||||
chatSessionId: string | null;
|
||||
/** Only tasks carry this today: the plugin that ships it, or null/absent when it is the owner's own. */
|
||||
plugin?: string | null;
|
||||
};
|
||||
|
||||
type CapabilityListProps = {
|
||||
@@ -538,7 +540,17 @@ export const CapabilityPage = ({ kind, endpoint, queryKey, basePath }: Capabilit
|
||||
<span className="text-sm font-medium text-duck-dark/70 flex-1">
|
||||
{detail?.name ?? t('capability.selectOne', { kind: kind.toLowerCase() })}
|
||||
</span>
|
||||
{detail && (
|
||||
{/*
|
||||
A plugin's capability is read where it lives, in a git checkout that `plugins:update`
|
||||
pulls. So there is nothing to offer here: an edit would be overwritten by the next update,
|
||||
and the delete refuses server-side (409). Say whose it is instead of showing two controls
|
||||
that cannot work.
|
||||
*/}
|
||||
{detail?.plugin ? (
|
||||
<span className="text-xs text-duck-dark/40">
|
||||
{t('capability.fromPlugin', { plugin: detail.plugin })}
|
||||
</span>
|
||||
) : detail ? (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setEditing(!editing)}
|
||||
@@ -553,7 +565,7 @@ export const CapabilityPage = ({ kind, endpoint, queryKey, basePath }: Capabilit
|
||||
<Trash2 className="h-3.5 w-3.5 text-duck-dark/50 hover:text-red-500" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
<div className="overflow-y-auto flex-1 p-6">
|
||||
{detail?.rawFrontmatter && <FrontmatterBlock yaml={detail.rawFrontmatter} />}
|
||||
|
||||
Reference in New Issue
Block a user