feat(FN-3080): document graph navigation canonicalization in plugin authori
Adds documentation for graph navigation canonicalization (FN-3080) covering plugin authoring and dashboard usage, along with a changeset for the `@runfusion/fusion` package release. Fusion-Task-Id: FN-3080
This commit is contained in:
5
.changeset/fn-3080-graph-nav-canonicalization.md
Normal file
5
.changeset/fn-3080-graph-nav-canonicalization.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
Normalize dependency graph dashboard navigation so Graph resolves through a canonical `graph` task view destination and appears only in secondary navigation surfaces (desktop Header overflow and mobile More sheet). Also add TaskCard embedding support via `disableDrag` for plugin-hosted graph nodes.
|
||||
@@ -612,6 +612,11 @@ Placement guidance:
|
||||
- `overflow`: desktop header overflow menu
|
||||
- `more`: mobile More sheet / secondary nav surfaces
|
||||
|
||||
Project-scoped UI state guidance:
|
||||
- Persist plugin view layout/state in browser storage using a plugin-owned base key and the shared project-scoped pattern (`kb:${projectId}:${baseKey}`).
|
||||
- For dependency graph layout, the canonical base key is `fusion-plugin-dependency-graph:positions`.
|
||||
- Do not persist plugin UI state in task metadata or server-side task records.
|
||||
|
||||
---
|
||||
|
||||
## 9. Registering Agent Runtimes
|
||||
|
||||
@@ -40,6 +40,20 @@ Features:
|
||||
|
||||

|
||||
|
||||
## Graph View
|
||||
|
||||
Graph view visualizes task dependencies as an interactive node/edge map.
|
||||
|
||||
Navigation:
|
||||
- Desktop: **Header → More views → Graph**
|
||||
- Mobile: **MobileNavBar → More → Graph**
|
||||
|
||||
Behavior:
|
||||
- Shows only tasks in `triage`, `todo`, `in-progress`, and `in-review`
|
||||
- Excludes `done` and `archived`
|
||||
- Supports pan/zoom, fit-to-graph, dependency-chain highlight, and drag-to-position
|
||||
- Persists node layout per project using plugin-scoped storage key `fusion-plugin-dependency-graph:positions`
|
||||
|
||||
## Chat View
|
||||
|
||||
Chat view provides project-scoped conversations with agents.
|
||||
|
||||
@@ -256,6 +256,12 @@ function AppInner() {
|
||||
});
|
||||
|
||||
const { views: pluginDashboardViews } = usePluginDashboardViews(currentProject?.id);
|
||||
const graphPluginTaskView = useMemo(() => {
|
||||
const graphView = pluginDashboardViews.find(
|
||||
(entry) => entry.pluginId === "fusion-plugin-dependency-graph" && entry.view.viewId === "graph",
|
||||
);
|
||||
return graphView ? (`plugin:${graphView.pluginId}:${graphView.view.viewId}` as const) : null;
|
||||
}, [pluginDashboardViews]);
|
||||
|
||||
// History-aware view change handler — pushes nav entry on back-navigation stack.
|
||||
const handleTaskViewChange = useCallback((newView: TaskView) => {
|
||||
@@ -536,6 +542,10 @@ function AppInner() {
|
||||
useEffect(() => {
|
||||
if (!settingsLoaded) return;
|
||||
if (isPluginViewId(taskView)) return;
|
||||
if (taskView === "graph" && !graphPluginTaskView) {
|
||||
handleChangeTaskView("board");
|
||||
return;
|
||||
}
|
||||
if (taskView === "skills" && !skillsEnabled) {
|
||||
handleChangeTaskView("board");
|
||||
}
|
||||
@@ -557,7 +567,7 @@ function AppInner() {
|
||||
if (taskView === "research" && !researchEnabled) {
|
||||
handleChangeTaskView("board");
|
||||
}
|
||||
}, [taskView, settingsLoaded, skillsEnabled, insightsEnabled, roadmapEnabled, handleChangeTaskView, agentsEnabled, memoryEnabled, devServerEnabled, researchEnabled]);
|
||||
}, [taskView, settingsLoaded, skillsEnabled, insightsEnabled, roadmapEnabled, handleChangeTaskView, agentsEnabled, memoryEnabled, devServerEnabled, researchEnabled, graphPluginTaskView]);
|
||||
|
||||
// Auto-close nodes overlay if feature flag is toggled off while overlay is open
|
||||
useEffect(() => {
|
||||
@@ -975,12 +985,14 @@ function AppInner() {
|
||||
);
|
||||
}
|
||||
|
||||
const resolvedPluginTaskView = taskView === "graph" ? graphPluginTaskView : (isPluginViewId(taskView) ? taskView : null);
|
||||
|
||||
// Project view
|
||||
if (isPluginViewId(taskView)) {
|
||||
if (resolvedPluginTaskView) {
|
||||
return (
|
||||
<PageErrorBoundary>
|
||||
<PluginDashboardViewHost
|
||||
taskView={taskView as `plugin:${string}:${string}`}
|
||||
taskView={resolvedPluginTaskView as `plugin:${string}:${string}`}
|
||||
context={{
|
||||
projectId: currentProject?.id,
|
||||
tasks: isRemote && remoteData.tasks.length > 0 ? remoteData.tasks : tasks,
|
||||
@@ -993,6 +1005,7 @@ function AppInner() {
|
||||
onOpenDetail={(value: Task | TaskDetail) => openDetailTask(value)}
|
||||
addToast={addToast}
|
||||
workflowStepNameLookup={workflowStepNameLookup}
|
||||
disableDrag={true}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
@@ -1434,7 +1447,7 @@ function AppInner() {
|
||||
}}
|
||||
pluginDashboardViews={pluginDashboardViews}
|
||||
/>
|
||||
{viewMode === "project" && currentProject && taskView !== "chat" && taskView !== "mailbox" && taskView !== "insights" && taskView !== "devserver" && taskView !== "dev-server" && !isPluginViewId(taskView) && (
|
||||
{viewMode === "project" && currentProject && taskView !== "chat" && taskView !== "mailbox" && taskView !== "insights" && taskView !== "devserver" && taskView !== "dev-server" && taskView !== "graph" && !isPluginViewId(taskView) && (
|
||||
<QuickChatFAB
|
||||
projectId={currentProject.id}
|
||||
addToast={addToast}
|
||||
|
||||
@@ -1159,8 +1159,8 @@ export function Header({
|
||||
return (
|
||||
<button
|
||||
key={`${entry.pluginId}:${entry.view.viewId}`}
|
||||
className={`view-toggle-btn${view === pluginTaskView ? " active" : ""}`}
|
||||
onClick={() => onChangeView(pluginTaskView)}
|
||||
className={`view-toggle-btn${view === pluginTaskView || (view === "graph" && entry.pluginId === "fusion-plugin-dependency-graph" && entry.view.viewId === "graph") ? " active" : ""}`}
|
||||
onClick={() => onChangeView(entry.pluginId === "fusion-plugin-dependency-graph" && entry.view.viewId === "graph" ? "graph" : pluginTaskView)}
|
||||
title={`${entry.view.label} view`}
|
||||
aria-label={`${entry.view.label} view`}
|
||||
aria-pressed={view === pluginTaskView}
|
||||
@@ -1174,7 +1174,7 @@ export function Header({
|
||||
<>
|
||||
<button
|
||||
ref={viewOverflowTriggerRef}
|
||||
className={`view-toggle-btn${["research", "skills", "roadmaps", "insights", "memory", "dev-server", "devserver"].includes(view) || (todosEnabled && todosOpen) || isPluginViewId(view) ? " active" : ""}`}
|
||||
className={`view-toggle-btn${["research", "skills", "roadmaps", "insights", "memory", "dev-server", "devserver", "graph"].includes(view) || (todosEnabled && todosOpen) || isPluginViewId(view) ? " active" : ""}`}
|
||||
onClick={() => setIsViewOverflowOpen((prev) => !prev)}
|
||||
title="More views"
|
||||
aria-label="More views"
|
||||
@@ -1298,9 +1298,9 @@ export function Header({
|
||||
return (
|
||||
<button
|
||||
key={`${entry.pluginId}:${entry.view.viewId}`}
|
||||
className={`view-toggle-overflow-item${view === pluginTaskView ? " active" : ""}`}
|
||||
className={`view-toggle-overflow-item${view === pluginTaskView || (view === "graph" && entry.pluginId === "fusion-plugin-dependency-graph" && entry.view.viewId === "graph") ? " active" : ""}`}
|
||||
onClick={() => {
|
||||
onChangeView(pluginTaskView);
|
||||
onChangeView(entry.pluginId === "fusion-plugin-dependency-graph" && entry.view.viewId === "graph" ? "graph" : pluginTaskView);
|
||||
setIsViewOverflowOpen(false);
|
||||
}}
|
||||
role="menuitem"
|
||||
|
||||
@@ -212,22 +212,11 @@ export function MobileNavBar({
|
||||
const showRoadmapsTopLevel = roadmapEnabled && (!skillsEnabled || view === "roadmaps");
|
||||
const showSkillsTopLevel = skillsEnabled && (!roadmapEnabled || view !== "roadmaps");
|
||||
const showSkillsInMore = skillsEnabled && !showSkillsTopLevel;
|
||||
const isDependencyGraphView = (entry: PluginDashboardViewEntry): boolean => (
|
||||
entry.pluginId === "fusion-plugin-dependency-graph" && entry.view.viewId === "graph"
|
||||
);
|
||||
const sortedPrimaryPluginViews = pluginDashboardViews
|
||||
.filter((entry) => entry.view.placement === "primary")
|
||||
.sort((a, b) => (a.view.order ?? Number.MAX_SAFE_INTEGER) - (b.view.order ?? Number.MAX_SAFE_INTEGER));
|
||||
const dependencyGraphPluginView = pluginDashboardViews.find(isDependencyGraphView) ?? null;
|
||||
// Keep plugin-provided top-level tabs constrained on mobile so fixed tabs retain
|
||||
// reasonable touch-target width. Additional primary plugin destinations overflow into More.
|
||||
// FN-3235: Always surface the dependency graph destination as the first plugin top-level tab
|
||||
// on mobile so task graph navigation has a clear entry point.
|
||||
const prioritizedPrimaryPluginViews = dependencyGraphPluginView
|
||||
? [dependencyGraphPluginView, ...sortedPrimaryPluginViews.filter((entry) => !isDependencyGraphView(entry))]
|
||||
: sortedPrimaryPluginViews;
|
||||
const MAX_PRIMARY_PLUGIN_TOP_LEVEL_TABS = 1;
|
||||
const topLevelPrimaryPluginViews = prioritizedPrimaryPluginViews.slice(0, MAX_PRIMARY_PLUGIN_TOP_LEVEL_TABS);
|
||||
const topLevelPrimaryPluginViews = sortedPrimaryPluginViews.slice(0, MAX_PRIMARY_PLUGIN_TOP_LEVEL_TABS);
|
||||
const topLevelPluginViewKeys = new Set(
|
||||
topLevelPrimaryPluginViews.map((entry) => `${entry.pluginId}:${entry.view.viewId}`),
|
||||
);
|
||||
@@ -245,6 +234,7 @@ export function MobileNavBar({
|
||||
|| (todosOpen && todoViewEnabled)
|
||||
|| (view === "roadmaps" && !showRoadmapsTopLevel)
|
||||
|| (view === "skills" && !showSkillsTopLevel)
|
||||
|| view === "graph"
|
||||
|| (isPluginViewId(view) && !topLevelPrimaryPluginViews.some((entry) => buildPluginTaskViewId(entry.pluginId, entry.view.viewId) === view));
|
||||
|
||||
return (
|
||||
@@ -365,11 +355,11 @@ export function MobileNavBar({
|
||||
<button
|
||||
key={`${entry.pluginId}:${entry.view.viewId}`}
|
||||
type="button"
|
||||
className={`mobile-nav-tab${view === pluginTaskView ? " mobile-nav-tab--active" : ""}`}
|
||||
className={`mobile-nav-tab${view === pluginTaskView || (view === "graph" && entry.pluginId === "fusion-plugin-dependency-graph" && entry.view.viewId === "graph") ? " mobile-nav-tab--active" : ""}`}
|
||||
data-testid={`mobile-nav-tab-plugin-${entry.pluginId}-${entry.view.viewId}`}
|
||||
role="tab"
|
||||
aria-selected={view === pluginTaskView}
|
||||
onClick={() => onChangeView(pluginTaskView)}
|
||||
aria-selected={view === pluginTaskView || (view === "graph" && entry.pluginId === "fusion-plugin-dependency-graph" && entry.view.viewId === "graph")}
|
||||
onClick={() => onChangeView(entry.pluginId === "fusion-plugin-dependency-graph" && entry.view.viewId === "graph" ? "graph" : pluginTaskView)}
|
||||
>
|
||||
<PluginIcon />
|
||||
<span className="mobile-nav-tab-label">{entry.view.label}</span>
|
||||
@@ -719,7 +709,7 @@ export function MobileNavBar({
|
||||
type="button"
|
||||
className="mobile-more-item"
|
||||
data-testid={`mobile-more-item-plugin-${entry.pluginId}-${entry.view.viewId}`}
|
||||
onClick={() => handleMoreAction(() => onChangeView(pluginTaskView))}
|
||||
onClick={() => handleMoreAction(() => onChangeView(entry.pluginId === "fusion-plugin-dependency-graph" && entry.view.viewId === "graph" ? "graph" : pluginTaskView))}
|
||||
>
|
||||
<PluginIcon />
|
||||
<span>{entry.view.label}</span>
|
||||
|
||||
@@ -274,6 +274,8 @@ interface TaskCardProps {
|
||||
lastFetchTimeMs?: number;
|
||||
/** Lookup of workflow step IDs to display names, fetched once at board level. */
|
||||
workflowStepNameLookup?: ReadonlyMap<string, string>;
|
||||
/** Disable card drag semantics when embedding in custom draggable containers (e.g. dependency graph). */
|
||||
disableDrag?: boolean;
|
||||
}
|
||||
|
||||
function areTaskBadgeInfosEqual(
|
||||
@@ -415,6 +417,7 @@ function areTaskCardPropsEqual(previous: TaskCardProps, next: TaskCardProps): bo
|
||||
previous.onOpenMission === next.onOpenMission &&
|
||||
previous.onMoveTask === next.onMoveTask &&
|
||||
previous.workflowStepNameLookup === next.workflowStepNameLookup &&
|
||||
previous.disableDrag === next.disableDrag &&
|
||||
previousTask.id === nextTask.id &&
|
||||
previousTask.title === nextTask.title &&
|
||||
previousTask.description === nextTask.description &&
|
||||
@@ -478,6 +481,7 @@ function TaskCardComponent({
|
||||
onMoveTask,
|
||||
lastFetchTimeMs,
|
||||
workflowStepNameLookup,
|
||||
disableDrag,
|
||||
}: TaskCardProps) {
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const [fileDragOver, setFileDragOver] = useState(false);
|
||||
@@ -728,7 +732,7 @@ function TaskCardComponent({
|
||||
const isAwaitingApproval = task.column === "triage" && task.status === "awaiting-approval";
|
||||
const isArchived = task.column === "archived";
|
||||
const isAgentActive = !globalPaused && !queued && !isFailed && !isPaused && !isStuck && !isAwaitingApproval && (task.column === "in-progress" || ACTIVE_STATUSES.has(task.status as string));
|
||||
const isDraggable = !queued && !isPaused && !isEditing && !isArchived; // Disable drag during edit or if archived
|
||||
const isDraggable = !disableDrag && !queued && !isPaused && !isEditing && !isArchived; // Disable drag during edit/archived or host embedding
|
||||
|
||||
// Check if this card can be edited inline
|
||||
const canEdit = EDITABLE_COLUMNS.has(task.column) && !isAgentActive && !isPaused && !queued && onUpdateTask;
|
||||
|
||||
@@ -167,14 +167,14 @@ describe("Header", () => {
|
||||
expect(screen.getByTestId("view-overflow-todos")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders plugin dashboard views by placement and uses manifest icon metadata", () => {
|
||||
it("renders dependency graph in overflow and uses canonical graph task view", () => {
|
||||
const onChangeView = vi.fn();
|
||||
renderHeader({
|
||||
onChangeView,
|
||||
pluginDashboardViews: [
|
||||
{
|
||||
pluginId: "fusion-plugin-dependency-graph",
|
||||
view: { viewId: "graph", label: "Graph", componentPath: "./GraphView", icon: "Map", placement: "primary" },
|
||||
view: { viewId: "graph", label: "Graph", componentPath: "./GraphView", icon: "Map", placement: "more" },
|
||||
},
|
||||
{
|
||||
pluginId: "fusion-plugin-dependency-graph",
|
||||
@@ -183,10 +183,13 @@ describe("Header", () => {
|
||||
],
|
||||
});
|
||||
|
||||
const graphPrimary = screen.getByTestId("view-toggle-plugin-fusion-plugin-dependency-graph-graph");
|
||||
expect(graphPrimary.querySelector(".lucide-map")).toBeTruthy();
|
||||
fireEvent.click(graphPrimary);
|
||||
expect(onChangeView).toHaveBeenCalledWith("plugin:fusion-plugin-dependency-graph:graph");
|
||||
expect(screen.queryByTestId("view-toggle-plugin-fusion-plugin-dependency-graph-graph")).toBeNull();
|
||||
|
||||
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
|
||||
const graphItem = screen.getByTestId("view-overflow-plugin-fusion-plugin-dependency-graph-graph");
|
||||
expect(graphItem.querySelector(".lucide-map")).toBeTruthy();
|
||||
fireEvent.click(graphItem);
|
||||
expect(onChangeView).toHaveBeenCalledWith("graph");
|
||||
|
||||
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
|
||||
const queueItem = screen.getByTestId("view-overflow-plugin-fusion-plugin-dependency-graph-queue");
|
||||
|
||||
@@ -121,7 +121,7 @@ describe("MobileNavBar", () => {
|
||||
expect(onOpenTodos).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("renders dependency graph as a top-level tab and keeps additional plugin views in More", () => {
|
||||
it("keeps dependency graph in More and routes to canonical graph task view", () => {
|
||||
const props = createDefaultProps();
|
||||
render(
|
||||
<MobileNavBar
|
||||
@@ -139,14 +139,14 @@ describe("MobileNavBar", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const primaryTab = screen.getByTestId("mobile-nav-tab-plugin-fusion-plugin-dependency-graph-graph");
|
||||
expect(primaryTab.querySelector(".lucide-map")).toBeTruthy();
|
||||
fireEvent.click(primaryTab);
|
||||
expect(props.onChangeView).toHaveBeenCalledWith("plugin:fusion-plugin-dependency-graph:graph");
|
||||
expect(screen.queryByTestId("mobile-nav-tab-plugin-fusion-plugin-dependency-graph-graph")).toBeNull();
|
||||
|
||||
fireEvent.click(screen.getByTestId("mobile-nav-tab-more"));
|
||||
expect(screen.queryByTestId("mobile-more-item-plugin-fusion-plugin-dependency-graph-graph")).toBeNull();
|
||||
const graphItem = screen.getByTestId("mobile-more-item-plugin-fusion-plugin-dependency-graph-graph");
|
||||
fireEvent.click(graphItem);
|
||||
expect(props.onChangeView).toHaveBeenCalledWith("graph");
|
||||
|
||||
fireEvent.click(screen.getByTestId("mobile-nav-tab-more"));
|
||||
const overflowItem = screen.getByTestId("mobile-more-item-plugin-fusion-plugin-dependency-graph-queue");
|
||||
expect(overflowItem.querySelector(".lucide-workflow")).toBeTruthy();
|
||||
fireEvent.click(overflowItem);
|
||||
@@ -177,11 +177,11 @@ describe("MobileNavBar", () => {
|
||||
expect(screen.getByTestId("mobile-more-item-plugin-fusion-plugin-dependency-graph-queue")).toBeDefined();
|
||||
});
|
||||
|
||||
it("marks dependency graph plugin tab active when viewing its canonical plugin task-view", () => {
|
||||
it("marks More active when current view is graph", () => {
|
||||
render(
|
||||
<MobileNavBar
|
||||
{...createDefaultProps()}
|
||||
view="plugin:fusion-plugin-dependency-graph:graph"
|
||||
view="graph"
|
||||
pluginDashboardViews={[
|
||||
{
|
||||
pluginId: "fusion-plugin-dependency-graph",
|
||||
@@ -195,8 +195,7 @@ describe("MobileNavBar", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("mobile-nav-tab-plugin-fusion-plugin-dependency-graph-graph").className).toContain("mobile-nav-tab--active");
|
||||
expect(screen.getByTestId("mobile-nav-tab-more").className).not.toContain("mobile-nav-tab--active");
|
||||
expect(screen.getByTestId("mobile-nav-tab-more").className).toContain("mobile-nav-tab--active");
|
||||
});
|
||||
|
||||
it("marks More active when current plugin view is overflow-only", () => {
|
||||
@@ -218,7 +217,7 @@ describe("MobileNavBar", () => {
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("mobile-nav-tab-more").className).toContain("mobile-nav-tab--active");
|
||||
expect(screen.getByTestId("mobile-nav-tab-plugin-fusion-plugin-dependency-graph-graph").className).not.toContain("mobile-nav-tab--active");
|
||||
expect(screen.queryByTestId("mobile-nav-tab-plugin-fusion-plugin-dependency-graph-graph")).toBeNull();
|
||||
});
|
||||
|
||||
it("active tab is highlighted for mailbox", () => {
|
||||
|
||||
@@ -59,6 +59,12 @@ describe("TaskCard", () => {
|
||||
expect(screen.getByText("FN-001")).toBeDefined();
|
||||
});
|
||||
|
||||
it("disables native card dragging when disableDrag is true", () => {
|
||||
const { container } = render(<TaskCard task={makeTask()} onOpenDetail={noop} addToast={noop} disableDrag={true} />);
|
||||
const card = container.querySelector(".card") as HTMLElement;
|
||||
expect(card.getAttribute("draggable")).toBe("false");
|
||||
});
|
||||
|
||||
it("clicking PR badge link does not open the task detail modal", () => {
|
||||
const onOpenDetail = vi.fn();
|
||||
render(
|
||||
|
||||
@@ -307,6 +307,28 @@ describe("useViewState", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("restores and persists graph taskView using scoped storage", async () => {
|
||||
localStorage.setItem("kb:proj_123:kb-dashboard-task-view", "graph");
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useViewState(
|
||||
createOptions({
|
||||
currentProject: PROJECT,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.taskView).toBe("graph");
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
result.current.setTaskView("graph");
|
||||
});
|
||||
|
||||
expect(localStorage.getItem("kb:proj_123:kb-dashboard-task-view")).toBe("graph");
|
||||
});
|
||||
|
||||
it("restores and persists plugin task views using the canonical composite key", async () => {
|
||||
localStorage.setItem("kb:proj_123:kb-dashboard-task-view", "plugin:fusion-plugin-dependency-graph:graph");
|
||||
|
||||
|
||||
@@ -5,13 +5,14 @@ import { getScopedItem, setScopedItem } from "../utils/projectStorage";
|
||||
import { isPluginViewId } from "../plugins/pluginViewRegistry";
|
||||
|
||||
export type ViewMode = "overview" | "project";
|
||||
export type BuiltInTaskView = "board" | "list" | "agents" | "missions" | "chat" | "documents" | "research" | "roadmaps" | "skills" | "mailbox" | "insights" | "memory" | "devserver" | "dev-server";
|
||||
export type BuiltInTaskView = "board" | "list" | "graph" | "agents" | "missions" | "chat" | "documents" | "research" | "roadmaps" | "skills" | "mailbox" | "insights" | "memory" | "devserver" | "dev-server";
|
||||
export type PluginTaskView = `plugin:${string}:${string}`;
|
||||
export type TaskView = BuiltInTaskView | PluginTaskView;
|
||||
|
||||
const BUILT_IN_TASK_VIEWS: readonly BuiltInTaskView[] = [
|
||||
"board",
|
||||
"list",
|
||||
"graph",
|
||||
"agents",
|
||||
"missions",
|
||||
"chat",
|
||||
|
||||
Reference in New Issue
Block a user