diff --git a/ui-tui-opentui-v2/eslint.config.mjs b/ui-tui-opentui-v2/eslint.config.mjs index 88287cc2234..a98b35d61db 100644 --- a/ui-tui-opentui-v2/eslint.config.mjs +++ b/ui-tui-opentui-v2/eslint.config.mjs @@ -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. diff --git a/ui-tui-opentui-v2/src/boundary/gateway/client.ts b/ui-tui-opentui-v2/src/boundary/gateway/client.ts index dcb2dbbef91..b6831ce2376 100644 --- a/ui-tui-opentui-v2/src/boundary/gateway/client.ts +++ b/ui-tui-opentui-v2/src/boundary/gateway/client.ts @@ -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 } diff --git a/ui-tui-opentui-v2/src/logic/theme.ts b/ui-tui-opentui-v2/src/logic/theme.ts index 5892a24d77d..e273a98f345 100644 --- a/ui-tui-opentui-v2/src/logic/theme.ts +++ b/ui-tui-opentui-v2/src/logic/theme.ts @@ -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). */ diff --git a/ui-tui-opentui-v2/src/view/homeHint.tsx b/ui-tui-opentui-v2/src/view/homeHint.tsx index 482495b8370..76fdfae9582 100644 --- a/ui-tui-opentui-v2/src/view/homeHint.tsx +++ b/ui-tui-opentui-v2/src/view/homeHint.tsx @@ -101,18 +101,22 @@ export function HomeHint(props: { store: SessionStore }) { {/* session info block: model · Nous Research / dir / Session id */} - - {shortModel(info().model!)} - · Nous Research - + {model => ( + + {shortModel(model())} + · Nous Research + + )} - - {shortCwd(info().cwd!)} - - {` (${info().branch})`} - - + {cwd => ( + + {shortCwd(cwd())} + + {` (${info().branch})`} + + + )} diff --git a/ui-tui-opentui-v2/src/view/reasoningPart.tsx b/ui-tui-opentui-v2/src/view/reasoningPart.tsx index a1690915d06..72f72f0e662 100644 --- a/ui-tui-opentui-v2/src/view/reasoningPart.tsx +++ b/ui-tui-opentui-v2/src/view/reasoningPart.tsx @@ -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 }) { diff --git a/ui-tui-opentui-v2/src/view/statusBar.tsx b/ui-tui-opentui-v2/src/view/statusBar.tsx index 1cc13c6c255..281ddabb37b 100644 --- a/ui-tui-opentui-v2/src/view/statusBar.tsx +++ b/ui-tui-opentui-v2/src/view/statusBar.tsx @@ -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 }) { {effort()} - {/* 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. */} {' │ '} - {ctxBar(pct()!, CTX_BAR_CELLS)} + {ctxBar(pct() ?? 0, CTX_BAR_CELLS)} {` ${pct()}%`} diff --git a/ui-tui-opentui-v2/src/view/toolPart.tsx b/ui-tui-opentui-v2/src/view/toolPart.tsx index 5dc1a4c4d6f..bbdac8949a5 100644 --- a/ui-tui-opentui-v2/src/view/toolPart.tsx +++ b/ui-tui-opentui-v2/src/view/toolPart.tsx @@ -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 }) { - {` · ${fmtDuration(props.part.duration!)}`} + {` · ${fmtDuration(props.part.duration ?? 0)}`} 1}> {` (${lines().length} lines)`} diff --git a/ui-tui-opentui-v2/tsconfig.json b/ui-tui-opentui-v2/tsconfig.json index b3b8db287ca..b0985fae5b6 100644 --- a/ui-tui-opentui-v2/tsconfig.json +++ b/ui-tui-opentui-v2/tsconfig.json @@ -13,6 +13,8 @@ "noUncheckedIndexedAccess": true, "noFallthroughCasesInSwitch": true, "noImplicitOverride": true, + "noUnusedLocals": true, + "noImplicitReturns": true, "jsx": "preserve", "jsxImportSource": "@opentui/solid", "types": ["bun"],