feat(tui): git view, project-scoped task stats, polished layout/theme

- Add a Git interactive view (hotkey [t]/[4]): branch + ahead/behind,
  recent commits with detail strip, staged/unstaged/untracked files
  panel, branches list, worktrees panel, and a [P] push modal with
  pre-flight commit list and capture of stdout/stderr. ←→ cycles
  status → branches → worktrees → commits → changes; ↑↓ navigates
  rows in the focused list. 5s poll while mounted.
- Project-scoped task stats: BoardView pushes its selected project
  path into the controller; refreshTUIStats now reads from a per-
  project TaskStore via a shared getProjectStore helper. Project
  switch triggers an immediate refresh via onBoardScopeChange.
- Unified MainHeader used by status and interactive modes — section
  tabs ([1]–[5]) and interactive view tabs ([b]/[a]/[g]/[t]) always
  visible; previously the interactive header replaced the main one.
- Logs panel keeps its size when expanding a single entry.
- 'f' cycles severity filter from any panel in status mode.
- BoardView: explicit cross-view shortcuts so g/a/t always switch
  views regardless of input-handler ordering.
- BoardView column overlap fix: flexShrink={0} on structural rows.
- TaskCard always shows the title with wrap="wrap"; single short-id
  pill, no duplicate id-as-title fallback.
- Stats panel reorg: StatRow helper, bold section headers, narrow-
  width wrap for Heap and Memory trailing fragments.
- Lighter blue palette: cyanBright fg accents / cyan active bg /
  logo gradient whiteBright → white → cyanBright → cyan → blue.
- System stats sampler: RSS, heap (V8 limit-aware color), external,
  CPU%, load avg, system used/free. Heap thresholds scale off
  --max-old-space-size automatically.
- Test fixture updated for the new InteractiveData.git block.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-24 23:32:45 -07:00
parent 8c9dfc2f56
commit 07d7bac165
10 changed files with 1431 additions and 212 deletions

View File

@@ -63,6 +63,34 @@ function makeInteractiveData(opts: {
getSettings: async () => settings,
updateSettings: async (_partial: Partial<SettingsValues>) => {},
listModels: () => models,
git: {
getStatus: async () => ({
branch: "main",
detached: false,
ahead: 0,
behind: 0,
staged: [],
unstaged: [],
untracked: [],
remoteUrl: "",
lastFetchAt: null,
}),
listCommits: async () => [],
showCommit: async () => ({
sha: "",
shortSha: "",
subject: "",
authorName: "",
relativeTime: "",
isoTime: "",
body: "",
stat: "",
}),
listBranches: async () => [],
listWorktrees: async () => [],
push: async () => ({ success: true, output: "" }),
fetch: async () => ({ success: true, output: "" }),
},
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -34,6 +34,10 @@ export class DashboardTUI {
systemInfo: SystemInfo | null = null;
taskStats: TaskStats | null = null;
systemStats: SystemStats | null = null;
// When set, dashboard.ts refreshes task stats from this project path
// instead of the launch cwd. Mirrors BoardView's selected project.
boardScopedProjectPath: string | null = null;
private boardScopeListener: ((path: string | null) => void) | null = null;
settings: SettingsValues | null = null;
callbacks: TUICallbacks | null = null;
isRunning = false;
@@ -132,6 +136,20 @@ export class DashboardTUI {
this.notify();
}
setBoardScopedProjectPath(path: string | null): void {
if (this.boardScopedProjectPath === path) return;
this.boardScopedProjectPath = path;
this.boardScopeListener?.(path);
this.notify();
}
onBoardScopeChange(listener: (path: string | null) => void): () => void {
this.boardScopeListener = listener;
return () => {
if (this.boardScopeListener === listener) this.boardScopeListener = null;
};
}
/** Sample process memory + CPU% in-place. Called from the sampler timer. */
sampleSystemStats(): void {
const mem = process.memoryUsage();

View File

@@ -17,4 +17,9 @@ export type {
InteractiveData,
ProjectItem,
TaskItem,
GitStatus,
GitCommit,
GitCommitDetail,
GitBranch,
GitWorktree,
} from "./state.js";

View File

@@ -8,7 +8,7 @@ export type SectionId = "logs" | "system" | "utilities" | "stats" | "settings";
export type AppMode = "status" | "interactive";
export type InteractiveView = "board" | "agents" | "settings";
export type InteractiveView = "board" | "agents" | "settings" | "git";
export interface SystemInfo {
host: string;
@@ -124,6 +124,50 @@ export interface ModelItem {
contextWindow: number;
}
// ── Git view types ────────────────────────────────────────────────────────────
export interface GitStatus {
branch: string;
detached: boolean;
ahead: number;
behind: number;
staged: Array<{ status: string; path: string }>;
unstaged: Array<{ status: string; path: string }>;
untracked: Array<{ path: string }>;
remoteUrl: string;
lastFetchAt: number | null;
}
export interface GitCommit {
sha: string;
shortSha: string;
subject: string;
authorName: string;
relativeTime: string;
isoTime: string;
}
export interface GitCommitDetail extends GitCommit {
body: string;
stat: string;
}
export interface GitBranch {
name: string;
shortSha: string;
relativeTime: string;
isCurrent: boolean;
upstreamTrack: string;
}
export interface GitWorktree {
path: string;
branch: string;
sha: string;
isCurrent: boolean;
isLocked: boolean;
}
export interface InteractiveData {
listProjects: () => Promise<ProjectItem[]>;
listTasks: (projectPath: string) => Promise<TaskItem[]>;
@@ -135,6 +179,15 @@ export interface InteractiveData {
getSettings: () => Promise<SettingsValues>;
updateSettings: (partial: Partial<SettingsValues>) => Promise<void>;
listModels: () => ModelItem[];
git: {
getStatus: (projectPath: string) => Promise<GitStatus>;
listCommits: (projectPath: string, limit?: number) => Promise<GitCommit[]>;
showCommit: (projectPath: string, sha: string) => Promise<GitCommitDetail>;
listBranches: (projectPath: string) => Promise<GitBranch[]>;
listWorktrees: (projectPath: string) => Promise<GitWorktree[]>;
push: (projectPath: string) => Promise<{ success: boolean; output: string }>;
fetch: (projectPath: string) => Promise<{ success: boolean; output: string }>;
};
}
// ── Dashboard state (mutable, shared between controller and App) ───────────────