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:
alt-glitch 2026-06-09 08:02:25 +00:00
parent b3d2de87f9
commit fdc0e5fea5
8 changed files with 69 additions and 34 deletions

View file

@ -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.

View file

@ -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 }

View file

@ -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). */

View file

@ -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}>

View file

@ -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 }) {

View file

@ -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>

View file

@ -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>

View file

@ -13,6 +13,8 @@
"noUncheckedIndexedAccess": true,
"noFallthroughCasesInSwitch": true,
"noImplicitOverride": true,
"noUnusedLocals": true,
"noImplicitReturns": true,
"jsx": "preserve",
"jsxImportSource": "@opentui/solid",
"types": ["bun"],