hermes-agent/apps/desktop/src/plugins/kanban/plugin.tsx
Brooklyn Nicholson 346149c4f8 feat(kanban): task effort estimate via the auxiliary model
An "Estimate" action asks the auto-routed auxiliary model for a rough token
count + complexity band (S/M/L) with a one-line rationale — tokens, not
dollars, since providers don't report cost reliably. POST /estimate (typed
title/body, for the create dialog) and POST /tasks/{id}/estimate (existing
cards) share one core. Desktop renders it inline ("~15k tok · Medium") with a
"makes a model call" disclaimer; SDK exports compactNumber.
2026-07-30 07:18:08 -05:00

115 lines
3.1 KiB
TypeScript

/**
* Kanban — the founding plugin use case, now pure SDK-consumer work: a
* first-class `/kanban` board page + sidebar nav row + a live statusbar count,
* all reusing the existing `plugins/kanban/dashboard/plugin_api.py` REST router
* through `ctx.rest` (namespace-scoped to `/api/plugins/kanban`). No new
* backend, no core edits.
*
* Ships OFF by default (`defaultEnabled: false`): it inventories in
* Settings ▸ Plugins and registers nothing until the user flips the switch.
*/
import './kanban.css'
import {
cn,
Codicon,
type HermesPlugin,
host,
PALETTE_AREA,
type PaletteContribution,
type RouteContribution,
ROUTES_AREA,
SIDEBAR_NAV_AREA,
type SidebarNavContribution,
STATUSBAR_AREAS,
Tip,
useQuery,
useValue
} from '@hermes/plugin-sdk'
import { $boardSlug, bindApi, boardKey, fetchBoard } from './api'
import { KanbanBoardPage } from './board'
// Live "N running / ready" pill — one glance at fleet activity from anywhere,
// clicks through to the board. Shares the board query (one cache, one poll with
// the page); hidden when nothing is in flight (or unloaded).
function KanbanCount() {
const slug = useValue($boardSlug)
// Socket-invalidated like the page (same cache); slow socketless heartbeat.
const { data: board } = useQuery({
queryFn: () => fetchBoard(false),
queryKey: boardKey(slug, false),
refetchInterval: 60_000
})
if (!board) {
return null
}
const count = (name: string) => board.columns.find(col => col.name === name)?.tasks.length ?? 0
const active = count('running') + count('ready')
if (active === 0) {
return null
}
return (
<Tip label={`Kanban — ${count('running')} running, ${count('ready')} ready`}>
<button
className={cn(
'inline-flex h-full items-center gap-1 rounded-none px-1.5 text-[0.6875rem] tabular-nums transition-colors',
'text-(--ui-text-tertiary) hover:bg-(--chrome-action-hover) hover:text-foreground'
)}
onClick={() => host.navigate('/kanban')}
type="button"
>
<Codicon name="project" size="0.7rem" />
<span>{active}</span>
</button>
</Tip>
)
}
const plugin: HermesPlugin = {
id: 'kanban',
name: 'Kanban',
defaultEnabled: false,
register(ctx) {
bindApi(ctx.rest, ctx.storage, ctx.socket)
ctx.registerMany([
{
id: 'page',
area: ROUTES_AREA,
data: { path: '/kanban' } satisfies RouteContribution,
render: () => <KanbanBoardPage />
},
{
id: 'nav',
area: SIDEBAR_NAV_AREA,
order: 50,
data: { codicon: 'project', label: 'Kanban', path: '/kanban' } satisfies SidebarNavContribution
},
{
id: 'count',
area: STATUSBAR_AREAS.right,
order: 80,
render: () => <KanbanCount />
},
{
id: 'open',
area: PALETTE_AREA,
data: {
id: 'kanban.open',
label: 'Kanban: Open board',
keywords: ['kanban', 'board', 'tasks', 'agents'],
run: () => host.navigate('/kanban')
} satisfies PaletteContribution
}
])
}
}
export default plugin