mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
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.
115 lines
3.1 KiB
TypeScript
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
|