Files
zeekay 1b367e2b62 chore: rebase the tree onto upstream master
The tree was a debranded snapshot of upstream taken around 2026-02-20, and
upstream has moved about five months since. This replaces it with upstream
master, mechanically re-derived: bin/debrand does the rename, and the
pipeline and squashed migrations are restored from the shipped tree.

Not a merge. The previous import replaced the tree wholesale in a single
commit, so there is no shared content history to merge against. Reproducing
the rename on the new tree is what actually converges.

What survives: .hanzo/workflows, the Dockerfiles and bin/docker* entrypoints,
compose files, LLM.md, and insights/migrations. The squashed initial
migration is load-bearing — the live database records it by name, so
upstream's ~918 individual migrations cannot come back.

Nine products are dropped as unreferenced. Twenty-one more that we do not run
are kept anyway because the core app still imports them; unpicking those is a
refactor and does not belong in a rebase.

This base does not build yet, and the reasons are not in the rename:
upstream now wants Python 3.13.13 against our 3.12.12 and Django 5.2 against
our 4.2, and the dependency manifests still name upstream packages that we
republish under our own names. Those are decisions, not renames.
2026-08-01 15:44:40 -07:00

619 lines
26 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useActions, useValues } from 'kea'
import { combineUrl, router } from 'kea-router'
import { useMemo, useRef } from 'react'
import { IconDownload, IconPlusSmall, IconUpload } from '@hanzo/icons'
import { Divider, Modal, Switch, Tabs, Tag, Link } from '@hanzo/elements'
import { AccessControlAction } from 'lib/components/AccessControlAction'
import { CodeSnippet, Language } from 'lib/components/CodeSnippet/CodeSnippet'
import { MemberSelect } from 'lib/components/MemberSelect'
import { dayjs } from 'lib/dayjs'
import { Banner } from 'lib/elements/Banner'
import { Button } from 'lib/elements/Button'
import { More } from 'lib/elements/Button/More'
import { Collapse } from 'lib/elements/Collapse'
import { ProfilePicture } from 'lib/elements/ProfilePicture'
import { Spinner } from 'lib/elements/Spinner'
import { SceneExport } from 'scenes/sceneTypes'
import { urls } from 'scenes/urls'
import { SceneContent } from '~/layout/scenes/components/SceneContent'
import { SceneTitleSection } from '~/layout/scenes/components/SceneTitleSection'
import { Dialog } from '~/lib/elements/Dialog'
import { Field } from '~/lib/elements/Field'
import { Input } from '~/lib/elements/Input'
import { Table, TableColumn, TableColumns } from '~/lib/elements/Table'
import { atColumn } from '~/lib/elements/Table/columnUtils'
import { ProductKey } from '~/queries/schema/schema-general'
import { AccessControlLevel, AccessControlResourceType } from '~/types'
import type { LLMSkillListApi } from 'products/skills/frontend/generated/api.schemas'
import {
DEFAULT_SKILLS_TAB_KEY,
SKILLS_GROUP_LIMIT,
SKILLS_PER_PAGE,
SkillGroupNode,
SkillGroupTree,
llmSkillsLogic,
skillTabUrl,
} from './llmSkillsLogic'
import { SKILL_NAME_MAX_LENGTH, validateSkillName } from './skillConstants'
import { openArchiveSkillDialog } from './skillSceneComponents'
export const scene: SceneExport = {
component: LLMSkillsScene,
logic: llmSkillsLogic,
productKey: ProductKey.AI_OBSERVABILITY,
}
// Mirrors `metadata.seeded_by` stamped by the Signals scout harness (kept local so the skills
// product needn't depend on the signals product). Its presence marks a canonical scout.
const HARNESS_SEEDED_BY = 'signals_scout_harness'
function isCanonicalScout(skill: LLMSkillListApi): boolean {
return (skill.metadata as Record<string, unknown> | undefined)?.seeded_by === HARNESS_SEEDED_BY
}
function buildSkillColumns(
skillUrl: (name: string) => string,
duplicateSkill: (name: string, newName: string) => void,
deleteSkill: (name: string) => void,
downloadSkillZip: (name: string) => void,
options?: { showScoutOrigin?: boolean }
): TableColumns<LLMSkillListApi> {
return [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
width: '20%',
render: function renderName(_, skill) {
return (
<Link to={skillUrl(skill.name)} className="font-semibold" data-attr="llma-skill-name-link">
{skill.name}
</Link>
)
},
},
...(options?.showScoutOrigin
? ([
{
title: 'Origin',
key: 'origin',
width: 110,
render: function renderOrigin(_, skill) {
const canonical = isCanonicalScout(skill)
return (
<Tag type={canonical ? 'completion' : 'default'}>
{canonical ? 'Canonical' : 'Custom'}
</Tag>
)
},
},
] as TableColumns<LLMSkillListApi>)
: []),
{
title: 'Description',
dataIndex: 'description',
key: 'description',
width: '35%',
render: function renderDescription(description) {
const text = typeof description === 'string' ? description : ''
const truncated = text.length > 100 ? text.slice(0, 100) + '...' : text
return <span className="text-muted text-sm">{truncated || <i>-</i>}</span>
},
},
{
title: 'Latest author',
dataIndex: 'created_by',
render: function renderCreatedBy(_, item) {
const { created_by } = item
return (
<div className="flex flex-row items-center flex-nowrap">
{created_by && <ProfilePicture user={created_by as any} size="md" showName />}
</div>
)
},
},
{
title: 'Versions',
dataIndex: 'version_count',
key: 'version_count',
width: 100,
render: function renderVersionCount(_, skill) {
return <span className="text-muted-alt">{skill.version_count}</span>
},
},
atColumn('created_at', 'Latest version created') as TableColumn<
LLMSkillListApi,
keyof LLMSkillListApi | undefined
>,
{
width: 0,
render: function renderMore(_, skill) {
return (
<More
overlay={
<>
<Button to={skillUrl(skill.name)} data-attr="llma-skill-dropdown-view" fullWidth>
View
</Button>
<Button
icon={<IconDownload />}
onClick={() => downloadSkillZip(skill.name)}
data-attr="llma-skill-dropdown-download"
fullWidth
>
Download .zip
</Button>
<AccessControlAction
resourceType={AccessControlResourceType.LlmSkill}
minAccessLevel={AccessControlLevel.Editor}
>
<Button
onClick={() => {
Dialog.openForm({
title: 'Duplicate skill',
initialValues: {
newName: `${skill.name}-copy`,
},
content: (
<Field name="newName" label="New skill name">
<Input
data-attr="llma-skill-duplicate-name"
placeholder="my-skill-copy"
maxLength={SKILL_NAME_MAX_LENGTH}
autoFocus
/>
</Field>
),
errors: {
newName: (name: string) => validateSkillName(name),
},
onSubmit: async ({ newName }) => {
duplicateSkill(skill.name, newName)
},
})
}}
data-attr="llma-skill-dropdown-duplicate"
fullWidth
>
Duplicate
</Button>
</AccessControlAction>
<AccessControlAction
resourceType={AccessControlResourceType.LlmSkill}
minAccessLevel={AccessControlLevel.Editor}
>
<Button
status="danger"
onClick={() => openArchiveSkillDialog(() => deleteSkill(skill.name))}
data-attr="llma-skill-dropdown-delete"
fullWidth
>
Archive
</Button>
</AccessControlAction>
</>
}
/>
)
},
},
]
}
function GroupHeader({ node }: { node: SkillGroupNode }): JSX.Element {
return (
<div className="flex items-center gap-2">
<span className="font-mono font-semibold">{node.prefix}</span>
<span className="text-muted-alt text-xs">
{node.count} skill{node.count === 1 ? '' : 's'}
</span>
</div>
)
}
function SkillLeafTable({
skills,
columns,
}: {
skills: LLMSkillListApi[]
columns: TableColumns<LLMSkillListApi>
}): JSX.Element {
return (
<Table
columns={columns}
dataSource={skills}
rowKey="id"
showHeader={false}
embedded
size="small"
nouns={['skill', 'skills']}
/>
)
}
function SkillGroupPanels({
groups,
columns,
}: {
groups: SkillGroupNode[]
columns: TableColumns<LLMSkillListApi>
}): JSX.Element {
return (
<Collapse
multiple
embedded
size="small"
panels={groups.map((node) => ({
key: node.prefix,
header: <GroupHeader node={node} />,
dataAttr: `llma-skill-group-${node.prefix}`,
content: (
<div className="flex flex-col gap-2">
{node.children.length > 0 && <SkillGroupPanels groups={node.children} columns={columns} />}
{node.leaves.length > 0 && <SkillLeafTable skills={node.leaves} columns={columns} />}
</div>
),
}))}
/>
)
}
function GroupedSkillsView({
tree,
columns,
}: {
tree: SkillGroupTree
columns: TableColumns<LLMSkillListApi>
}): JSX.Element {
const hasGroups = tree.groups.length > 0
const hasUngrouped = tree.ungrouped.length > 0
if (!hasGroups && !hasUngrouped) {
return (
<div className="text-muted-alt text-sm p-4 text-center border rounded">
No skills match the current filters.
</div>
)
}
return (
<div className="flex flex-col gap-3">
{hasGroups && <SkillGroupPanels groups={tree.groups} columns={columns} />}
{hasUngrouped && (
<div className="flex flex-col gap-1" data-attr="llma-skill-group-ungrouped">
<div className="flex items-center gap-2 px-2">
<span className="font-semibold">Ungrouped</span>
<span className="text-muted-alt text-xs">
{tree.ungrouped.length} skill{tree.ungrouped.length === 1 ? '' : 's'}
</span>
</div>
<SkillLeafTable skills={tree.ungrouped} columns={columns} />
</div>
)}
</div>
)
}
function MarketplaceCredentialSection(): JSX.Element {
const { marketplaceCommand, codexCommand, marketplaceState, marketplaceLoading, issuingCredential } =
useValues(llmSkillsLogic)
const { issueMarketplaceCommand } = useActions(llmSkillsLogic)
if (marketplaceLoading && !marketplaceState) {
return (
<div className="flex items-center gap-2 text-sm text-secondary">
<Spinner /> Checking your skill store connection…
</div>
)
}
const justIssued = !!marketplaceState?.token
const alreadyConnected = !!marketplaceState?.connected && !justIssued
return (
<>
{justIssued ? (
<Banner type="warning" className="text-sm">
This token is shown <b>once</b>. It is <b>read-only</b> and scoped to skills only (
<code>llm_skill:read</code>) — it can't touch anything else. It's tied to your account and
automatically stops working if you lose access to this project. Manage or revoke it anytime in{' '}
<Link to={urls.settings('user-api-keys')}>Settings → Personal API keys</Link>.
</Banner>
) : alreadyConnected ? (
<Banner type="info" className="text-sm">
You already have a skill store credential
{marketplaceState?.mask_value ? (
<>
{' '}
(<code>{marketplaceState.mask_value}</code>)
</>
) : null}
{marketplaceState?.created_at ? <> from {dayjs(marketplaceState.created_at).fromNow()}</> : null}.
Existing setups keep working — the token can't be shown again. Setting up a new machine? Generate an
install command below.
</Banner>
) : (
<p className="m-0 text-sm text-secondary">
We'll mint a dedicated <b>read-only</b> credential (scope <code>llm_skill:read</code> only — it can
read this project's skills and nothing else), just for you, and drop it straight into a
ready-to-paste command.
</p>
)}
{!justIssued && (
<div>
<AccessControlAction
resourceType={AccessControlResourceType.LlmSkill}
minAccessLevel={AccessControlLevel.Editor}
>
<Button
type="primary"
onClick={() => issueMarketplaceCommand(alreadyConnected)}
loading={issuingCredential}
data-attr="generate-marketplace-credential-button"
>
{alreadyConnected
? 'Generate an install command (replaces your old token)'
: 'Generate read-only credential & command'}
</Button>
</AccessControlAction>
</div>
)}
<div className="flex flex-col gap-1">
<p className="m-0 text-xs font-semibold text-secondary">Claude Code</p>
<CodeSnippet language={Language.Bash} thing="Claude Code command">
{marketplaceCommand}
</CodeSnippet>
<p className="m-0 text-xs text-secondary">
Run each line in Claude Code — skills then appear as <code>/insights-skill-store:&lt;name&gt;</code>,
auto-updating as you publish.
</p>
</div>
<div className="flex flex-col gap-1">
<p className="m-0 text-xs font-semibold text-secondary">Codex</p>
<CodeSnippet language={Language.Bash} thing="Codex command">
{codexCommand}
</CodeSnippet>
<p className="m-0 text-xs text-secondary">
Run both lines in your terminal — the same marketplace, added to Codex and installed. Codex loads
the skills automatically.
</p>
</div>
</>
)
}
function ConnectToClaudeCodeModal(): JSX.Element {
const { connectModalOpen } = useValues(llmSkillsLogic)
const { setConnectModalOpen } = useActions(llmSkillsLogic)
return (
<Modal
isOpen={connectModalOpen}
onClose={() => setConnectModalOpen(false)}
title="Connect a coding agent"
description="Install your team's skills into Claude Code or Codex with automatic updates – or let any MCP-connected agent load them directly."
width={640}
>
<div className="flex flex-col gap-4">
<section className="flex flex-col gap-2">
<h4 className="m-0 font-semibold">Plugin marketplace (Claude Code &amp; Codex)</h4>
<MarketplaceCredentialSection />
</section>
<Divider />
<section className="flex flex-col gap-2">
<h4 className="m-0 font-semibold">Any agent (Insights MCP)</h4>
<p className="m-0 text-sm text-secondary">
If the Insights MCP is connected, an agent can list and load these skills directly via its{' '}
<code>skill-*</code> tools — no marketplace needed. Just ask:
</p>
<CodeSnippet language={Language.Text} thing="agent prompt">
List my Insights skills, then load the one that fits this task.
</CodeSnippet>
</section>
</div>
</Modal>
)
}
export function LLMSkillsScene(): JSX.Element {
const { setFilters, deleteSkill, duplicateSkill, downloadSkillZip, importSkill, setConnectModalOpen } =
useActions(llmSkillsLogic)
const {
skills,
skillsLoading,
sorting,
pagination,
filters,
skillCountLabel,
groupedSkills,
importing,
activeTabKey,
activeCategory,
activeTabDescription,
visibleCategoryTabs,
} = useValues(llmSkillsLogic)
const { searchParams } = useValues(router)
const skillUrl = (name: string): string => combineUrl(urls.skill(name), searchParams).url
const fileInputRef = useRef<HTMLInputElement | null>(null)
const showScoutOrigin = activeCategory === 'scout'
// Memoize columns so the array reference doesn't change every render — otherwise every
// nested Table inside the grouped tree reconciles on each parent re-render.
const columns = useMemo(
() => buildSkillColumns(skillUrl, duplicateSkill, deleteSkill, downloadSkillZip, { showScoutOrigin }),
// eslint-disable-next-line react-hooks/exhaustive-deps
[searchParams, duplicateSkill, deleteSkill, downloadSkillZip, showScoutOrigin]
)
const showGroupedView = filters.group_by_prefix && groupedSkills && !skillsLoading
const showGroupedLoadingSkeleton = filters.group_by_prefix && skillsLoading
const truncated = filters.group_by_prefix && skills.count > skills.results.length
return (
<SceneContent>
<input
ref={fileInputRef}
type="file"
accept=".zip,application/zip"
className="hidden"
data-attr="import-skill-file-input"
onChange={(e) => {
const file = e.currentTarget.files?.[0]
if (file) {
importSkill(file)
}
// Reset so selecting the same file again re-triggers onChange.
e.currentTarget.value = ''
}}
/>
<ConnectToClaudeCodeModal />
<SceneTitleSection
name="Skills"
description={activeTabDescription}
resourceType={{ type: 'llm_analytics' }}
actions={
<>
<Button
type="secondary"
size="small"
onClick={() => setConnectModalOpen(true)}
data-attr="connect-coding-agent-button"
>
Load skills in your agent
</Button>
<AccessControlAction
resourceType={AccessControlResourceType.LlmSkill}
minAccessLevel={AccessControlLevel.Editor}
>
<Button
type="secondary"
size="small"
icon={<IconUpload />}
onClick={() => fileInputRef.current?.click()}
loading={importing}
data-attr="import-skill-button"
tooltip="Import a skill from a spec-compliant .zip"
>
Import
</Button>
</AccessControlAction>
<AccessControlAction
resourceType={AccessControlResourceType.LlmSkill}
minAccessLevel={AccessControlLevel.Editor}
>
<Button
type="primary"
size="small"
to={skillUrl('new')}
icon={<IconPlusSmall />}
data-attr="new-skill-button"
>
New skill
</Button>
</AccessControlAction>
</>
}
/>
{visibleCategoryTabs.length > 0 && (
<Tabs
activeKey={activeTabKey}
onChange={(key) => router.actions.push(skillTabUrl(key))}
tabs={[
{ key: DEFAULT_SKILLS_TAB_KEY, label: 'Skills' },
...visibleCategoryTabs.map((tab) => ({ key: tab.key, label: tab.label })),
]}
/>
)}
<div className="space-y-4">
<div className="flex gap-x-4 gap-y-2 items-center flex-wrap">
<Input
type="search"
placeholder="Search skills..."
value={filters.search}
data-attr="skills-search-input"
onChange={(value) => setFilters({ search: value })}
className="max-w-md"
/>
<Switch
label="Group by prefix"
checked={filters.group_by_prefix}
onChange={(checked) => setFilters({ group_by_prefix: checked })}
bordered
size="small"
data-attr="skills-group-by-prefix-toggle"
/>
<div className="text-muted-alt">{skillCountLabel}</div>
<div className="flex-1" />
<span>
<b>Created by</b>
</span>
<MemberSelect
defaultLabel="Any user"
value={filters.created_by_id ?? null}
size="xsmall"
onChange={(user) => setFilters({ created_by_id: user?.id, page: 1 })}
/>
</div>
{filters.group_by_prefix ? (
<>
{truncated && (
<Banner type="warning">
Showing the first {skills.results.length} of {skills.count} skills. The grouped view is
capped at {SKILLS_GROUP_LIMIT} skills — use search or turn off grouping to see the rest.
</Banner>
)}
{showGroupedLoadingSkeleton ? (
<Table
loading
columns={columns}
dataSource={[]}
rowKey="id"
loadingSkeletonRows={SKILLS_PER_PAGE}
nouns={['skill', 'skills']}
/>
) : (
showGroupedView && <GroupedSkillsView tree={groupedSkills!} columns={columns} />
)}
</>
) : (
<Table
loading={skillsLoading}
columns={columns}
// Drop the cached results while a reload is in flight if they exceed the
// expected page size — otherwise toggling out of grouped mode flashes the
// full 500-item payload through the controlled-paginated flat table.
dataSource={skillsLoading && skills.results.length > SKILLS_PER_PAGE ? [] : skills.results}
pagination={pagination}
noSortingCancellation
sorting={sorting}
onSort={(newSorting) =>
setFilters({
order_by: newSorting
? `${newSorting.order === -1 ? '-' : ''}${newSorting.columnKey}`
: undefined,
})
}
rowKey="id"
loadingSkeletonRows={SKILLS_PER_PAGE}
nouns={['skill', 'skills']}
/>
)}
</div>
</SceneContent>
)
}