mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-16 14:32:34 +00:00
Route the app off its hand-rolled helpers onto lib/{text,time,format,json-format}
and the new primitives, plus assorted small tidy-ups:
- compactNumber for counts/tokens; normalize/capitalize/asText at the many
filter/label sites; shared Intl date/time formatters; row-hover + framed
editor adoption; scrollbar-gutter + padding parity on list surfaces.
- Messaging/Artifacts/Cron search hints + narrow-viewport tab dropdown;
floating-pet adopts useOnProfileSwitch; number formatting in statusbar,
command-center, agents.
- Electron: native overlay width + backend spawn tidy.
- Settings > Keys: credential fields read as plain subtext (all-unset) until
the group is focused or expanded, then take full input chrome with no
horizontal/vertical shift; inline Remove (trash) + Save mirror SearchField's
trailing-clear pattern instead of a floating hint that overlapped the card;
Esc still cancels. Drops the now-dead or/escToCancel i18n keys.
- Shared TabDropdown/ResponsiveTabs (components/ui): PageSearchShell and the
Command Center log file/level filters reuse the one narrow-width collapse.
- OverlayNav: data-driven pane nav — persistent rail on wide, a single dropdown
riding the titlebar strip on narrow; Settings and Command Center adopt it, and
the mobile dropdown carries the same section icons as the rail. Fixes narrow
vertical centering, redundant mobile section titles, gateway-status wrap, and
Panel master/detail stacking.
- OverlayIconButton is now the titlebar ghost button, matching the close X at
every size. Settings sub-view nav opens section + sub-view in one navigate so
API-keys/accounts actually open on narrow.
- Settings > Model: cube icon (was the {} namespace glyph) and a DOM-shaped
skeleton in place of the centered spinner.
- Command palette / session switcher clear the macOS traffic lights on small
screens.
- Prettier/eslint sweep across the touched files.
179 lines
4.3 KiB
TypeScript
179 lines
4.3 KiB
TypeScript
/**
|
|
* Tiny user-facing changelog builder. Takes a list of raw commit summaries,
|
|
* parses the Conventional Commits 1.0 header (`type(scope)!: subject`),
|
|
* filters internal noise (chore/ci/docs/...), and groups the rest into
|
|
* friendly buckets for end users (What's new, Fixed, Faster, Improved).
|
|
*
|
|
* Inlined (rather than depending on `conventional-commits-parser`) because
|
|
* that package's index re-exports a Node `stream` helper which won't load
|
|
* in the sandboxed Electron renderer, and its actual parse logic for the
|
|
* header is a small regex.
|
|
*/
|
|
|
|
import { capitalize } from '@/lib/text'
|
|
|
|
export type CommitGroupId = 'new' | 'fixed' | 'faster' | 'improved' | 'other'
|
|
|
|
export interface CommitGroup {
|
|
id: CommitGroupId
|
|
label: string
|
|
items: string[]
|
|
}
|
|
|
|
export interface ParsedCommit {
|
|
type: null | string
|
|
scope: null | string
|
|
breaking: boolean
|
|
subject: string
|
|
}
|
|
|
|
export interface CommitChangelogInput {
|
|
summary?: string
|
|
}
|
|
|
|
interface BuildOptions {
|
|
maxGroups?: number
|
|
maxPerGroup?: number
|
|
maxTotal?: number
|
|
}
|
|
|
|
const GROUP_META: Record<CommitGroupId, { label: string; order: number }> = {
|
|
new: { label: "What's new", order: 0 },
|
|
fixed: { label: 'Fixed', order: 1 },
|
|
faster: { label: 'Faster', order: 2 },
|
|
improved: { label: 'Improved', order: 3 },
|
|
other: { label: 'Other improvements', order: 4 }
|
|
}
|
|
|
|
const TYPE_TO_GROUP: Record<string, CommitGroupId> = {
|
|
feat: 'new',
|
|
feature: 'new',
|
|
fix: 'fixed',
|
|
bugfix: 'fixed',
|
|
hotfix: 'fixed',
|
|
revert: 'fixed',
|
|
perf: 'faster',
|
|
performance: 'faster',
|
|
refactor: 'improved',
|
|
a11y: 'improved',
|
|
ui: 'improved',
|
|
ux: 'improved'
|
|
}
|
|
|
|
const HIDDEN_TYPES = new Set([
|
|
'build',
|
|
'chore',
|
|
'ci',
|
|
'dep',
|
|
'deps',
|
|
'doc',
|
|
'docs',
|
|
'lint',
|
|
'release',
|
|
'style',
|
|
'test',
|
|
'tests',
|
|
'wip'
|
|
])
|
|
|
|
const FALLBACK_GROUP: CommitGroup = { id: 'other', items: ['Improvements and fixes'], label: 'In this update' }
|
|
|
|
const CONVENTIONAL_HEADER = /^(?<type>[a-zA-Z][a-zA-Z0-9_-]*)(?:\((?<scope>[^)]+)\))?(?<bang>!)?:\s+(?<subject>.+)$/
|
|
|
|
/** Parse a single commit header line per Conventional Commits 1.0. */
|
|
export function parseCommitHeader(raw: string): ParsedCommit {
|
|
const header = (raw ?? '').split(/\r?\n/, 1)[0].trim()
|
|
|
|
if (!header) {
|
|
return { breaking: false, scope: null, subject: '', type: null }
|
|
}
|
|
|
|
const match = CONVENTIONAL_HEADER.exec(header)
|
|
|
|
if (!match?.groups) {
|
|
return { breaking: false, scope: null, subject: header, type: null }
|
|
}
|
|
|
|
return {
|
|
breaking: Boolean(match.groups.bang),
|
|
scope: match.groups.scope ?? null,
|
|
subject: match.groups.subject.trim(),
|
|
type: match.groups.type.toLowerCase()
|
|
}
|
|
}
|
|
|
|
function tidySubject(subject: string): string {
|
|
const cleaned = subject
|
|
.replace(/\s+/g, ' ')
|
|
.replace(/[.;,\s]+$/, '')
|
|
.trim()
|
|
|
|
if (!cleaned) {
|
|
return cleaned
|
|
}
|
|
|
|
return capitalize(cleaned)
|
|
}
|
|
|
|
/**
|
|
* Build a small grouped changelog from a list of raw commits.
|
|
* Always returns at least one group; falls back to a neutral placeholder
|
|
* when every commit was filtered or unparseable.
|
|
*/
|
|
export function buildCommitChangelog(
|
|
commits: readonly CommitChangelogInput[] | undefined,
|
|
options: BuildOptions = {}
|
|
): CommitGroup[] {
|
|
const { maxGroups = 3, maxPerGroup = 4, maxTotal = 6 } = options
|
|
const groups = new Map<CommitGroupId, string[]>()
|
|
const seen = new Set<string>()
|
|
let total = 0
|
|
|
|
for (const commit of commits ?? []) {
|
|
if (total >= maxTotal) {
|
|
break
|
|
}
|
|
|
|
const parsed = parseCommitHeader(commit.summary ?? '')
|
|
|
|
if (parsed.type && HIDDEN_TYPES.has(parsed.type)) {
|
|
continue
|
|
}
|
|
|
|
const groupId: CommitGroupId = parsed.type ? (TYPE_TO_GROUP[parsed.type] ?? 'other') : 'other'
|
|
const subject = tidySubject(parsed.subject)
|
|
|
|
if (!subject) {
|
|
continue
|
|
}
|
|
|
|
const dedupeKey = subject.toLowerCase()
|
|
|
|
if (seen.has(dedupeKey)) {
|
|
continue
|
|
}
|
|
|
|
const bucket = groups.get(groupId) ?? []
|
|
|
|
if (bucket.length >= maxPerGroup) {
|
|
continue
|
|
}
|
|
|
|
bucket.push(subject)
|
|
groups.set(groupId, bucket)
|
|
seen.add(dedupeKey)
|
|
total += 1
|
|
}
|
|
|
|
const result = Array.from(groups.entries())
|
|
.map(([id, items]) => ({ id, items, label: GROUP_META[id].label, order: GROUP_META[id].order }))
|
|
.sort((a, b) => a.order - b.order)
|
|
.slice(0, maxGroups)
|
|
.map(({ id, items, label }): CommitGroup => ({ id, items, label }))
|
|
|
|
if (result.length === 0) {
|
|
return [FALLBACK_GROUP]
|
|
}
|
|
|
|
return result
|
|
}
|