feat(cli): merge @fusion/tui into fn dashboard with Ink TUI

Replace the legacy ANSI-based DashboardTUI with an Ink/React rewrite
under packages/cli/src/commands/dashboard-tui/, delete the standalone
@fusion/tui package, and make `fn` (no args) launch the dashboard.

The new TUI keeps the existing 5-panel status mode (system, logs,
utilities, stats, settings) but adds an interactive mode (b/a/g) with
three views: a kanban board with project picker and per-task detail,
an agents list+detail with state management, and a settings editor.
Bordered focus-aware panels, solid-background help overlay, static
all-blue FUSION splash that adapts to small terminals. DashboardTUI
and DashboardLogSink public API are unchanged so dashboard.ts only
needed import-path updates plus interactiveData/loadingStatus wiring.

Also adds zod to @fusion/dashboard to satisfy a peer dep introduced
by pi-coding-agent 0.70.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-24 18:52:03 -07:00
parent bdcb048e20
commit a283ef2b79
46 changed files with 5578 additions and 6239 deletions

View File

@@ -0,0 +1,54 @@
import { useState, useEffect } from "react";
import type { ProjectItem, TaskItem, InteractiveData } from "../state.js";
export interface ProjectsState {
projects: ProjectItem[];
loading: boolean;
error: string | null;
}
export interface TasksState {
tasks: TaskItem[];
loading: boolean;
error: string | null;
}
export function useProjects(interactiveData: InteractiveData | null): ProjectsState {
const [state, setState] = useState<ProjectsState>({ projects: [], loading: false, error: null });
useEffect(() => {
if (!interactiveData) return;
setState({ projects: [], loading: true, error: null });
interactiveData.listProjects().then((projects) => {
setState({ projects, loading: false, error: null });
}).catch((err: unknown) => {
const message = err instanceof Error ? err.message : String(err);
setState({ projects: [], loading: false, error: message });
});
}, [interactiveData]);
return state;
}
export function useTasks(
interactiveData: InteractiveData | null,
selectedProject: ProjectItem | null,
): TasksState {
const [state, setState] = useState<TasksState>({ tasks: [], loading: false, error: null });
useEffect(() => {
if (!interactiveData || !selectedProject) {
setState({ tasks: [], loading: false, error: null });
return;
}
setState({ tasks: [], loading: true, error: null });
interactiveData.listTasks(selectedProject.path).then((tasks) => {
setState({ tasks, loading: false, error: null });
}).catch((err: unknown) => {
const message = err instanceof Error ? err.message : String(err);
setState({ tasks: [], loading: false, error: message });
});
}, [interactiveData, selectedProject]);
return state;
}