hermes-agent/apps/desktop/eslint.config.mjs
ethernet e026fd61d8 lint(desktop): ban atom-mirrored refs via no-restricted-syntax eslint rule
A ref synced from a nanostores atom via useEffect lags the atom by one
render. Callbacks that read the ref instead of the atom get stale values —
cancelRun sent session.interrupt to the wrong session (#66485), and
steerPrompt/restoreToMessage/editMessage had closure-priority stale reads.

The rule catches the three shapes of this antipattern:
  - useEffect(() => { ref.current = value }, [value])
  - useEffect(() => { ref.current = value; ... }, [value])
  - useEffect(() => { setMutableRef(ref, value) }, [value])

All 79 existing hits get eslint-disable-next-line with a comment. The
legitimate ref writes (DOM instances, mount flags, request tokens,
prev-value tracking, prop mirrors) stay suppressed; the 11 real bug
sites will be fixed in the next commit so their suppressions can be
removed.

Rule is scoped to apps/desktop only — ui-tui and tests-js don't use
nanostores and don't have this pattern.
2026-07-25 18:12:48 -07:00

81 lines
3.5 KiB
JavaScript

import shared from '../../eslint.config.shared.mjs'
import globals from 'globals'
export default [
...shared,
{
// Desktop is an Electron renderer — it legitimately uses browser globals
// (window, document, etc). Re-add them here; the shared config omits
// globals.browser so terminal-only workspaces (ui-tui) don't get them.
files: ['**/*.{ts,tsx}'],
languageOptions: {
globals: {
...globals.browser,
...globals.node
}
}
},
{
// THE PLUGIN FENCE: plugins speak @hermes/plugin-sdk (+ react), never `@/…`
// internals — the same isolation a runtime-fetched published plugin gets,
// enforced on bundled ones so the SDK surface stays honest and sufficient.
files: ['src/plugins/**/*.{ts,tsx}'],
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
{
group: ['@/*', '../*', '@hermes/shared'],
message: 'Plugins import only @hermes/plugin-sdk (and react). Missing something? Add it to the SDK.'
}
]
}
]
}
},
{
files: ['**/*.test.tsx'],
rules: {
'no-restricted-globals': ['warn', 'document']
}
},
{
// Ban mirroring reactive values into refs via useEffect — the "atom-mirrored
// ref" antipattern. A ref synced from a nanostores atom via useEffect lags the
// atom by one render, which creates stale-read bugs in callbacks that read the
// ref (cancelRun sent session.interrupt to the wrong session; steerPrompt,
// restoreToMessage, editMessage all had closure-priority stale reads). The fix
// is to read $atom.get() directly in callbacks instead. This rule catches the
// mirroring effect at lint time so the pattern can't reappear. Legitimate
// non-atom ref writes inside useEffect (DOM instance refs, mount flags, request
// tokens, prop mirrors) get an eslint-disable-next-line with a comment.
files: ['src/**/*.{ts,tsx}'],
rules: {
'no-restricted-syntax': [
'error',
{
// useEffect(() => { someRef.current = value }, [value])
selector:
'CallExpression[callee.name="useEffect"] > ArrowFunctionExpression[body.type="AssignmentExpression"][body.left.type="MemberExpression"][body.left.property.name="current"]',
message:
'Do not mirror reactive values into refs via useEffect. Read $atom.get() directly in callbacks instead — refs synced from atoms lag one render and cause stale-read bugs.'
},
{
// useEffect(() => { someRef.current = value; ... }, [value])
selector:
'CallExpression[callee.name="useEffect"] > ArrowFunctionExpression[body.type="BlockStatement"]:has(AssignmentExpression[left.type="MemberExpression"][left.property.name="current"])',
message:
'Do not mirror reactive values into refs via useEffect. Read $atom.get() directly in callbacks instead — refs synced from atoms lag one render and cause stale-read bugs.'
},
{
// useEffect(() => { setMutableRef(ref, value) }, [value])
selector:
'CallExpression[callee.name="useEffect"] > ArrowFunctionExpression[body.type="BlockStatement"]:has(CallExpression[callee.name="setMutableRef"])',
message:
'Do not mirror reactive values into refs via useEffect (setMutableRef included). Read $atom.get() directly in callbacks instead — refs synced from atoms lag one render and cause stale-read bugs.'
}
]
}
}
]