mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-10 13:31:38 +00:00
opentui(ts): no-non-null-assertion + noUnusedLocals + noImplicitReturns
Promote strictness in ui-tui-opentui-v2:
- eslint: @typescript-eslint/no-non-null-assertion: error (with a test
override block keeping `!` in *.test.ts/tsx fixtures), and promote
unused-imports/no-unused-vars warn → error.
- tsconfig: add noUnusedLocals + noImplicitReturns.
Remove all 24 production `!` non-null assertions by replacing each with
a real guard / default / early-return, preserving rendered behavior:
- gateway/client.ts: read the pending entry once and guard (vs has()+get()!).
- logic/theme.ts: guard the parseHex regex match; `?? 0` on the always-
in-bounds XTERM_6_LEVELS lookups; restructure backgroundLuminance to
branch into a typed tuple and use charAt() for the 3-digit hex expand.
- view/homeHint.tsx: use Solid's <Show>{value => …} callback form to
narrow info().model / info().cwd instead of `!`.
- view/reasoningPart.tsx: guard the regex match before slicing m[0].
- view/statusBar.tsx: read model/cwd/pct into locals + guard; `?? 0` on
the showBar()-guarded context-bar percentage.
- view/toolPart.tsx: guard the single-arg entry; `?? 0` on the
duration-guarded fmtDuration.
This commit is contained in:
parent
b3d2de87f9
commit
fdc0e5fea5
8 changed files with 69 additions and 34 deletions
|
|
@ -24,9 +24,10 @@ export default tseslint.config(
|
|||
"@typescript-eslint/no-explicit-any": "error",
|
||||
"@typescript-eslint/consistent-type-imports": ["error", { prefer: "type-imports" }],
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "error",
|
||||
"unused-imports/no-unused-imports": "error",
|
||||
"unused-imports/no-unused-vars": [
|
||||
"warn",
|
||||
"error",
|
||||
{ vars: "all", varsIgnorePattern: "^_", args: "after-used", argsIgnorePattern: "^_" },
|
||||
],
|
||||
|
||||
|
|
@ -56,6 +57,13 @@ export default tseslint.config(
|
|||
"@typescript-eslint/require-await": "warn",
|
||||
},
|
||||
},
|
||||
{
|
||||
// Tests keep their `!` non-null assertions (fixtures with known-present data).
|
||||
files: ["**/*.test.ts", "**/*.test.tsx"],
|
||||
rules: {
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
// The eslint flat config is JS-only; the typed parser project service does not
|
||||
// cover it, so disable type-checking there to avoid parser errors.
|
||||
|
|
|
|||
|
|
@ -170,8 +170,9 @@ export class RawGatewayClient {
|
|||
const frame = msg as { id?: unknown; method?: unknown; params?: unknown; result?: unknown; error?: unknown }
|
||||
|
||||
// Response: has an id matching a pending request.
|
||||
if (typeof frame.id === 'string' && this.pending.has(frame.id)) {
|
||||
const p = this.pending.get(frame.id)!
|
||||
const pending = typeof frame.id === 'string' ? this.pending.get(frame.id) : undefined
|
||||
if (typeof frame.id === 'string' && pending) {
|
||||
const p = pending
|
||||
this.pending.delete(frame.id)
|
||||
if (frame.error) {
|
||||
const err = frame.error as { code?: number; message?: string }
|
||||
|
|
|
|||
|
|
@ -81,8 +81,9 @@ export interface GatewaySkin {
|
|||
|
||||
function parseHex(h: string): [number, number, number] | null {
|
||||
const m = /^#?([0-9a-f]{6})$/i.exec(h)
|
||||
if (!m) return null
|
||||
const n = parseInt(m[1]!, 16)
|
||||
const hex = m?.[1]
|
||||
if (!hex) return null
|
||||
const n = parseInt(hex, 16)
|
||||
return [(n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff]
|
||||
}
|
||||
|
||||
|
|
@ -124,10 +125,12 @@ function xtermEightBitRgb(colorNumber: number): [number, number, number] {
|
|||
}
|
||||
if (colorNumber >= 16) {
|
||||
const offset = colorNumber - 16
|
||||
// Indices are `% 6`, always within XTERM_6_LEVELS' bounds; `?? 0` only
|
||||
// satisfies noUncheckedIndexedAccess and is never actually reached.
|
||||
return [
|
||||
XTERM_6_LEVELS[Math.floor(offset / 36) % 6]!,
|
||||
XTERM_6_LEVELS[Math.floor(offset / 6) % 6]!,
|
||||
XTERM_6_LEVELS[offset % 6]!
|
||||
XTERM_6_LEVELS[Math.floor(offset / 36) % 6] ?? 0,
|
||||
XTERM_6_LEVELS[Math.floor(offset / 6) % 6] ?? 0,
|
||||
XTERM_6_LEVELS[offset % 6] ?? 0
|
||||
]
|
||||
}
|
||||
return [0, 0, 0]
|
||||
|
|
@ -327,13 +330,20 @@ function backgroundLuminance(raw: string): null | number {
|
|||
const v = raw.trim().toLowerCase()
|
||||
if (!v) return null
|
||||
const hex = v.startsWith('#') ? v.slice(1) : v
|
||||
const rgb = HEX_6_RE.test(hex)
|
||||
? [parseInt(hex.slice(0, 2), 16), parseInt(hex.slice(2, 4), 16), parseInt(hex.slice(4, 6), 16)]
|
||||
: HEX_3_RE.test(hex)
|
||||
? [parseInt(hex[0]! + hex[0]!, 16), parseInt(hex[1]! + hex[1]!, 16), parseInt(hex[2]! + hex[2]!, 16)]
|
||||
: null
|
||||
let rgb: [number, number, number] | null = null
|
||||
if (HEX_6_RE.test(hex)) {
|
||||
rgb = [parseInt(hex.slice(0, 2), 16), parseInt(hex.slice(2, 4), 16), parseInt(hex.slice(4, 6), 16)]
|
||||
} else if (HEX_3_RE.test(hex)) {
|
||||
// `charAt` always returns a string (vs index access, which is `string |
|
||||
// undefined` under noUncheckedIndexedAccess); the regex guarantees 3 chars.
|
||||
const r = hex.charAt(0)
|
||||
const g = hex.charAt(1)
|
||||
const b = hex.charAt(2)
|
||||
rgb = [parseInt(r + r, 16), parseInt(g + g, 16), parseInt(b + b, 16)]
|
||||
}
|
||||
if (!rgb) return null
|
||||
return (0.2126 * rgb[0]! + 0.7152 * rgb[1]! + 0.0722 * rgb[2]!) / 255
|
||||
const [r, g, b] = rgb
|
||||
return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255
|
||||
}
|
||||
|
||||
/** Pick light vs dark with ordered, explainable env signals (mirror Ink). */
|
||||
|
|
|
|||
|
|
@ -101,18 +101,22 @@ export function HomeHint(props: { store: SessionStore }) {
|
|||
{/* session info block: model · Nous Research / dir / Session id */}
|
||||
<box style={{ flexDirection: 'column' }}>
|
||||
<Show when={info().model}>
|
||||
<text selectable={false}>
|
||||
<span style={{ fg: theme().color.accent }}>{shortModel(info().model!)}</span>
|
||||
<span style={{ fg: theme().color.muted }}> · Nous Research</span>
|
||||
</text>
|
||||
{model => (
|
||||
<text selectable={false}>
|
||||
<span style={{ fg: theme().color.accent }}>{shortModel(model())}</span>
|
||||
<span style={{ fg: theme().color.muted }}> · Nous Research</span>
|
||||
</text>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={info().cwd}>
|
||||
<text selectable={false}>
|
||||
<span style={{ fg: theme().color.muted }}>{shortCwd(info().cwd!)}</span>
|
||||
<Show when={info().branch}>
|
||||
<span style={{ fg: theme().color.muted }}>{` (${info().branch})`}</span>
|
||||
</Show>
|
||||
</text>
|
||||
{cwd => (
|
||||
<text selectable={false}>
|
||||
<span style={{ fg: theme().color.muted }}>{shortCwd(cwd())}</span>
|
||||
<Show when={info().branch}>
|
||||
<span style={{ fg: theme().color.muted }}>{` (${info().branch})`}</span>
|
||||
</Show>
|
||||
</text>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={props.store.state.sessionId}>
|
||||
<text selectable={false}>
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ function reasoningSummary(text: string): { title?: string; body: string } {
|
|||
const s = (text ?? '').replace('[REDACTED]', '').trim()
|
||||
const m = s.match(/^\*\*([^*\n]+)\*\*(?:\r?\n\r?\n|$)/)
|
||||
const title = m?.[1]?.trim()
|
||||
if (!title) return { body: s }
|
||||
return { title, body: s.slice(m![0].length).trimStart() }
|
||||
if (!m || !title) return { body: s }
|
||||
return { title, body: s.slice(m[0].length).trimStart() }
|
||||
}
|
||||
|
||||
export function ReasoningPart(props: { text: string; streaming?: boolean }) {
|
||||
|
|
|
|||
|
|
@ -71,18 +71,25 @@ export function StatusBar(props: { store: SessionStore }) {
|
|||
const dotColor = () =>
|
||||
info().running ? theme().color.statusWarn : props.store.state.ready ? theme().color.statusGood : theme().color.muted
|
||||
|
||||
const model = () => (info().model ? shortModel(info().model!) : '')
|
||||
const model = () => {
|
||||
const m = info().model
|
||||
return m ? shortModel(m) : ''
|
||||
}
|
||||
const effort = () => effortSuffix(info().effort, info().fast)
|
||||
const pct = () => info().contextPercent
|
||||
|
||||
// Progressive disclosure budget (the row is `width - 2` after the box padding).
|
||||
// left = dot+space+model+effort ; the context bar shows only when there's room.
|
||||
const showBar = createMemo(() => pct() !== undefined && dims().width >= 64)
|
||||
const ctxText = () => (showBar() ? `${ctxBar(pct()!, CTX_BAR_CELLS)} ${pct()}%` : '')
|
||||
const ctxText = () => {
|
||||
const p = pct()
|
||||
return showBar() && p !== undefined ? `${ctxBar(p, CTX_BAR_CELLS)} ${p}%` : ''
|
||||
}
|
||||
|
||||
// Right side: cwd (branch), left-truncated to whatever the left side leaves.
|
||||
const cwdFull = createMemo(() => {
|
||||
const c = info().cwd ? shortCwd(info().cwd!) : ''
|
||||
const cwd = info().cwd
|
||||
const c = cwd ? shortCwd(cwd) : ''
|
||||
if (!c) return ''
|
||||
return info().branch ? `${c} (${info().branch})` : c
|
||||
})
|
||||
|
|
@ -111,9 +118,11 @@ export function StatusBar(props: { store: SessionStore }) {
|
|||
<span style={{ fg: theme().color.muted }}>{effort()}</span>
|
||||
</Show>
|
||||
<Show when={showBar()}>
|
||||
{/* a dim divider segments the bar into scannable fields (item 8) */}
|
||||
{/* a dim divider segments the bar into scannable fields (item 8).
|
||||
showBar() already guarantees pct() is defined; `?? 0` only
|
||||
satisfies the type and is never reached. */}
|
||||
<span style={{ fg: theme().color.border }}>{' │ '}</span>
|
||||
<span style={{ fg: ctxColor(pct()!) }}>{ctxBar(pct()!, CTX_BAR_CELLS)}</span>
|
||||
<span style={{ fg: ctxColor(pct() ?? 0) }}>{ctxBar(pct() ?? 0, CTX_BAR_CELLS)}</span>
|
||||
<span style={{ fg: theme().color.statusFg }}>{` ${pct()}%`}</span>
|
||||
</Show>
|
||||
</text>
|
||||
|
|
|
|||
|
|
@ -69,8 +69,9 @@ export function ToolPart(props: { part: ToolPartState }) {
|
|||
const e = argEntries()
|
||||
if (argsObj() === undefined) return !!props.part.argsText // unparsed → show raw
|
||||
if (e.length === 0) return false
|
||||
if (e.length === 1) {
|
||||
const v = e[0]![1]
|
||||
const only = e.length === 1 ? e[0] : undefined
|
||||
if (only) {
|
||||
const v = only[1]
|
||||
const vs = (typeof v === 'string' ? v : JSON.stringify(v)).trim()
|
||||
return vs !== (props.part.argsPreview ?? '').trim()
|
||||
}
|
||||
|
|
@ -118,7 +119,7 @@ export function ToolPart(props: { part: ToolPartState }) {
|
|||
</span>
|
||||
</Show>
|
||||
<Show when={!running() && props.part.duration !== undefined}>
|
||||
<span style={{ fg: theme().color.muted }}>{` · ${fmtDuration(props.part.duration!)}`}</span>
|
||||
<span style={{ fg: theme().color.muted }}>{` · ${fmtDuration(props.part.duration ?? 0)}`}</span>
|
||||
</Show>
|
||||
<Show when={collapsible() && !expanded() && lines().length > 1}>
|
||||
<span style={{ fg: theme().color.muted }}>{` (${lines().length} lines)`}</span>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@
|
|||
"noUncheckedIndexedAccess": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitOverride": true,
|
||||
"noUnusedLocals": true,
|
||||
"noImplicitReturns": true,
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "@opentui/solid",
|
||||
"types": ["bun"],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue