fix(tui): help overlay crash + Main header staying rendered
* HelpOverlay used `key={key}` on each shortcut row, but several keys
legitimately repeat across contexts (`[t]` for Git view AND
Toggle engine pause; `[r]` for Refresh stats AND Refresh agent
detail). React threw "Encountered two children with the same
key" and the overlay crashed. Switch to index-based keys with a
comment explaining each row is unique by position.
* Refresh the help text: unified header tabs, Settings/Files/
Agents ←/→ pane swap, Git push/fetch shortcuts, Files hidden-
files toggle, Logs G jump-to-end.
* Main (status mode) header sometimes vanished after a tmux pane
switch and only resize fixed it. Two fixes:
- Remove the `rows < 10` auto-hide in MainHeader. tmux pane
switches can briefly report stale or zero dimensions, and the
transient `return null` orphaned the header.
- Wrap MainHeader and StatusBar in flexShrink={0} boxes inside
StatusModeGrid/StatusModeSingle (matching the prior
InteractiveMode fix) so Yoga can't squeeze them to 0 rows.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
10
.changeset/help-and-header-fixes.md
Normal file
10
.changeset/help-and-header-fixes.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
"runfusion.ai": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
TUI fixes: help overlay no longer crashes, header stays rendered
|
||||||
|
|
||||||
|
- Help overlay (`?` / `h`) crashed with "Encountered two children with the same key" because several shortcut entries share the same display key (`[t]` for Git view AND for Toggle engine pause; `[r]` for Refresh stats AND Refresh agent detail). Switch to index-based keys — each row is unique by position, not by character.
|
||||||
|
- Refresh the help text to reflect the unified header (`[m]` Main, `[b/a/g/t/e]` views), the Settings/Files/Agents `←/→` pane swap, the Git push/fetch shortcuts, the Files hidden-files toggle, and the Logs `G` jump-to-end.
|
||||||
|
- Main view (status mode) header sometimes vanished after a tmux pane switch and stayed missing until a terminal resize. Two fixes: (a) drop the `rows < 10` auto-hide in `MainHeader` — tmux pane switches can briefly report stale or zero dimensions, and a transient `return null` was orphaning the header. (b) Wrap `MainHeader` and `StatusBar` in `flexShrink={0}` boxes inside `StatusModeGrid` and `StatusModeSingle` (matching the prior fix in `InteractiveMode`), so Yoga can't squeeze them to 0 rows when content pressures the row budget.
|
||||||
@@ -615,30 +615,32 @@ function UtilitiesPanel({ isFocused }: { isFocused: boolean }) {
|
|||||||
|
|
||||||
function HelpOverlay() {
|
function HelpOverlay() {
|
||||||
const shortcuts: Array<[string, string]> = [
|
const shortcuts: Array<[string, string]> = [
|
||||||
["[b]", "Board view (interactive mode)"],
|
["[m] / [s]", "Main (status mode)"],
|
||||||
|
["[b]", "Board view"],
|
||||||
["[a]", "Agents view"],
|
["[a]", "Agents view"],
|
||||||
["[g]", "Settings view"],
|
["[g]", "Settings view"],
|
||||||
["[t]", "Git view"],
|
["[t]", "Git view"],
|
||||||
["[s]", "Status mode"],
|
["[e]", "Explorer (file browser)"],
|
||||||
["[1] / [2] / [3]", "Board / Agents / Settings (interactive)"],
|
["[Tab]", "Cycle focused panel / pane forward"],
|
||||||
["[Tab]", "Cycle focused panel forward"],
|
["[Shift+Tab]", "Cycle focused panel / pane backward"],
|
||||||
["[Shift+Tab]", "Cycle focused panel backward"],
|
["[1-5]", "Jump to panel (Main: System/Logs/Utilities/Stats/Settings)"],
|
||||||
["[1-5]", "Jump to panel by number (status mode)"],
|
["[← / →]", "Switch pane (Agents, Settings, Files, Git)"],
|
||||||
["[→] / [n]", "Next panel (status mode)"],
|
["[→] / [n]", "Next panel (Main)"],
|
||||||
["[←] / [p]", "Previous panel (status mode)"],
|
["[←] / [p]", "Previous panel (Main)"],
|
||||||
["[r]", "Refresh stats (Utilities)"],
|
["[r]", "Refresh stats (Utilities)"],
|
||||||
["[c]", "Clear logs (Utilities)"],
|
["[c]", "Clear logs (Utilities)"],
|
||||||
["[t]", "Toggle engine pause (Utilities)"],
|
["[↑/↓/k/j]", "Navigate list / log entries"],
|
||||||
["[↑/↓/k/j]", "Navigate log entries (Logs)"],
|
["[Home / G]", "First / last log entry (Logs)"],
|
||||||
["[Home/End]", "First/last log entry (Logs)"],
|
["[Enter/Space]", "Expand log entry (Logs)"],
|
||||||
["[Enter/Space/e]", "Expand log entry (Logs)"],
|
["[w]", "Toggle word wrap (Logs / Files)"],
|
||||||
["[w]", "Toggle word wrap (Logs)"],
|
["[f]", "Cycle severity filter (Main, any panel)"],
|
||||||
["[f]", "Cycle severity filter (Logs)"],
|
["[Space]", "Toggle boolean (Settings)"],
|
||||||
["[s/x]", "Start/stop agent (Agents view)"],
|
["[+/-]", "Adjust number (Settings)"],
|
||||||
["[D]", "Delete agent — requires confirm (Agents view)"],
|
["[p]", "Project picker (Board, Files)"],
|
||||||
["[r]", "Refresh agent detail (Agents view)"],
|
["[n]", "New task (Board)"],
|
||||||
["[Space]", "Toggle boolean (Settings view)"],
|
["[D]", "Delete agent — requires confirm (Agents)"],
|
||||||
["[+/-]", "Adjust number (Settings view)"],
|
["[P] / [F]", "Push / fetch (Git)"],
|
||||||
|
["[.]", "Toggle hidden files (Files)"],
|
||||||
["[?] / [h]", "Toggle help"],
|
["[?] / [h]", "Toggle help"],
|
||||||
["[q]", "Quit"],
|
["[q]", "Quit"],
|
||||||
["[Ctrl+C]", "Force quit"],
|
["[Ctrl+C]", "Force quit"],
|
||||||
@@ -653,11 +655,14 @@ function HelpOverlay() {
|
|||||||
<Box borderStyle="round" borderColor="cyanBright" flexDirection="column" backgroundColor="black">
|
<Box borderStyle="round" borderColor="cyanBright" flexDirection="column" backgroundColor="black">
|
||||||
<Text backgroundColor="black" bold color="white">{titleRow}</Text>
|
<Text backgroundColor="black" bold color="white">{titleRow}</Text>
|
||||||
<Text backgroundColor="black"> </Text>
|
<Text backgroundColor="black"> </Text>
|
||||||
{shortcuts.map(([key, desc]) => {
|
{shortcuts.map(([key, desc], i) => {
|
||||||
const keyCell = ` ${key.padEnd(rowKeyWidth - 1)} `;
|
const keyCell = ` ${key.padEnd(rowKeyWidth - 1)} `;
|
||||||
const descCell = ` ${desc.padEnd(rowDescWidth)} `;
|
const descCell = ` ${desc.padEnd(rowDescWidth)} `;
|
||||||
|
// Some shortcut keys repeat across contexts (e.g. [t] for Git view
|
||||||
|
// and [t] for Toggle engine pause), so index-based keys are correct
|
||||||
|
// here — each row is genuinely unique by position, not by key char.
|
||||||
return (
|
return (
|
||||||
<Box key={key} flexDirection="row">
|
<Box key={i} flexDirection="row">
|
||||||
<Text backgroundColor="black" color="yellow">{keyCell}</Text>
|
<Text backgroundColor="black" color="yellow">{keyCell}</Text>
|
||||||
<Text backgroundColor="black" color="white">{descCell}</Text>
|
<Text backgroundColor="black" color="white">{descCell}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -686,7 +691,9 @@ function StatusModeGrid({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" flexGrow={1}>
|
<Box flexDirection="column" flexGrow={1}>
|
||||||
<MainHeader state={state} />
|
<Box flexShrink={0}>
|
||||||
|
<MainHeader state={state} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Box flexDirection="row" flexGrow={1} overflow="hidden">
|
<Box flexDirection="row" flexGrow={1} overflow="hidden">
|
||||||
<Box flexDirection="column" flexGrow={5} overflow="hidden">
|
<Box flexDirection="column" flexGrow={5} overflow="hidden">
|
||||||
@@ -710,7 +717,9 @@ function StatusModeGrid({
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<StatusBar state={state} controller={controller} />
|
<Box flexShrink={0}>
|
||||||
|
<StatusBar state={state} controller={controller} />
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -736,11 +745,15 @@ function StatusModeSingle({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" flexGrow={1}>
|
<Box flexDirection="column" flexGrow={1}>
|
||||||
<MainHeader state={state} />
|
<Box flexShrink={0}>
|
||||||
|
<MainHeader state={state} />
|
||||||
|
</Box>
|
||||||
<Box flexGrow={1} flexDirection="column" overflow="hidden">
|
<Box flexGrow={1} flexDirection="column" overflow="hidden">
|
||||||
{activePanel()}
|
{activePanel()}
|
||||||
</Box>
|
</Box>
|
||||||
<StatusBar state={state} controller={controller} />
|
<Box flexShrink={0}>
|
||||||
|
<StatusBar state={state} controller={controller} />
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -780,7 +793,6 @@ function MainHeader({ state }: { state: DashboardState }) {
|
|||||||
const interactiveView = state.interactiveView;
|
const interactiveView = state.interactiveView;
|
||||||
const { stdout } = useStdout();
|
const { stdout } = useStdout();
|
||||||
const cols = stdout?.columns ?? 80;
|
const cols = stdout?.columns ?? 80;
|
||||||
const rows = stdout?.rows ?? 24;
|
|
||||||
// Single unified tab strip. "Main" is the status mode; the rest are
|
// Single unified tab strip. "Main" is the status mode; the rest are
|
||||||
// interactive views. Active key matches the current mode/view.
|
// interactive views. Active key matches the current mode/view.
|
||||||
type Tab =
|
type Tab =
|
||||||
@@ -794,7 +806,10 @@ function MainHeader({ state }: { state: DashboardState }) {
|
|||||||
{ key: "t", label: "Git", kind: "interactive", view: "git" },
|
{ key: "t", label: "Git", kind: "interactive", view: "git" },
|
||||||
{ key: "e", label: "Explorer", kind: "interactive", view: "files" },
|
{ key: "e", label: "Explorer", kind: "interactive", view: "files" },
|
||||||
];
|
];
|
||||||
if (rows < 10) return null;
|
// Don't gate header rendering on stdout.rows — tmux pane switches and
|
||||||
|
// other resize events can briefly report stale or zero dimensions, and a
|
||||||
|
// transient `return null` orphans the header on the next layout pass.
|
||||||
|
// Always render; Yoga/overflow:hidden handles the extreme cases.
|
||||||
// Width tiers, measured against actual rendered content with 6 tabs:
|
// Width tiers, measured against actual rendered content with 6 tabs:
|
||||||
// * Full + help (cols >= 110): "[k] Label" tabs + help/quit hint.
|
// * Full + help (cols >= 110): "[k] Label" tabs + help/quit hint.
|
||||||
// * Full (90-109): "[k] Label" tabs, no help hint (~85 chars).
|
// * Full (90-109): "[k] Label" tabs, no help hint (~85 chars).
|
||||||
|
|||||||
Reference in New Issue
Block a user