fmt(js): npm run fix on merge (#88813)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
hermes-seaeye[bot]
2026-08-18 01:10:42 +00:00
committed by GitHub
parent 133381508f
commit a3995f8aed
4 changed files with 127 additions and 38 deletions
+14 -2
View File
@@ -9,7 +9,14 @@
* desktop's selection never flips the server-wide current-board pointer.
*/
import { atom, type PluginOs, type PluginRestOptions, type PluginStorage, type PluginTranslate, queryClient } from '@hermes/plugin-sdk'
import {
atom,
type PluginOs,
type PluginRestOptions,
type PluginStorage,
type PluginTranslate,
queryClient
} from '@hermes/plugin-sdk'
// Native completion notification.
import { bindCompletionNotify, type CompletionEvent, onKanbanEventsFrame } from './completion-notify'
@@ -85,7 +92,12 @@ interface Persisted<T> {
* runs on unload/disable — so nothing (store sync, socket) survives a toggle
* or duplicates on re-enable. The events socket is pinned to a board at
* handshake, so a board switch closes + reopens it. */
export function bindApi(r: Rest, storage: PluginStorage, socket: Socket, notifyDoors?: { os?: PluginOs; t?: PluginTranslate }): () => void {
export function bindApi(
r: Rest,
storage: PluginStorage,
socket: Socket,
notifyDoors?: { os?: PluginOs; t?: PluginTranslate }
): () => void {
rest = r
bindCompletionNotify(r, notifyDoors?.t, notifyDoors?.os)
const unsubs: Array<() => void> = []
@@ -16,25 +16,34 @@ import type { CompletionEvent } from './completion-notify'
* bans import() in type annotations). */
type Rest = <T>(path: string, opts?: PluginRestOptions) => Promise<T>
type Translate = (key: string, ...args: unknown[]) => string
interface OsDoor { notify: (input: { title: string; body?: string; silent?: boolean }) => void }
interface OsDoor {
notify: (input: { title: string; body?: string; silent?: boolean }) => void
}
interface Mod {
bindCompletionNotify(r: Rest, t?: Translate, os?: OsDoor): void
onKanbanEventsFrame(slug: string, events?: CompletionEvent[]): Promise<boolean>
}
const { hostMock } = vi.hoisted(() => ({
hostMock: { notify: vi.fn(), navigate: vi.fn() },
hostMock: { notify: vi.fn(), navigate: vi.fn() }
}))
vi.mock('@hermes/plugin-sdk', () => ({
host: hostMock,
// Pulled in transitively via ./i18n (the module reads its `en` bundle for
// fallback titles); never called in these tests.
usePluginI18n: () => (key: string) => key,
usePluginI18n: () => (key: string) => key
}))
type NotifyInput = { message: string; title?: string; kind?: string; detail?: string; action?: { label: string; onClick: () => void } }
const lastNotify = (): NotifyInput => hostMock.notify.mock.calls[hostMock.notify.mock.calls.length - 1][0] as NotifyInput
type NotifyInput = {
message: string
title?: string
kind?: string
detail?: string
action?: { label: string; onClick: () => void }
}
const lastNotify = (): NotifyInput =>
hostMock.notify.mock.calls[hostMock.notify.mock.calls.length - 1][0] as NotifyInput
/** Rest stub: GET /board resolves to the current latest_event_id. */
function makeRest(latest: () => number) {
@@ -53,11 +62,16 @@ async function loadModule(): Promise<Mod> {
return import('./completion-notify')
}
const ev = (id: number, kind = 'created', payload: Record<string, unknown> | null = null, taskId = `t${id}`): CompletionEvent => ({
const ev = (
id: number,
kind = 'created',
payload: Record<string, unknown> | null = null,
taskId = `t${id}`
): CompletionEvent => ({
id,
kind,
task_id: taskId,
payload,
payload
})
beforeEach(() => {
@@ -83,7 +97,7 @@ describe('authoritative baseline', () => {
m.bindCompletionNotify(makeRest(() => 100) as never)
const fired = await m.onKanbanEventsFrame('smoke', [
ev(101, 'completed', { summary: 'Done', artifacts: ['/tmp/x/report.md'] }),
ev(101, 'completed', { summary: 'Done', artifacts: ['/tmp/x/report.md'] })
])
expect(fired).toBe(true)
@@ -113,7 +127,12 @@ describe('authoritative baseline', () => {
expect(hostMock.notify).toHaveBeenCalledTimes(2)
// Reconnect replays from 0: ids <= cursor are history, only 103 is new.
await m.onKanbanEventsFrame('smoke', [ev(99, 'completed'), ev(101, 'completed'), ev(102, 'completed'), ev(103, 'completed')])
await m.onKanbanEventsFrame('smoke', [
ev(99, 'completed'),
ev(101, 'completed'),
ev(102, 'completed'),
ev(103, 'completed')
])
expect(hostMock.notify).toHaveBeenCalledTimes(3)
expect(hostMock.notify.mock.calls[2][0]).toMatchObject({ message: 't103' })
@@ -165,7 +184,9 @@ describe('authoritative baseline', () => {
const rest = vi.fn(async (path: string) => {
if (path.startsWith('/board')) {
if (failBoard) {throw new Error('board unavailable')}
if (failBoard) {
throw new Error('board unavailable')
}
return { latest_event_id: 200 }
}
@@ -226,7 +247,10 @@ describe('cursor advancement', () => {
describe('board isolation', () => {
it('never mixes cursors between boards', async () => {
const latest = new Map<string, number>([['a', 100], ['b', 200]])
const latest = new Map<string, number>([
['a', 100],
['b', 200]
])
const rest = vi.fn(async (path: string) => {
if (path.startsWith('/board')) {
@@ -250,7 +274,10 @@ describe('board isolation', () => {
})
it('switch away and back reuses the prior cursor, never reset to current MAX', async () => {
const latest = new Map<string, number>([['a', 100], ['b', 50]])
const latest = new Map<string, number>([
['a', 100],
['b', 50]
])
const rest = vi.fn(async (path: string) => {
if (path.startsWith('/board')) {
@@ -308,7 +335,7 @@ describe('notification content', () => {
title: 'Task completed',
message: 'Done',
detail: 't101',
action: { label: 'Open Kanban', onClick: expect.any(Function) },
action: { label: 'Open Kanban', onClick: expect.any(Function) }
})
})
@@ -317,7 +344,7 @@ describe('notification content', () => {
m.bindCompletionNotify(makeRest(() => 100) as never)
await m.onKanbanEventsFrame('smoke', [
ev(101, 'completed', { summary: 'Done', artifacts: ['/work/x/out/report.md'] }),
ev(101, 'completed', { summary: 'Done', artifacts: ['/work/x/out/report.md'] })
])
expect(lastNotify().detail).toBe('t101 · report.md')
@@ -328,7 +355,7 @@ describe('notification content', () => {
m.bindCompletionNotify(makeRest(() => 100) as never)
await m.onKanbanEventsFrame('smoke', [
ev(101, 'completed', { summary: 'Done', artifacts: ['/a/1.md', '/b/2.md', '/c/3.md'] }),
ev(101, 'completed', { summary: 'Done', artifacts: ['/a/1.md', '/b/2.md', '/c/3.md'] })
])
expect(lastNotify().detail).toBe('t101 · 3 artifacts')
@@ -341,7 +368,7 @@ describe('notification content', () => {
await m.onKanbanEventsFrame('smoke', [
ev(101, 'completed', null),
ev(102, 'completed', { summary: 42, artifacts: 'nope' }),
ev(103, 'completed', { summary: 'ok', artifacts: [7, ' /tmp/x/ok.md ', null] }),
ev(103, 'completed', { summary: 'ok', artifacts: [7, ' /tmp/x/ok.md ', null] })
])
// All three are unseen completions; none may throw.
@@ -396,7 +423,7 @@ describe('terminal kinds beyond completed', () => {
kind: 'warning',
title: 'Task blocked — needs your input',
message: 'needs API key',
detail: 't101',
detail: 't101'
})
})
@@ -427,7 +454,11 @@ describe('terminal kinds beyond completed', () => {
const m = await loadModule()
m.bindCompletionNotify(makeRest(() => 100) as never)
await m.onKanbanEventsFrame('smoke', [ev(101, 'status', { status: 'running' }), ev(102, 'archived'), ev(103, 'unblocked')])
await m.onKanbanEventsFrame('smoke', [
ev(101, 'status', { status: 'running' }),
ev(102, 'archived'),
ev(103, 'unblocked')
])
expect(hostMock.notify).not.toHaveBeenCalled()
// Cursor moved past 103: a replayed blocked at 102 stays silent, 104 fires.
@@ -448,12 +479,16 @@ describe('native OS door', () => {
expect(os.notify).toHaveBeenCalledTimes(1)
expect(os.notify.mock.calls[0][0]).toEqual({
title: 'Task blocked — needs your input',
body: 'needs input\nt101',
body: 'needs input\nt101'
})
})
it('an os door that throws never breaks the toast or the frame result', async () => {
const os = { notify: vi.fn(() => { throw new Error('no shell') }) }
const os = {
notify: vi.fn(() => {
throw new Error('no shell')
})
}
const m = await loadModule()
m.bindCompletionNotify(makeRest(() => 100) as never, undefined, os)
@@ -480,11 +515,17 @@ describe('native OS door', () => {
describe('i18n routing', () => {
it('uses the bound plugin translator when it resolves the key', async () => {
const t = vi.fn((key: string, ...args: unknown[]) => {
if (key === 'notify.completedTitle') {return 'タスク完了'}
if (key === 'notify.completedTitle') {
return 'タスク完了'
}
if (key === 'notify.openKanban') {return 'かんばんを開く'}
if (key === 'notify.openKanban') {
return 'かんばんを開く'
}
if (key === 'notify.artifacts') {return `成果物 ${args[0]} 件`}
if (key === 'notify.artifacts') {
return `成果物 ${args[0]} 件`
}
return key
})
@@ -497,7 +538,7 @@ describe('i18n routing', () => {
expect(lastNotify()).toMatchObject({
title: 'タスク完了',
detail: 't101 · 成果物 2 件',
action: { label: 'かんばんを開く', onClick: expect.any(Function) },
action: { label: 'かんばんを開く', onClick: expect.any(Function) }
})
})
@@ -92,11 +92,15 @@ export function bindCompletionNotify(r: Rest, pluginTranslate?: PluginTranslate,
}
async function ensureBaseline(slug: string): Promise<void> {
if (seenEventIdByBoard.has(slug) || baselinePending.has(slug)) { return }
if (seenEventIdByBoard.has(slug) || baselinePending.has(slug)) {
return
}
baselinePending.add(slug)
try {
const board = (await rest!<{ latest_event_id?: unknown }>(`/board?board=${encodeURIComponent(slug)}`)) as { latest_event_id?: unknown }
const board = (await rest!<{ latest_event_id?: unknown }>(`/board?board=${encodeURIComponent(slug)}`)) as {
latest_event_id?: unknown
}
seenEventIdByBoard.set(slug, typeof board.latest_event_id === 'number' ? board.latest_event_id : 0)
} catch {
// Fail-closed: unknown baseline → notifications stay suppressed.
@@ -133,41 +137,72 @@ function notifyOne(kind: string, spec: { titleKey: string; toast: ToastKind }, e
const taskId = (ev.task_id ?? '').trim()
const body = bodyFor(kind, ev)
const artifacts = kind === 'completed' && Array.isArray(ev.payload?.artifacts)
? (ev.payload!.artifacts as unknown[]).filter((a): a is string => typeof a === 'string' && a.trim().length > 0).map(a => a.trim())
: []
const artifacts =
kind === 'completed' && Array.isArray(ev.payload?.artifacts)
? (ev.payload!.artifacts as unknown[])
.filter((a): a is string => typeof a === 'string' && a.trim().length > 0)
.map(a => a.trim())
: []
const artifactText = artifacts.length === 1 ? (artifacts[0].split(/[\\/]/).pop() || artifacts[0]) : artifacts.length > 1 ? t('notify.artifacts', artifacts.length) : ''
const artifactText =
artifacts.length === 1
? artifacts[0].split(/[\\/]/).pop() || artifacts[0]
: artifacts.length > 1
? t('notify.artifacts', artifacts.length)
: ''
const detail = [taskId, artifactText].filter(Boolean).join(' · ')
const title = t(spec.titleKey)
const message = body || taskId || title
host.notify({ kind: spec.toast, title, message, ...(detail ? { detail } : {}), action: { label: t('notify.openKanban'), onClick: () => host.navigate('/kanban') } })
host.notify({
kind: spec.toast,
title,
message,
...(detail ? { detail } : {}),
action: { label: t('notify.openKanban'), onClick: () => host.navigate('/kanban') }
})
// Native OS notification — the desktop shell fires it only while the user
// is away from Hermes (the toast above covers the foreground case). Isolated:
// a missing/broken shell must not mark the toast as unfired.
try { osDoor?.notify({ title, body: [message, detail].filter(Boolean).join('\n') }) } catch { /* swallowed */ }
try {
osDoor?.notify({ title, body: [message, detail].filter(Boolean).join('\n') })
} catch {
/* swallowed */
}
}
/** Consume one /events frame for a board. Returns true when a terminal-event
* notification was fired. Never throws: notification failure cannot
* interfere with api.ts cache invalidation. */
export async function onKanbanEventsFrame(slug: string, events?: CompletionEvent[]): Promise<boolean> {
if (!events?.length || slug === '' || !rest) { return false }
if (!events?.length || slug === '' || !rest) {
return false
}
await ensureBaseline(slug)
const seen = seenEventIdByBoard.get(slug)
if (seen === undefined) { return false } // fail-closed
if (seen === undefined) {
return false
} // fail-closed
let fired = false
let cursor = seen
for (const ev of events) {
if (typeof ev.id !== 'number' || ev.id <= cursor) { continue }
if (typeof ev.id !== 'number' || ev.id <= cursor) {
continue
}
cursor = ev.id
seenEventIdByBoard.set(slug, cursor)
const spec = TERMINAL_NOTIFY.get(ev.kind ?? '')
if (spec) { try { notifyOne(ev.kind!, spec, ev); fired = true } catch { /* swallowed */ } }
if (spec) {
try {
notifyOne(ev.kind!, spec, ev)
fired = true
} catch {
/* swallowed */
}
}
}
return fired
@@ -150,6 +150,7 @@ export async function setMainModelAssignment(
): Promise<ModelAssignmentResponse> {
const { connection, generation } = beginCronModelImpactAssignment()
const profile = profileIdentity()
// Only pass the extra arg when a scope override exists, so unscoped callers
// keep the exact legacy call shape.
const result =