diff --git a/ui-tui/babel.compiler.config.cjs b/ui-tui/babel.compiler.config.cjs index b81ff95485..ab41a82e2b 100644 --- a/ui-tui/babel.compiler.config.cjs +++ b/ui-tui/babel.compiler.config.cjs @@ -26,7 +26,7 @@ module.exports = { ], // We feed already-compiled JS into babel; don't re-parse as TS/JSX. // @babel/preset-env etc. would over-transform — the compiler is our only - // transform here. - babelrc: false, - configFile: false + // transform here. babelrc:false stops @babel/cli from walking up the + // filesystem looking for other configs (the parent repo might add one). + babelrc: false } diff --git a/ui-tui/eslint.config.mjs b/ui-tui/eslint.config.mjs index 1b20c3244f..4452f49fa5 100644 --- a/ui-tui/eslint.config.mjs +++ b/ui-tui/eslint.config.mjs @@ -3,6 +3,7 @@ import typescriptEslint from '@typescript-eslint/eslint-plugin' import typescriptParser from '@typescript-eslint/parser' import perfectionist from 'eslint-plugin-perfectionist' import reactPlugin from 'eslint-plugin-react' +import reactCompiler from 'eslint-plugin-react-compiler' import hooksPlugin from 'eslint-plugin-react-hooks' import unusedImports from 'eslint-plugin-unused-imports' import globals from 'globals' @@ -43,6 +44,7 @@ export default [ 'custom-rules': customRules, perfectionist, react: reactPlugin, + 'react-compiler': reactCompiler, 'react-hooks': hooksPlugin, 'unused-imports': unusedImports }, @@ -53,6 +55,12 @@ export default [ '@typescript-eslint/no-unused-vars': 'off', 'no-undef': 'off', 'no-unused-vars': 'off', + // React Compiler: warn (not error) so the gate doesn't block merges + // while we migrate. Flags patterns that would break the compiler at + // runtime (mutating refs during render, non-PascalCase components, + // etc.). See audit §5 — we run the compiler in `npm run build` as a + // post-pass over tsc's `dist/` output. + 'react-compiler/react-compiler': 'warn', 'padding-line-between-statements': [ 1, { blankLine: 'always', next: ['block-like', 'block', 'return', 'if', 'class', 'continue', 'debugger', 'break', 'multiline-const', 'multiline-let'], prev: '*' }, @@ -89,6 +97,9 @@ export default [ 'no-constant-condition': 'off', 'no-empty': 'off', 'no-redeclare': 'off', + // Ink internals: reconciler, style pool, DOM node impl — full of + // intentional side effects the compiler rules reject. + 'react-compiler/react-compiler': 'off', 'react-hooks/exhaustive-deps': 'off' } }, diff --git a/ui-tui/package.json b/ui-tui/package.json index 4a16c9c3a3..061e3bc448 100644 --- a/ui-tui/package.json +++ b/ui-tui/package.json @@ -6,7 +6,8 @@ "scripts": { "dev": "npm run build --prefix packages/hermes-ink && tsx --watch src/entry.tsx", "start": "tsx src/entry.tsx", - "build": "npm run build --prefix packages/hermes-ink && tsc -p tsconfig.build.json && chmod +x dist/entry.js", + "build": "npm run build --prefix packages/hermes-ink && tsc -p tsconfig.build.json && npm run build:compile && chmod +x dist/entry.js", + "build:compile": "babel dist --out-dir dist --config-file ./babel.compiler.config.cjs --extensions .js --keep-file-extension", "type-check": "tsc --noEmit -p tsconfig.json", "lint": "eslint src/ packages/", "lint:fix": "eslint src/ packages/ --fix", diff --git a/ui-tui/src/components/todoPanel.tsx b/ui-tui/src/components/todoPanel.tsx index 567050a39d..8b5b59b6a4 100644 --- a/ui-tui/src/components/todoPanel.tsx +++ b/ui-tui/src/components/todoPanel.tsx @@ -1,5 +1,5 @@ import { Box, Text } from '@hermes/ink' -import { memo } from 'react' +import { memo, useState } from 'react' import { countPendingTodos } from '../lib/liveProgress.js' import { todoGlyph, todoTone } from '../lib/todo.js' @@ -13,7 +13,7 @@ const rowColor = (t: Theme, status: TodoItem['status']) => { } export const TodoPanel = memo(function TodoPanel({ - collapsed = false, + collapsed, incomplete = false, onToggle, t, @@ -25,6 +25,25 @@ export const TodoPanel = memo(function TodoPanel({ t: Theme todos: TodoItem[] }) { + // Fallback local state for archived todos in transcript where there's no + // external controller. Live TodoPanel passes collapsed+onToggle from the + // turn store so clicks still work there. + const [localCollapsed, setLocalCollapsed] = useState(false) + const isControlled = typeof collapsed === 'boolean' + const effectiveCollapsed = isControlled ? collapsed : localCollapsed + + const handleToggle = () => { + if (onToggle) { + onToggle() + + return + } + + if (!isControlled) { + setLocalCollapsed(v => !v) + } + } + if (!todos.length) { return null } @@ -34,9 +53,9 @@ export const TodoPanel = memo(function TodoPanel({ return ( - + - {collapsed ? '▸ ' : '▾ '} + {effectiveCollapsed ? '▸ ' : '▾ '} Todo {' '} @@ -52,7 +71,7 @@ export const TodoPanel = memo(function TodoPanel({ - {!collapsed && ( + {!effectiveCollapsed && ( {todos.map(todo => { const tone = todoTone(todo.status)