Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions apps/desktop/src/plugins/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ in the companion
[`hermes-example-plugins`](https://github.com/NousResearch/hermes-example-plugins)
repo instead.

The Axiom fork's bundled **Update Control** plugin belongs here because it
dogfoods the fork-local `host.updates` facade and provides a shipped management
surface. It reads detached local-client and active-backend snapshots, including
deploy/upstream disparity when core publishes it, then hands the active target
to the core-owned native updater. Do not add direct checks, branch writes, pull,
merge, install, restart, or relaunch mutation to plugin code; polling,
confirmation, dirty-tree policy, deploy reconciliation, and process handoff
stay in core.

User- and agent-authored plugins load at runtime from
`$HERMES_HOME/desktop-plugins/<name>/plugin.js` (the disk door) — see the
`hermes-desktop-plugins` skill.
24 changes: 24 additions & 0 deletions apps/desktop/src/plugins/update-control/model.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { describe, expect, it } from 'vitest'

import { friendlyError, hasUpdate, shortSha } from './model'

describe('update summaries', () => {
it('treats either the explicit flag or a positive behind count as an update', () => {
expect(hasUpdate({ supported: true, updateAvailable: true })).toBe(true)
expect(hasUpdate({ supported: true, behind: 2 })).toBe(true)
expect(hasUpdate({ supported: false, behind: 2 })).toBe(false)
expect(hasUpdate({ supported: true, behind: 0 })).toBe(false)
})

it('shortens commit identifiers without inventing one', () => {
expect(shortSha('1234567890abcdef')).toBe('12345678')
expect(shortSha('abc')).toBe('abc')
expect(shortSha()).toBe('—')
})

it('turns unknown failures into useful general-purpose copy', () => {
expect(friendlyError(new Error('bridge offline'))).toBe('bridge offline')
expect(friendlyError('timeout')).toBe('timeout')
expect(friendlyError(null)).toBe('Update information is unavailable right now.')
})
})
25 changes: 25 additions & 0 deletions apps/desktop/src/plugins/update-control/model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export interface UpdateSummary {
supported?: boolean
updateAvailable?: boolean
behind?: number
}

export function hasUpdate(status: UpdateSummary | null | undefined): boolean {
return status?.supported === true && (status.updateAvailable === true || (status.behind ?? 0) > 0)
}

export function shortSha(value?: null | string): string {
return value ? value.slice(0, 8) : '—'
}

export function friendlyError(error: unknown): string {
if (error instanceof Error && error.message.trim()) {
return error.message
}

if (typeof error === 'string' && error.trim()) {
return error
}

return 'Update information is unavailable right now.'
}
33 changes: 33 additions & 0 deletions apps/desktop/src/plugins/update-control/plugin.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { describe, expect, it, vi } from 'vitest'

import plugin from './plugin'

describe('Update Control plugin registration', () => {
it('ships opt-in and contributes the page, navigation, status, and palette action', () => {
const registerMany = vi.fn()

plugin.register({ registerMany } as never)

expect(plugin.defaultEnabled).toBe(false)
expect(plugin.id).toBe('update-control')

const contributions = registerMany.mock.calls[0]?.[0] as Array<{
area: string
data?: { label?: string; path?: string }
id: string
render?: unknown
}>

expect(contributions.map(contribution => contribution.id)).toEqual(['page', 'nav', 'status', 'open'])
expect(contributions.find(contribution => contribution.id === 'page')).toMatchObject({
area: 'routes',
data: { path: '/update-control' }
})
expect(contributions.find(contribution => contribution.id === 'nav')).toMatchObject({
area: 'sidebar.nav',
data: { label: 'Update Control', path: '/update-control' }
})
expect(contributions.find(contribution => contribution.id === 'status')?.render).toBeTypeOf('function')
expect(contributions.find(contribution => contribution.id === 'open')?.data?.label).toBe('Update Control: Open')
})
})
Loading
Loading