Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
JULES_API_KEY=<YOUR_JULES_API_KEY>
DASHBOARD_PORT=4444

# Unfinished dashboard surfaces are visible by default in dev/test and hidden by default in production builds.
# Set to true in a production build only when the Nodes page is ready to expose.
# VITE_CODEUX_FEATURE_NODES=true
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
name: Mockup Sprint Pentest (temporary dev validation)
name: Mockup Sprint Orchestration

on:
push:
branches:
- main
# Dev pushes run the rapid orchestration lane before integration.
- dev
branches: [main, dev]
pull_request:
branches: [main, dev]
workflow_dispatch:

concurrency:
group: mockup-sprint-pentest-${{ github.workflow }}-${{ github.ref }}
group: mockup-sprint-orchestration-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
mockup-sprint-pentest:
name: Mockup sprint pentest
mockup-sprint-orchestration:
name: Mockup sprint orchestration
runs-on: ubuntu-latest

steps:
Expand Down
4 changes: 3 additions & 1 deletion dashboard/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { TitleBar } from "./v2/components/TitleBar.js";
import { DashboardAssistantWidget } from "./v2/components/chat/DashboardAssistantWidget.js";
import { AddProjectModal, type AddProjectModalSubmission } from "./v2/components/ui/AddProjectModal.js";
import { ASSISTANT_OPEN_ADD_PROJECT_EVENT } from "./v2/lib/no-project-chat-assistant.js";
import { isDashboardFeatureEnabled } from "./v2/lib/dashboard-feature-flags.js";
import { buildProjectCreationSettingsOverride } from "./lib/settings-updaters.js";
import { DEFAULT_DASHBOARD_SETTINGS } from "./lib/settings.js";
import "./styles.css";
Expand Down Expand Up @@ -455,7 +456,8 @@ const notFoundRoute = createRoute({
component: ErrorPage,
});

const routeTree = rootRoute.addChildren([indexRoute, sprintsRoute, tasksRoute, projectsRoute, chatRoute, agentsRoute, nodesRoute, statsRoute, schedulerRoute, configRoute, memoryRoute, knowledgeRoute, browserRoute, fileBrowserRoute, docsRoute, docsDocumentRoute, liveRoute, notFoundRoute]);
const nodesFeatureEnabled = isDashboardFeatureEnabled("nodes");
const routeTree = rootRoute.addChildren([indexRoute, sprintsRoute, tasksRoute, projectsRoute, chatRoute, agentsRoute, ...(nodesFeatureEnabled ? [nodesRoute] : []), statsRoute, schedulerRoute, configRoute, memoryRoute, knowledgeRoute, browserRoute, fileBrowserRoute, docsRoute, docsDocumentRoute, liveRoute, notFoundRoute]);
// `defaultPreload: "intent"` warms route matching on hover/focus; the page chunks themselves are
// prefetched explicitly by the nav components via prefetchRoute() since they are Preact-lazy.
const router = createRouter({ routeTree, defaultPreload: "intent", defaultPreloadDelay: 50 });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { ArrowLeft, ArrowRight, BookOpen, Box, CalendarDays, Check, Compass, Eye
import { DASHBOARD_TOUR_START_EVENT, DASHBOARD_TOUR_STORAGE_KEY } from "../../lib/onboarding-control.js";
import { useReducedMotion } from "../../hooks/use-reduced-motion.js";
import { useInteractionTokens } from "../../lib/motion/tokens.js";
import type { DashboardFeatureId } from "../../lib/dashboard-feature-flags.js";
import { isDashboardFeatureEnabled } from "../../lib/dashboard-feature-flags.js";

type TourStep = {
id: string;
Expand All @@ -13,6 +15,7 @@ type TourStep = {
title: string;
body: string;
accent: "signal" | "ember" | "sky";
feature?: DashboardFeatureId;
};

type RectState = {
Expand Down Expand Up @@ -94,6 +97,7 @@ const TOUR_STEPS: TourStep[] = [
title: "Nodes",
body: "Nodes lets you compose project workflow graphs, configure node widgets, attach flows to agents, and inspect persisted runs.",
accent: "signal",
feature: "nodes",
},
{
id: "stats",
Expand Down Expand Up @@ -247,6 +251,9 @@ export const GuidedDashboardTour: FunctionComponent = () => {

const refreshSteps = useCallback(() => {
const steps = TOUR_STEPS.filter((step) => {
if (step.feature && !isDashboardFeatureEnabled(step.feature)) {
return false;
}
const element = getTourElement(step.targetId);
return element ? isVisibleTarget(element) : false;
});
Expand Down
67 changes: 67 additions & 0 deletions dashboard/src/v2/lib/dashboard-feature-flags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
export const DASHBOARD_FEATURE_IDS = ["nodes"] as const;

export type DashboardFeatureId = typeof DASHBOARD_FEATURE_IDS[number];

export type DashboardFeatureFlagMap = Record<DashboardFeatureId, boolean>;

export type DashboardFeatureFlagValues = Partial<Record<DashboardFeatureId, unknown>>;

export interface DashboardFeatureFlagSource {
devMode?: boolean;
values?: DashboardFeatureFlagValues;
}

export const DASHBOARD_FEATURE_ENV_KEYS: Record<DashboardFeatureId, string> = {
nodes: "VITE_CODEUX_FEATURE_NODES",
};

const ENABLED_VALUES = new Set(["1", "true", "yes", "on", "enabled"]);
const DISABLED_VALUES = new Set(["0", "false", "no", "off", "disabled"]);

export const parseDashboardFeatureFlagValue = (value: unknown): boolean | null => {
if (typeof value === "boolean") {
return value;
}
if (typeof value !== "string") {
return null;
}

const normalized = value.trim().toLowerCase();
if (!normalized) {
return null;
}
if (ENABLED_VALUES.has(normalized)) {
return true;
}
if (DISABLED_VALUES.has(normalized)) {
return false;
}
return null;
};

const readDashboardFeatureFlagSource = (): DashboardFeatureFlagSource => {
const env = import.meta.env as ImportMetaEnv & Record<string, unknown>;
return {
devMode: Boolean(env.DEV),
values: {
nodes: env[DASHBOARD_FEATURE_ENV_KEYS.nodes],
},
};
};

export const resolveDashboardFeatureFlags = (
source: DashboardFeatureFlagSource = readDashboardFeatureFlagSource(),
): DashboardFeatureFlagMap => {
const defaultEnabled = source.devMode ?? false;

return DASHBOARD_FEATURE_IDS.reduce<DashboardFeatureFlagMap>((flags, feature) => {
const explicitValue = parseDashboardFeatureFlagValue(source.values?.[feature]);
flags[feature] = explicitValue ?? defaultEnabled;
return flags;
}, {} as DashboardFeatureFlagMap);
};

export const isDashboardFeatureEnabled = (
feature: DashboardFeatureId,
flags: DashboardFeatureFlagMap = resolveDashboardFeatureFlags(),
): boolean => flags[feature];
10 changes: 9 additions & 1 deletion dashboard/src/v2/lib/navigation-items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
Zap,
} from "lucide-preact";
import type { DashboardExperienceMode } from "../../types.js";
import type { DashboardFeatureFlagMap, DashboardFeatureId } from "./dashboard-feature-flags.js";
import { isDashboardFeatureEnabled, resolveDashboardFeatureFlags } from "./dashboard-feature-flags.js";
import { normalizeDashboardExperienceMode } from "./experience-mode.js";

export const EXTERNAL_DOCS_URL = "https://github.com/codeux-ai/codeux#readme";
Expand Down Expand Up @@ -51,6 +53,7 @@ interface BaseNavigationItem {
group: NavigationItemGroup;
dockSection: NavigationDockSection;
tourId: string;
feature?: DashboardFeatureId;
}

export interface RouteNavigationItem extends BaseNavigationItem {
Expand All @@ -71,6 +74,7 @@ export type PrimaryNavigationItem = NavigationItem & {
interface GetPrimaryNavigationItemsOptions {
browserVisible?: boolean;
unavailableBrowserReason?: string;
featureFlags?: DashboardFeatureFlagMap;
}

export const ALL_NAVIGATION_ITEMS: readonly NavigationItem[] = [
Expand All @@ -79,7 +83,7 @@ export const ALL_NAVIGATION_ITEMS: readonly NavigationItem[] = [
{ id: "sprints", icon: Layers, label: "Sprints", path: "/sprints", color: "text-ember-500", group: "workspace", dockSection: "right", tourId: "nav-sprints", kind: "route" },
{ id: "tasks", icon: ListChecks, label: "Tasks", path: "/tasks", color: "text-signal-400", group: "workspace", dockSection: "right", tourId: "nav-tasks", kind: "route" },
{ id: "agents", icon: Cpu, label: "Agents", path: "/agents", color: "text-signal-400", group: "workspace", dockSection: "right", tourId: "nav-agents", kind: "route" },
{ id: "nodes", icon: Workflow, label: "Nodes", path: "/nodes", color: "text-signal-500", group: "workspace", dockSection: "right", tourId: "nav-nodes", kind: "route" },
{ id: "nodes", icon: Workflow, label: "Nodes", path: "/nodes", color: "text-signal-500", group: "workspace", dockSection: "right", tourId: "nav-nodes", kind: "route", feature: "nodes" },
{ id: "stats", icon: BarChart3, label: "Stats", path: "/stats", color: "text-signal-500", group: "workspace", dockSection: "right", tourId: "nav-stats", kind: "route" },
{ id: "scheduler", icon: CalendarDays, label: "Schedule", path: "/scheduler", color: "text-signal-500", group: "workspace", dockSection: "right", tourId: "nav-schedule", kind: "route" },
{ id: "memory", icon: Inbox, label: "Memory", path: "/memory", color: "text-signal-500", group: "workspace", dockSection: "right", tourId: "nav-memory", kind: "route" },
Expand Down Expand Up @@ -130,11 +134,15 @@ export const getPrimaryNavigationItems = (
): PrimaryNavigationItem[] => {
const normalizedMode = normalizeDashboardExperienceMode(mode);
const browserVisible = options.browserVisible ?? true;
const featureFlags = options.featureFlags ?? resolveDashboardFeatureFlags();

return NAVIGATION_ITEM_IDS_BY_MODE[normalizedMode]
.map((id) => navigationItemById.get(id))
.filter((item): item is NavigationItem => !!item)
.flatMap((item): PrimaryNavigationItem[] => {
if (item.feature && !isDashboardFeatureEnabled(item.feature, featureFlags)) {
return [];
}
if (item.id !== "browser" || browserVisible) {
return [item];
}
Expand Down
49 changes: 31 additions & 18 deletions dashboard/src/v2/router/route-prefetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,39 +7,52 @@
// The specifiers below resolve to the exact modules used by main.tsx's `lazy()` calls; the bundler
// and the ESM module cache dedupe by resolved module id, so a prefetch and the later `lazy()` load
// share one chunk and one in-flight request.
import type { DashboardFeatureFlagMap, DashboardFeatureId } from "../lib/dashboard-feature-flags.js";
import { isDashboardFeatureEnabled } from "../lib/dashboard-feature-flags.js";

type ModuleImporter = () => Promise<unknown>;

const componentImporters: Record<string, ModuleImporter> = {
"/sprints": () => import("../pages/sprints/SprintsPage.js"),
"/projects": () => import("../ProjectsPage.js"),
"/chat": () => import("../ChatPage.js"),
"/tasks": () => import("../TasksPage.js"),
"/agents": () => import("../AgentsPage.js"),
"/nodes": () => import("../NodesPage.js"),
"/stats": () => import("../StatsPage.js"),
"/scheduler": () => import("../SchedulerPage.js"),
"/config": () => import("../SettingsPage.js"),
"/memory": () => import("../MemoryPage.js"),
"/knowledge": () => import("../KnowledgePage.js"),
"/browser": () => import("../BrowserPage.js"),
"/files": () => import("../FileBrowserPage.js"),
"/docs": () => import("../docs-web/DocsWebPage.js"),
interface ComponentImporterEntry {
importer: ModuleImporter;
feature?: DashboardFeatureId;
}

const componentImporters: Record<string, ComponentImporterEntry> = {
"/sprints": { importer: () => import("../pages/sprints/SprintsPage.js") },
"/projects": { importer: () => import("../ProjectsPage.js") },
"/chat": { importer: () => import("../ChatPage.js") },
"/tasks": { importer: () => import("../TasksPage.js") },
"/agents": { importer: () => import("../AgentsPage.js") },
"/nodes": { importer: () => import("../NodesPage.js"), feature: "nodes" },
"/stats": { importer: () => import("../StatsPage.js") },
"/scheduler": { importer: () => import("../SchedulerPage.js") },
"/config": { importer: () => import("../SettingsPage.js") },
"/memory": { importer: () => import("../MemoryPage.js") },
"/knowledge": { importer: () => import("../KnowledgePage.js") },
"/browser": { importer: () => import("../BrowserPage.js") },
"/files": { importer: () => import("../FileBrowserPage.js") },
"/docs": { importer: () => import("../docs-web/DocsWebPage.js") },
};

const startedPaths = new Set<string>();

export const canPrefetchRoute = (path: string, featureFlags?: DashboardFeatureFlagMap): boolean => {
const entry = componentImporters[path];
return Boolean(entry && (!entry.feature || isDashboardFeatureEnabled(entry.feature, featureFlags)));
};

/**
* Begin downloading the chunk for `path` if it is code-split and not already requested.
* Safe to call repeatedly and on every pointer event — it is a no-op after the first call,
* and a failed prefetch is reset so a later real navigation can retry.
*/
export const prefetchRoute = (path: string): void => {
const importer = componentImporters[path];
if (!importer || startedPaths.has(path)) {
const entry = componentImporters[path];
if (!entry || !canPrefetchRoute(path) || startedPaths.has(path)) {
return;
}
startedPaths.add(path);
void importer().catch(() => {
void entry.importer().catch(() => {
// Allow a real navigation (or a later intent) to retry the load.
startedPaths.delete(path);
});
Expand Down
29 changes: 29 additions & 0 deletions docs-web/content/docs/developer-feature-flags.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Dashboard Feature Flags

Dashboard feature flags hide unfinished dashboard surfaces without deleting their implementation or tests.

Flags live in `dashboard/src/v2/lib/dashboard-feature-flags.ts`. They are resolved at dashboard bundle time through Vite `import.meta.env` values:

- Development and test builds enable all flagged unfinished features by default.
- Production builds disable flagged unfinished features by default.
- Explicit env values override the mode default.

Supported values are `true`, `1`, `yes`, `on`, `enabled`, `false`, `0`, `no`, `off`, and `disabled`. Empty or unrecognized values fall back to the mode default.

## Current Flags

| Feature | Env variable | Development default | Production default | Scope |
| --- | --- | --- | --- | --- |
| `nodes` | `VITE_CODEUX_FEATURE_NODES` | enabled | disabled | Hides the unfinished `/nodes` surface from route registration, shared navigation, route prefetch, and the guided dashboard tour. |

When `nodes` is disabled, the `NodesPage` module remains in source for local development and tests, but `/nodes` is not added to the TanStack route tree. Direct navigation to `/nodes` falls through to the dashboard not-found route.

## Adding a Flag

1. Add the feature id and Vite env key in `dashboard-feature-flags.ts`.
2. Attach the `feature` id to any affected navigation item in `dashboard/src/v2/lib/navigation-items.ts`.
3. Gate route registration in `dashboard/src/main.tsx`.
4. Gate route prefetch entries in `dashboard/src/v2/router/route-prefetch.ts`.
5. Gate guided tour steps or other entry points that reference the hidden surface.
6. Add focused tests for default behavior, explicit overrides, and every hidden entry point.
7. Update this page and the matching canonical `docs/` page.
15 changes: 12 additions & 3 deletions docs-web/content/docs/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export type DocsSlug =
| 'developer-http-api'
| 'developer-websocket-realtime'
| 'developer-configuration'
| 'developer-feature-flags'
| 'developer-settings-reference'
| 'developer-sprint-format'
| 'developer-building-from-source'
Expand Down Expand Up @@ -199,21 +200,21 @@ export const docsRegistry: Record<DocsSlug, DocsRegistryEntry> = {
path: '/docs/user-dashboard-nodes',
section: 'User Guide',
title: "Nodes",
description: "The Nodes page (/nodes) opens a browser-local canvas workspace for drafting workflow graphs with localStorage persistence, validation, JSON import/export, and agent command metadata.",
description: "The Nodes page (/nodes) opens the browser-local Nodes Canvas workspace for drafting Code UX workflow graphs. It does not require a selected project and does not call the node-flow backend APIs.",
},
'user-dashboard-nodes-canvas': {
id: 'user-dashboard-nodes-canvas',
path: '/docs/user-dashboard-nodes-canvas',
section: 'User Guide',
title: "Nodes Canvas",
description: "The Nodes Canvas page is a local graph drafting surface with trigger, agent, task, condition, and output nodes, structural validation, deterministic JSON exchange, and agent command helpers.",
description: "The Nodes Canvas page (/nodes) is a browser-local workspace for drafting Code UX workflow graphs. It combines the canvas, palette, inspector, validation panel, JSON exchange controls, and agent command summary without...",
},
'user-dashboard-node-flows': {
id: 'user-dashboard-node-flows',
path: '/docs/user-dashboard-node-flows',
section: 'User Guide',
title: "Node Flows",
description: "Create and operate saved node-flow workflows for the active project, including dynamic widgets, validation, manual runs, scheduling, run inspection, and agent skill attachments.",
description: "The Nodes page (/nodes) is where dashboard users create and operate saved node-flow workflows for the active project. A node flow is a repeatable graph that can be validated, run manually, scheduled, inspected through...",
},
'user-dashboard-scheduler': {
id: 'user-dashboard-scheduler',
Expand Down Expand Up @@ -313,6 +314,13 @@ export const docsRegistry: Record<DocsSlug, DocsRegistryEntry> = {
title: "Configuration & CLI",
description: "This page is the precise reference for every CLI flag, environment variable, and configuration file Code UX consumes.",
},
'developer-feature-flags': {
id: 'developer-feature-flags',
path: '/docs/developer-feature-flags',
section: 'Developer Reference',
title: "Dashboard Feature Flags",
description: "Dashboard feature flags hide unfinished dashboard surfaces without deleting their implementation or tests.",
},
'developer-settings-reference': {
id: 'developer-settings-reference',
path: '/docs/developer-settings-reference',
Expand Down Expand Up @@ -491,6 +499,7 @@ export const orderedDocs: DocsRegistryEntry[] = [
docsRegistry['developer-http-api'],
docsRegistry['developer-websocket-realtime'],
docsRegistry['developer-configuration'],
docsRegistry['developer-feature-flags'],
docsRegistry['developer-settings-reference'],
docsRegistry['developer-sprint-format'],
docsRegistry['developer-building-from-source'],
Expand Down
29 changes: 29 additions & 0 deletions docs-web/developer/feature-flags.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Dashboard Feature Flags

Dashboard feature flags hide unfinished dashboard surfaces without deleting their implementation or tests.

Flags live in `dashboard/src/v2/lib/dashboard-feature-flags.ts`. They are resolved at dashboard bundle time through Vite `import.meta.env` values:

- Development and test builds enable all flagged unfinished features by default.
- Production builds disable flagged unfinished features by default.
- Explicit env values override the mode default.

Supported values are `true`, `1`, `yes`, `on`, `enabled`, `false`, `0`, `no`, `off`, and `disabled`. Empty or unrecognized values fall back to the mode default.

## Current Flags

| Feature | Env variable | Development default | Production default | Scope |
| --- | --- | --- | --- | --- |
| `nodes` | `VITE_CODEUX_FEATURE_NODES` | enabled | disabled | Hides the unfinished `/nodes` surface from route registration, shared navigation, route prefetch, and the guided dashboard tour. |

When `nodes` is disabled, the `NodesPage` module remains in source for local development and tests, but `/nodes` is not added to the TanStack route tree. Direct navigation to `/nodes` falls through to the dashboard not-found route.

## Adding a Flag

1. Add the feature id and Vite env key in `dashboard-feature-flags.ts`.
2. Attach the `feature` id to any affected navigation item in `dashboard/src/v2/lib/navigation-items.ts`.
3. Gate route registration in `dashboard/src/main.tsx`.
4. Gate route prefetch entries in `dashboard/src/v2/router/route-prefetch.ts`.
5. Gate guided tour steps or other entry points that reference the hidden surface.
6. Add focused tests for default behavior, explicit overrides, and every hidden entry point.
7. Update this page and the matching canonical `docs/` page.
Loading