feat(FN-2311): default dashboard TUI to system section
- Reorder dashboard TUI sections so System is tab [1] and the initial active view - Update header rendering across wide, medium, and narrow terminal layouts to reflect the new tab order - Expand dashboard TUI tests to assert system-first labels and default active section behavior - Document system-first startup behavior in CLI reference and add a patch changeset for @runfusion/fusion
This commit is contained in:
5
.changeset/dashboard-tui-status-first.md
Normal file
5
.changeset/dashboard-tui-status-first.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
`fn dashboard` TTY mode now opens on the System tab first so users immediately see host, port, URL, and auth token access details.
|
||||||
@@ -70,12 +70,15 @@ interactive TUI with five sections:
|
|||||||
|
|
||||||
| Section | Description |
|
| Section | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **Logs** | Real-time log entries with timestamps and severity levels |
|
|
||||||
| **System** | Host, port, URL, auth mode, token, engine status, uptime |
|
| **System** | Host, port, URL, auth mode, token, engine status, uptime |
|
||||||
|
| **Logs** | Real-time log entries with timestamps and severity levels |
|
||||||
| **Utilities** | Actions: refresh stats, clear logs, toggle engine pause |
|
| **Utilities** | Actions: refresh stats, clear logs, toggle engine pause |
|
||||||
| **Stats** | Task counts by column, active task count, agent state counts |
|
| **Stats** | Task counts by column, active task count, agent state counts |
|
||||||
| **Settings** | Key settings from the task store |
|
| **Settings** | Key settings from the task store |
|
||||||
|
|
||||||
|
On startup, the TUI opens on the **System** section by default so you can
|
||||||
|
immediately see host/port and access-token details.
|
||||||
|
|
||||||
**Keyboard Navigation:**
|
**Keyboard Navigation:**
|
||||||
|
|
||||||
| Key | Action |
|
| Key | Action |
|
||||||
|
|||||||
@@ -166,26 +166,34 @@ describe("renderHeaderToString", () => {
|
|||||||
it("renders correctly at wide terminal width (>= 70 cols)", () => {
|
it("renders correctly at wide terminal width (>= 70 cols)", () => {
|
||||||
const header = renderHeaderToString(80);
|
const header = renderHeaderToString(80);
|
||||||
expect(header).toContain("fusion");
|
expect(header).toContain("fusion");
|
||||||
expect(header).toContain("Logs");
|
expect(header).toContain("[1] System");
|
||||||
expect(header).toContain("System");
|
expect(header).toContain("[2] Logs");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders correctly at medium terminal width (>= 40 cols)", () => {
|
it("renders correctly at medium terminal width (>= 40 cols)", () => {
|
||||||
const header = renderHeaderToString(50);
|
const header = renderHeaderToString(50);
|
||||||
expect(header).toContain("fusion");
|
expect(header).toContain("fusion");
|
||||||
expect(header).toContain("[1]L"); // Short label for Logs
|
expect(header).toContain("[1]S"); // Short label for System
|
||||||
|
expect(header).toContain("[2]L"); // Short label for Logs
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders correctly at narrow terminal width (< 40 cols)", () => {
|
it("renders correctly at narrow terminal width (< 40 cols)", () => {
|
||||||
const header = renderHeaderToString(30);
|
const header = renderHeaderToString(30);
|
||||||
expect(header).toContain("fusion");
|
expect(header).toContain("fusion");
|
||||||
expect(header).toContain("Logs");
|
expect(header).toContain("[1]System");
|
||||||
expect(header).toContain("[n/p]nav");
|
expect(header).toContain("[n/p]nav");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Type exports verification ─────────────────────────────────────────────
|
// ── Type exports verification ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("DashboardTUI default section", () => {
|
||||||
|
it("starts on the system section by default", () => {
|
||||||
|
const tui = new DashboardTUI();
|
||||||
|
expect((tui as any).activeSection).toBe("system");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("Type exports", () => {
|
describe("Type exports", () => {
|
||||||
it("exports LogEntry type", () => {
|
it("exports LogEntry type", () => {
|
||||||
const entry = {
|
const entry = {
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
* Dashboard TUI Renderer
|
* Dashboard TUI Renderer
|
||||||
*
|
*
|
||||||
* An interactive terminal user interface for `fn dashboard` with five sections:
|
* An interactive terminal user interface for `fn dashboard` with five sections:
|
||||||
* - logs: real-time log entries with ring buffer
|
|
||||||
* - system: host, port, URL, auth mode, token, engine mode
|
* - system: host, port, URL, auth mode, token, engine mode
|
||||||
|
* - logs: real-time log entries with ring buffer
|
||||||
* - utilities: action list with keybindings
|
* - utilities: action list with keybindings
|
||||||
* - stats: task counts, active task count, agent state counts
|
* - stats: task counts, active task count, agent state counts
|
||||||
* - settings: real key/value settings from TaskStore
|
* - settings: real key/value settings from TaskStore
|
||||||
@@ -257,10 +257,10 @@ function visibleTruncate(text: string, maxWidth: number): string {
|
|||||||
|
|
||||||
// ── Dashboard TUI Renderer ───────────────────────────────────────────────────
|
// ── Dashboard TUI Renderer ───────────────────────────────────────────────────
|
||||||
|
|
||||||
const SECTION_ORDER: SectionId[] = ["logs", "system", "utilities", "stats", "settings"];
|
const SECTION_ORDER: SectionId[] = ["system", "logs", "utilities", "stats", "settings"];
|
||||||
|
|
||||||
export class DashboardTUI {
|
export class DashboardTUI {
|
||||||
private activeSection: SectionId = "logs";
|
private activeSection: SectionId = "system";
|
||||||
private logBuffer: LogRingBuffer;
|
private logBuffer: LogRingBuffer;
|
||||||
private systemInfo: SystemInfo | null = null;
|
private systemInfo: SystemInfo | null = null;
|
||||||
private taskStats: TaskStats | null = null;
|
private taskStats: TaskStats | null = null;
|
||||||
@@ -684,8 +684,8 @@ export class DashboardTUI {
|
|||||||
process.stdout.write(title);
|
process.stdout.write(title);
|
||||||
|
|
||||||
// Responsive header modes based on terminal width:
|
// Responsive header modes based on terminal width:
|
||||||
// - Wide (>= 70 cols): Full labels "[1] Logs [2] System [3] Utilities [4] Stats [5] Settings"
|
// - Wide (>= 70 cols): Full labels "[1] System [2] Logs [3] Utilities [4] Stats [5] Settings"
|
||||||
// - Medium (>= 40 cols): Short labels "[1] L [2] S [3] U [4] St [5] Se"
|
// - Medium (>= 40 cols): Short labels "[1] S [2] L [3] U [4] St [5] Se"
|
||||||
// - Narrow (< 40 cols): Only active tab with navigation hint "[n/p] Previous/Next"
|
// - Narrow (< 40 cols): Only active tab with navigation hint "[n/p] Previous/Next"
|
||||||
|
|
||||||
if (cols >= 70) {
|
if (cols >= 70) {
|
||||||
@@ -1331,7 +1331,7 @@ export function renderHeaderToString(cols: number): string {
|
|||||||
if (cols >= 70) {
|
if (cols >= 70) {
|
||||||
for (let i = 0; i < SECTION_ORDER.length; i++) {
|
for (let i = 0; i < SECTION_ORDER.length; i++) {
|
||||||
const section = SECTION_ORDER[i];
|
const section = SECTION_ORDER[i];
|
||||||
const isActive = section === "logs"; // Default active section for test
|
const isActive = section === "system"; // Default active section for test
|
||||||
const num = (i + 1).toString();
|
const num = (i + 1).toString();
|
||||||
const label = section.charAt(0).toUpperCase() + section.slice(1);
|
const label = section.charAt(0).toUpperCase() + section.slice(1);
|
||||||
const tabText = `[${num}] ${label}`;
|
const tabText = `[${num}] ${label}`;
|
||||||
@@ -1348,7 +1348,7 @@ export function renderHeaderToString(cols: number): string {
|
|||||||
};
|
};
|
||||||
for (let i = 0; i < SECTION_ORDER.length; i++) {
|
for (let i = 0; i < SECTION_ORDER.length; i++) {
|
||||||
const section = SECTION_ORDER[i];
|
const section = SECTION_ORDER[i];
|
||||||
const isActive = section === "logs";
|
const isActive = section === "system";
|
||||||
const num = (i + 1).toString();
|
const num = (i + 1).toString();
|
||||||
const shortLabel = shortLabels[section];
|
const shortLabel = shortLabels[section];
|
||||||
const tabText = `[${num}]${shortLabel}`;
|
const tabText = `[${num}]${shortLabel}`;
|
||||||
@@ -1356,7 +1356,7 @@ export function renderHeaderToString(cols: number): string {
|
|||||||
output += colorize(` ${tabText} `, style);
|
output += colorize(` ${tabText} `, style);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
output += colorize(" [1]Logs ", "brightBlue");
|
output += colorize(" [1]System ", "brightBlue");
|
||||||
output += colorize(" [n/p]nav ", "dim");
|
output += colorize(" [n/p]nav ", "dim");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
|
|||||||
//
|
//
|
||||||
// When both stdout and stdin are TTY, we activate the interactive TUI
|
// When both stdout and stdin are TTY, we activate the interactive TUI
|
||||||
// instead of plain console output. The TUI provides 5 sections:
|
// instead of plain console output. The TUI provides 5 sections:
|
||||||
// logs, system, utilities, stats, settings with keyboard navigation.
|
// system, logs, utilities, stats, settings with keyboard navigation.
|
||||||
//
|
//
|
||||||
// In non-TTY mode (CI, piped output), we fall back to plain console
|
// In non-TTY mode (CI, piped output), we fall back to plain console
|
||||||
// output to maintain compatibility with automated workflows.
|
// output to maintain compatibility with automated workflows.
|
||||||
|
|||||||
@@ -236,6 +236,11 @@ export class FirstRunDetector {
|
|||||||
* @returns Generated name
|
* @returns Generated name
|
||||||
*/
|
*/
|
||||||
async generateProjectName(projectPath: string): Promise<string> {
|
async generateProjectName(projectPath: string): Promise<string> {
|
||||||
|
// Fast path: avoid invoking git for non-repositories (prevents unnecessary delays in tests/startup).
|
||||||
|
if (!existsSync(join(projectPath, ".git"))) {
|
||||||
|
return basename(projectPath);
|
||||||
|
}
|
||||||
|
|
||||||
// Try git remote first
|
// Try git remote first
|
||||||
try {
|
try {
|
||||||
const { execFile } = await import("node:child_process");
|
const { execFile } = await import("node:child_process");
|
||||||
@@ -245,7 +250,7 @@ export class FirstRunDetector {
|
|||||||
const { stdout } = await execFileAsync(
|
const { stdout } = await execFileAsync(
|
||||||
"git",
|
"git",
|
||||||
["remote", "get-url", "origin"],
|
["remote", "get-url", "origin"],
|
||||||
{ cwd: projectPath, timeout: 5000 }
|
{ cwd: projectPath, timeout: 1000 }
|
||||||
);
|
);
|
||||||
|
|
||||||
const remoteUrl = stdout.trim();
|
const remoteUrl = stdout.trim();
|
||||||
|
|||||||
Reference in New Issue
Block a user