feat(KB-171): propagate availableModels prop through dashboard component tree

- Update App.tsx to fetch and pass availableModels to Board
- Update Board component to accept and pass availableModels to Column
- Update Column component to receive availableModels prop
- Update ListView to handle availableModels prop
- Update Board test mock to include availableModels prop
This commit is contained in:
gsxdsm
2026-03-30 18:00:17 -07:00
parent f137d0ccf3
commit 7cfce83479
5 changed files with 27 additions and 5 deletions

View File

@@ -1,6 +1,7 @@
import { useState, useCallback, useEffect } from "react";
import type { TaskDetail, TaskCreateInput, Task, ThemeMode } from "@kb/core";
import { fetchConfig, fetchSettings, fetchAuthStatus, updateSettings } from "./api";
import { fetchConfig, fetchSettings, fetchAuthStatus, updateSettings, fetchModels } from "./api";
import type { ModelInfo } from "./api";
import { Header } from "./components/Header";
import { Board } from "./components/Board";
import { ListView } from "./components/ListView";
@@ -49,6 +50,7 @@ function AppInner() {
});
const [searchQuery, setSearchQuery] = useState("");
const [githubTokenConfigured, setGithubTokenConfigured] = useState(false);
const [availableModels, setAvailableModels] = useState<ModelInfo[]>([]);
const { tasks, createTask, moveTask, deleteTask, mergeTask, retryTask, updateTask, duplicateTask, archiveTask, unarchiveTask, archiveAllDone } = useTasks();
// Theme management
@@ -86,6 +88,13 @@ function AppInner() {
})
.catch(() => {/* fail silently — do not auto-open */});
}, []);
// Fetch available models
useEffect(() => {
fetchModels()
.then((models) => setAvailableModels(models))
.catch(() => {/* keep empty array on failure */});
}, []);
const { toasts, addToast, removeToast } = useToast();
// Persist view preference to localStorage
@@ -230,6 +239,7 @@ function AppInner() {
onUnarchiveTask={unarchiveTask}
onArchiveAllDone={archiveAllDone}
searchQuery={searchQuery}
availableModels={availableModels}
/>
) : (
// List view now uses the same modal-based create flow as board view.
@@ -241,6 +251,7 @@ function AppInner() {
globalPaused={globalPaused}
onNewTask={handleNewTaskOpen}
onQuickCreate={handleBoardQuickCreate}
availableModels={availableModels}
/>
)}
{detailTask && (

View File

@@ -4,6 +4,7 @@ import { Column } from "./Column";
import type { ToastType } from "../hooks/useToast";
import { useState, useMemo, useEffect, useCallback, useRef } from "react";
import { useBatchBadgeFetch } from "../hooks/useBatchBadgeFetch";
import type { ModelInfo } from "../api";
interface BoardProps {
tasks: Task[];
@@ -24,6 +25,7 @@ interface BoardProps {
onUnarchiveTask?: (id: string) => Promise<Task>;
onArchiveAllDone?: () => Promise<Task[]>;
searchQuery?: string;
availableModels?: ModelInfo[];
}
function sortTasksForColumn(tasks: Task[]): Task[] {
@@ -42,7 +44,7 @@ function areTaskArraysEqual(previous: Task[], next: Task[]): boolean {
return previous.every((task, index) => task === next[index]);
}
export function Board({ tasks, maxConcurrent, onMoveTask, onOpenDetail, addToast, onQuickCreate, onNewTask, autoMerge, onToggleAutoMerge, globalPaused, onUpdateTask, onArchiveTask, onUnarchiveTask, onArchiveAllDone, searchQuery = "" }: BoardProps) {
export function Board({ tasks, maxConcurrent, onMoveTask, onOpenDetail, addToast, onQuickCreate, onNewTask, autoMerge, onToggleAutoMerge, globalPaused, onUpdateTask, onArchiveTask, onUnarchiveTask, onArchiveAllDone, searchQuery = "", availableModels }: BoardProps) {
const [archivedCollapsed, setArchivedCollapsed] = useState(true);
const { fetchBatch } = useBatchBadgeFetch();
const debounceTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
@@ -147,6 +149,7 @@ export function Board({ tasks, maxConcurrent, onMoveTask, onOpenDetail, addToast
onArchiveTask={onArchiveTask}
onUnarchiveTask={onUnarchiveTask}
allTasks={filteredTasks}
availableModels={availableModels}
{...(col === "triage" ? { onQuickCreate, onNewTask } : {})}
{...(col === "in-review" ? { autoMerge, onToggleAutoMerge } : {})}
{...(col === "done" ? { onArchiveAllDone } : {})}

View File

@@ -8,6 +8,7 @@ import { QuickEntryBox } from "./QuickEntryBox";
import { groupByWorktree } from "../utils/worktreeGrouping";
import type { ToastType } from "../hooks/useToast";
import { ChevronDown, ChevronUp, Archive } from "lucide-react";
import type { ModelInfo } from "../api";
const PAGINATED_COLUMN_THRESHOLD = 100;
const VISIBLE_TASKS_INITIAL = 50;
@@ -35,9 +36,10 @@ interface ColumnProps {
collapsed?: boolean;
onToggleCollapse?: () => void;
allTasks?: Task[];
availableModels?: ModelInfo[];
}
function ColumnComponent({ column, tasks, maxConcurrent, onMoveTask, onOpenDetail, addToast, onQuickCreate, onNewTask, autoMerge, onToggleAutoMerge, globalPaused, onUpdateTask, onArchiveTask, onUnarchiveTask, onArchiveAllDone, collapsed, onToggleCollapse, allTasks }: ColumnProps) {
function ColumnComponent({ column, tasks, maxConcurrent, onMoveTask, onOpenDetail, addToast, onQuickCreate, onNewTask, autoMerge, onToggleAutoMerge, globalPaused, onUpdateTask, onArchiveTask, onUnarchiveTask, onArchiveAllDone, collapsed, onToggleCollapse, allTasks, availableModels }: ColumnProps) {
const [dragOver, setDragOver] = useState(false);
const [visibleTaskCount, setVisibleTaskCount] = useState(VISIBLE_TASKS_INITIAL);
const countFlashing = useFlashOnIncrease(tasks.length);
@@ -174,6 +176,7 @@ function ColumnComponent({ column, tasks, maxConcurrent, onMoveTask, onOpenDetai
onCreate={onQuickCreate}
addToast={addToast}
tasks={allTasks ?? []}
availableModels={availableModels}
/>
)}
{column === "in-progress" ? (

View File

@@ -3,6 +3,7 @@ import { LayoutGrid, List as ListIcon, ArrowUpDown, ArrowUp, ArrowDown, Search,
import type { Task, TaskDetail, Column, TaskStep, TaskCreateInput } from "@kb/core";
import { COLUMN_LABELS, COLUMNS } from "@kb/core";
import { fetchTaskDetail } from "../api";
import type { ModelInfo } from "../api";
import { QuickEntryBox } from "./QuickEntryBox";
import type { ToastType } from "../hooks/useToast";
@@ -32,6 +33,7 @@ interface ListViewProps {
globalPaused?: boolean;
onNewTask?: () => void;
onQuickCreate?: (input: TaskCreateInput) => Promise<void>;
availableModels?: ModelInfo[];
}
function getStepProgress(steps: TaskStep[]): string {
@@ -54,6 +56,7 @@ export function ListView({
globalPaused,
onNewTask,
onQuickCreate,
availableModels,
}: ListViewProps) {
const [sortField, setSortField] = useState<SortField>("id");
const [sortDirection, setSortDirection] = useState<SortDirection>("desc");
@@ -390,7 +393,9 @@ export function ListView({
<div className="list-quick-entry">
<QuickEntryBox
onCreate={onQuickCreate ?? (async () => addToast("Task creation not available", "error"))}
addToast={addToast}
addToast={addToast}
tasks={tasks}
availableModels={availableModels}
/>
</div>
<div className="list-column-toggle" ref={columnDropdownRef}>

View File

@@ -10,7 +10,7 @@ const columnRenderCounts: Record<string, number> = {};
// Mock child components so we only test Board's own rendering
vi.mock("../Column", () => ({
Column: React.memo(({ column, tasks, onToggleCollapse }: { column: string; tasks: Task[]; onToggleCollapse?: () => void }) => {
Column: React.memo(({ column, tasks, onToggleCollapse, availableModels }: { column: string; tasks: Task[]; onToggleCollapse?: () => void; availableModels?: unknown }) => {
columnRenderCounts[column] = (columnRenderCounts[column] ?? 0) + 1;
return (
<div data-testid={`column-${column}`} data-tasks={JSON.stringify(tasks)}>