- Add ChatStore mock to all dashboard route tests that mock @fusion/core, since server.ts now instantiates ChatStore(store.getFusionDir(), ...) - Add getFusionDir to createMockStore in server.test.ts - Gate AI session cleanup scheduling behind shouldScheduleAiSessionCleanup() (returns false in test env) to prevent open handle warnings - Fix desktop tests: DASHBOARD_URL is now exported as a function alias, update assertions to call DASHBOARD_URL() instead of using as string - Add node:os mocks to system-metrics.test.ts for deterministic results - Replace hardcoded maxWorkers=16 with availableParallelism()-based calculation in all vitest configs to prevent OOM on 2-core CI runners - Add --workspace-concurrency=2 to pnpm test commands - Fix TaskCard tests: update mission badge title assertions to full titles - Remove unused /api/mesh/state route - Fix plugin-auto-label: add isError field, async onTaskCreated, "tests" keyword - Fix plugin-ci-status: add module-level logger, tighten test assertions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@fusion/tui
Terminal UI components for fn, built with Ink (React for the command line).
Status
This package is under active development and not yet published.
Terminal Compatibility
This package is designed for terminals with a minimum size of 80×24 characters:
- Minimum width: 80 columns
- Minimum height: 24 rows
When the terminal is smaller than these minimums, the UI enforces these bounds for layout calculations, ensuring consistent rendering across different terminal sizes.
Responsive Layout
The TUI provides responsive layout utilities that adapt to terminal dimensions:
- Minimum bounds: Layouts always respect the 80×24 minimum, ensuring readability
- Dynamic column widths: Tables and lists compute column widths based on available space
- Truncation with ellipsis: Long content is automatically truncated with
…(U+2026) when it exceeds the available width
Installation
This package is part of the fn workspace and is not installed separately. It is available as a private workspace package.
API Reference
FusionProvider
The FusionProvider component initializes a TaskStore and provides it via React context.
import { FusionProvider } from "@fusion/tui";
function App() {
return (
<FusionProvider>
<MyComponent />
</FusionProvider>
);
}
Props
| Prop | Type | Description |
|---|---|---|
projectDir |
string (optional) |
Explicit project directory override. When provided, skips auto-detection. |
children |
React.ReactNode |
Child components that will have access to the Fusion context. |
Behavior
- On mount, auto-detects the Fusion project by walking up from
process.cwd()looking for.fusion/fusion.db - If no project is found, renders a red error message
- On unmount, calls
store.close()to cleanly shut down the SQLite connection
useFusion
Hook to access the Fusion context. Must be used within a FusionProvider.
import { useFusion } from "@fusion/tui";
function TaskList() {
const { store, projectPath } = useFusion();
useEffect(() => {
store.listTasks().then((tasks) => {
// Render tasks...
});
}, [store]);
return <Text>Project: {projectPath}</Text>;
}
Returns
| Property | Type | Description |
|---|---|---|
store |
TaskStore |
The initialized TaskStore instance |
projectPath |
string |
Absolute path to the project directory |
Throws
Error if used outside of a FusionProvider.
detectProjectDir
Detect the Fusion project root directory by walking up from a starting path.
import { detectProjectDir } from "@fusion/tui";
// Find project from current directory
const projectPath = detectProjectDir();
// Find project from a specific directory
const projectPath = detectProjectDir("/Users/me/code/my-project/src");
Parameters
| Parameter | Type | Description |
|---|---|---|
startPath |
string (optional) |
Starting directory for the search (defaults to process.cwd()) |
Returns
The absolute path to the project root, or null if no project directory is detected.
ScreenRouter
The ScreenRouter component provides a keyboard-navigable tab bar for switching between application screens.
import { ScreenRouter } from "@fusion/tui";
function App() {
return (
<ScreenRouter>
{({ activeScreen }) => (
<>
{activeScreen === "board" && <BoardScreen />}
{activeScreen === "detail" && <DetailScreen />}
{activeScreen === "activity" && <ActivityScreen />}
{activeScreen === "agents" && <AgentsScreen />}
{activeScreen === "settings" && <SettingsScreen />}
</>
)}
</ScreenRouter>
);
}
Available Screens
The router manages five screens in this order:
| Index | Screen ID | Label | Shortcut |
|---|---|---|---|
| 1 | board |
Board | 1 |
| 2 | detail |
Detail | 2 |
| 3 | activity |
Activity | 3 |
| 4 | agents |
Agents | 4 |
| 5 | settings |
Settings | 5 |
Keyboard Navigation
| Key | Action |
|---|---|
1 - 5 |
Jump directly to the corresponding tab |
Tab |
Cycle forward through tabs (wraps from end to start) |
Shift+Tab |
Cycle backward through tabs (wraps from start to end) |
Tab Bar Rendering
The tab bar displays all five tabs horizontally with:
- Active tab highlighted with bold text, cyan background, and black text
- Inactive tabs shown in white text
- A border line below the tab bar
Props
| Prop | Type | Description |
|---|---|---|
initialScreen |
ScreenId (optional) |
Initial screen to display on mount (default: "board") |
onScreenChange |
(screenId: ScreenId) => void (optional) |
Callback when user navigates to a different screen |
activeScreen |
ScreenId (optional) |
Externally controlled active screen |
children |
(props: ScreenComponentProps) => React.ReactNode |
Render function that receives activeScreen and returns the screen content |
ScreenComponentProps
| Property | Type | Description |
|---|---|---|
activeScreen |
ScreenId |
The currently active screen ID ("board" | "detail" | "activity" | "agents" | "settings") |
Exports
The following are exported from @fusion/tui:
ScreenRouter— The main router componentSCREENS— Array of screen definitions withid,label, andshortcutgetScreenById(id)— Get screen definition by IDgetScreenIndex(id)— Get screen index by IDtype ScreenId— Type for screen identifiers
Global Keyboard Shortcuts
The TUI provides centralized global keyboard shortcuts via the useGlobalShortcuts hook. Place this hook at the app root level to enable consistent shortcut handling across all screens.
import { useGlobalShortcuts, HelpOverlay } from "@fusion/tui";
function App() {
const { helpVisible, toggleHelp } = useGlobalShortcuts({
onScreenChange: setActiveScreen,
});
return (
<>
{helpVisible && <HelpOverlay onClose={toggleHelp} />}
<ScreenRouter ... />
</>
);
}
Available Shortcuts
| Key | Action | Focus Guard |
|---|---|---|
Ctrl+C |
Quit (emergency exit) | Always works |
q |
Quit | Only when no text input focused |
? |
Toggle help overlay | Only when no text input focused |
h |
Toggle help overlay (alternate) | Only when no text input focused |
1 - 5 |
Switch screens | Only when no text input focused |
Focus Guard
Global shortcuts (except Ctrl+C) are suppressed when text input is focused. This prevents accidental navigation while typing.
To enable focus guarding for text inputs, import FocusGuardRef and set isFocused on focus/blur events:
import { FocusGuardRef } from "@fusion/tui";
function TextInput() {
return (
<Input
onFocus={() => { FocusGuardRef.isFocused = true; }}
onBlur={() => { FocusGuardRef.isFocused = false; }}
/>
);
}
useGlobalShortcuts Hook
const result = useGlobalShortcuts({
onScreenChange: (screenId) => {
// Handle screen switch triggered by number keys
},
});
Options
| Property | Type | Description |
|---|---|---|
onScreenChange |
(screenId: ScreenId) => void (optional) |
Callback when user presses 1-5 to switch screens |
Returns
| Property | Type | Description |
|---|---|---|
helpVisible |
boolean |
Whether the help overlay is currently visible |
toggleHelp |
() => void |
Toggle the help overlay visibility |
hideHelp |
() => void |
Hide the help overlay |
HelpOverlay Component
The HelpOverlay component displays available keyboard shortcuts. It handles Escape and q to close.
<HelpOverlay onClose={toggleHelp} />
Props
| Property | Type | Description |
|---|---|---|
onClose |
() => void |
Callback to close the overlay |
Exports
The following are exported from @fusion/tui:
useGlobalShortcuts— Hook for handling global keyboard shortcutsHelpOverlay— Component for displaying keyboard shortcutsFocusGuardRef— Shared ref for tracking text input focus state
Example
import React, { useState } from "react";
import { render, Box } from "ink";
import { FusionProvider, useFusion, ScreenRouter, useGlobalShortcuts, HelpOverlay, ResponsiveHeader, ResponsiveTable, ResponsiveStatusBar } from "@fusion/tui";
function App() {
const { projectPath } = useFusion();
const [activeScreen, setActiveScreen] = useState("board");
// Global keyboard shortcuts
const { helpVisible, toggleHelp } = useGlobalShortcuts({
onScreenChange: setActiveScreen,
});
return (
<Box flexDirection="column">
{/* Help overlay */}
{helpVisible && (
<Box marginBottom={1}>
<HelpOverlay onClose={toggleHelp} />
</Box>
)}
{/* Responsive header */}
<ResponsiveHeader title={`Fusion TUI | Project: ${projectPath}`} />
{/* Screen router */}
<ScreenRouter
activeScreen={activeScreen}
onScreenChange={setActiveScreen}
>
{({ activeScreen }) => (
<Box flexDirection="column">
{activeScreen === "board" && (
<ResponsiveTable
columns={[
{ header: "ID", minWidth: 10 },
{ header: "Description", minWidth: 30, canGrow: true },
{ header: "Status", minWidth: 12 },
]}
rows={[
["FN-001", "Implement feature", "todo"],
["FN-002", "Fix bug in auth", "done"],
]}
/>
)}
{activeScreen === "detail" && (
<Box>
<Text>Detail Screen</Text>
</Box>
)}
</Box>
)}
</ScreenRouter>
{/* Responsive status bar */}
<ResponsiveStatusBar />
</Box>
);
}
render(
<FusionProvider>
<App />
</FusionProvider>
);
Responsive Layout Utilities
The TUI provides utilities for building responsive layouts that adapt to terminal dimensions.
useTerminalDimensions
Hook to read live terminal dimensions from Ink's useStdout() with minimum bounds applied.
import { useTerminalDimensions } from "@fusion/tui";
function MyComponent() {
const { columns, rows, isMinimumSize, extraColumns } = useTerminalDimensions();
return (
<Box>
<Text>Terminal: {columns}x{rows}</Text>
{!isMinimumSize && <Text dimColor> (wider than minimum)</Text>}
</Box>
);
}
Returns
| Property | Type | Description |
|---|---|---|
columns |
number |
Effective column count (minimum 80) |
rows |
number |
Effective row count (minimum 24) |
isMinimumSize |
boolean |
Whether terminal meets minimum size |
extraColumns |
number |
Extra columns beyond the 80-column minimum |
computeColumnLayout
Calculate column widths based on terminal dimensions and column definitions.
import { computeColumnLayout } from "@fusion/tui";
const layout = computeColumnLayout(120, [
{ minWidth: 10, canGrow: false }, // Fixed-width ID column
{ minWidth: 30, canGrow: true, growWeight: 2 }, // Description (grows 2x)
{ minWidth: 15, canGrow: true }, // Status (grows 1x)
]);
console.log(layout.widths); // e.g., [10, 63, 47]
console.log(layout.totalWidth); // 120
Parameters
| Parameter | Type | Description |
|---|---|---|
columns |
number |
Available terminal columns |
definitions |
ColumnDefinition[] |
Column definitions with minWidth, preferredWidth, canGrow, growWeight |
strategy |
ColumnStrategy |
Allocation strategy: "equal", "fixed", "proportional", "content-heavy" |
Returns
| Property | Type | Description |
|---|---|---|
widths |
number[] |
Calculated width for each column |
totalWidth |
number |
Total width used by all columns |
remainingColumns |
number |
Leftover columns after minimum allocations |
truncateText
Truncate text to a maximum width with ellipsis.
import { truncateText } from "@fusion/tui";
truncateText("Hello World", 8); // "Hello W…"
truncateText("Hi", 10); // "Hi" (fits)
truncateText("Hello", 2); // "…" (too short)
truncateText("Hello World", 10, "~~"); // "Hello Wo~~" (custom ellipsis)
Responsive Components
ResponsiveHeader
A header component that adapts to terminal width.
import { ResponsiveHeader } from "@fusion/tui";
<ResponsiveHeader title="My App" />
ResponsiveTable
A table component with responsive column widths and truncation.
import { ResponsiveTable } from "@fusion/tui";
<ResponsiveTable
columns={[
{ header: "ID", minWidth: 10 },
{ header: "Description", minWidth: 30, canGrow: true },
{ header: "Status", minWidth: 12 },
]}
rows={[
["FN-001", "Implement feature", "todo"],
["FN-002", "Fix bug in auth", "done"],
]}
/>
ResponsiveStatusBar
A status bar showing current terminal dimensions.
import { ResponsiveStatusBar } from "@fusion/tui";
<ResponsiveStatusBar />
// Displays: "Terminal: 120x40 | Minimum: 80x24"
Exports
The following are exported from @fusion/tui:
useTerminalDimensions— Hook for reading terminal dimensionscomputeColumnLayout— Function for calculating column widthstruncateText— Function for truncating text with ellipsisResponsiveHeader— Header component with responsive contentResponsiveTable— Table component with responsive columnsResponsiveTaskRow— Task row with truncationResponsiveStatusBar— Status bar showing terminal infoMIN_TERMINAL_COLUMNS— Minimum supported terminal width (80)MIN_TERMINAL_ROWS— Minimum supported terminal height (24)