feat(FN-1971): Step 1 - Add streaming variant of generateFeatureSuggestions
- Extract generateFeatureSuggestionsCore() that accepts onText/onThinking callbacks - Refactor generateFeatureSuggestions() to use core with no-op callbacks - Add generateFeatureSuggestionsStream() that uses core with SSE callbacks - Add SseCallbacks interface for streaming output types
This commit is contained in:
@@ -193,6 +193,7 @@ function AppInner() {
|
||||
experimentalFeatures,
|
||||
insightsEnabled,
|
||||
roadmapEnabled,
|
||||
memoryEnabled,
|
||||
toggleAutoMerge,
|
||||
toggleGlobalPause,
|
||||
toggleEnginePause,
|
||||
@@ -216,7 +217,10 @@ function AppInner() {
|
||||
if (taskView === "agents" && !agentsEnabled) {
|
||||
handleChangeTaskView("board");
|
||||
}
|
||||
}, [taskView, insightsEnabled, roadmapEnabled, experimentalFeatures, handleChangeTaskView, agentsEnabled]);
|
||||
if (taskView === "memory" && !memoryEnabled) {
|
||||
handleChangeTaskView("board");
|
||||
}
|
||||
}, [taskView, insightsEnabled, roadmapEnabled, experimentalFeatures, handleChangeTaskView, agentsEnabled, memoryEnabled]);
|
||||
|
||||
// Auto-close nodes overlay if feature flag is toggled off while overlay is open
|
||||
useEffect(() => {
|
||||
@@ -617,7 +621,7 @@ function AppInner() {
|
||||
}
|
||||
}}
|
||||
isRemote={isRemote}
|
||||
experimentalFeatures={{ insights: insightsEnabled, roadmap: roadmapEnabled }}
|
||||
experimentalFeatures={{ insights: insightsEnabled, roadmap: roadmapEnabled, memoryView: memoryEnabled }}
|
||||
/>
|
||||
{viewMode === "project" && currentProject && !nodesOpen && taskView !== "missions" && !modalManager.isPlanningOpen && (
|
||||
<SessionNotificationBanner
|
||||
@@ -680,7 +684,7 @@ function AppInner() {
|
||||
projectId={currentProject?.id}
|
||||
showSkillsTab={skillsEnabled}
|
||||
showAgentsTab={agentsEnabled}
|
||||
experimentalFeatures={{ insights: insightsEnabled, roadmap: roadmapEnabled }}
|
||||
experimentalFeatures={{ insights: insightsEnabled, roadmap: roadmapEnabled, memoryView: memoryEnabled }}
|
||||
/>
|
||||
{viewMode === "project" && currentProject && taskView !== "chat" && taskView !== "mailbox" && taskView !== "insights" && (
|
||||
<QuickChatFAB
|
||||
|
||||
@@ -209,7 +209,7 @@ export interface HeaderProps {
|
||||
/** Whether the current view is a remote node */
|
||||
isRemote?: boolean;
|
||||
/** Experimental feature flags controlling visibility of nav items. */
|
||||
experimentalFeatures?: { insights?: boolean; roadmap?: boolean };
|
||||
experimentalFeatures?: { insights?: boolean; roadmap?: boolean; memoryView?: boolean };
|
||||
}
|
||||
|
||||
export function Header({
|
||||
@@ -851,18 +851,20 @@ export function Header({
|
||||
<span>Skills</span>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className={`view-toggle-overflow-item${view === "memory" ? " active" : ""}`}
|
||||
onClick={() => {
|
||||
onChangeView("memory");
|
||||
setIsViewOverflowOpen(false);
|
||||
}}
|
||||
role="menuitem"
|
||||
data-testid="view-toggle-memory"
|
||||
>
|
||||
<Brain size={14} />
|
||||
<span>Memory</span>
|
||||
</button>
|
||||
{experimentalFeatures?.memoryView && (
|
||||
<button
|
||||
className={`view-toggle-overflow-item${view === "memory" ? " active" : ""}`}
|
||||
onClick={() => {
|
||||
onChangeView("memory");
|
||||
setIsViewOverflowOpen(false);
|
||||
}}
|
||||
role="menuitem"
|
||||
data-testid="view-toggle-memory"
|
||||
>
|
||||
<Brain size={14} />
|
||||
<span>Memory</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -62,7 +62,7 @@ export interface MobileNavBarProps {
|
||||
/** When true, shows the Agents mobile tab. Hidden by default (experimental feature). */
|
||||
showAgentsTab?: boolean;
|
||||
/** Experimental feature flags controlling visibility of nav items. */
|
||||
experimentalFeatures?: { insights?: boolean; roadmap?: boolean };
|
||||
experimentalFeatures?: { insights?: boolean; roadmap?: boolean; memoryView?: boolean };
|
||||
}
|
||||
|
||||
function GitHubLogo({ size = 20 }: { size?: number }) {
|
||||
@@ -540,15 +540,17 @@ export function MobileNavBar({
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="mobile-more-item"
|
||||
data-testid="mobile-more-item-memory"
|
||||
onClick={() => handleMoreAction(() => onChangeView("memory"))}
|
||||
>
|
||||
<Brain />
|
||||
<span>Memory</span>
|
||||
</button>
|
||||
{experimentalFeatures?.memoryView && (
|
||||
<button
|
||||
type="button"
|
||||
className="mobile-more-item"
|
||||
data-testid="mobile-more-item-memory"
|
||||
onClick={() => handleMoreAction(() => onChangeView("memory"))}
|
||||
>
|
||||
<Brain />
|
||||
<span>Memory</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="mobile-more-separator" />
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@ const AUTO_ARCHIVE_DEFAULT_AFTER_DAYS = 2;
|
||||
const KNOWN_EXPERIMENTAL_FEATURES: Record<string, string> = {
|
||||
insights: "Insights",
|
||||
roadmap: "Roadmaps",
|
||||
memoryView: "Memory",
|
||||
};
|
||||
|
||||
export type SectionId = SettingsSection["id"];
|
||||
|
||||
@@ -15,7 +15,7 @@ const defaultSettings: Settings = {
|
||||
worktreeInitCommand: "",
|
||||
testCommand: "",
|
||||
buildCommand: "",
|
||||
experimentalFeatures: { insights: true, roadmap: true, skillsView: true, agentsView: true },
|
||||
experimentalFeatures: { insights: true, roadmap: true, skillsView: true, agentsView: true, memoryView: true },
|
||||
};
|
||||
|
||||
vi.mock("../../api", async (importOriginal) => {
|
||||
@@ -1348,6 +1348,49 @@ describe("App view switching", () => {
|
||||
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
|
||||
expect(screen.queryByTestId("view-overflow-insights")).toBeNull();
|
||||
});
|
||||
|
||||
it("does not render memory view button when memoryView experimental feature is disabled", async () => {
|
||||
// Override fetchSettings to return memoryView as disabled
|
||||
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
...defaultSettings,
|
||||
experimentalFeatures: { memoryView: false },
|
||||
});
|
||||
|
||||
render(<App />);
|
||||
|
||||
// Wait for the header to render
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTitle("Board view")).toBeTruthy();
|
||||
});
|
||||
|
||||
// Open the overflow menu - Memory item should not be rendered
|
||||
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
|
||||
expect(screen.queryByTestId("view-toggle-memory")).toBeNull();
|
||||
});
|
||||
|
||||
it("redirects to board when memoryView experimental feature is disabled and taskView is memory", async () => {
|
||||
// Set localStorage to memory view but memoryView is disabled
|
||||
localStorage.setItem(taskViewStorageKey(), "memory");
|
||||
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
...defaultSettings,
|
||||
experimentalFeatures: { memoryView: false },
|
||||
});
|
||||
|
||||
render(<App />);
|
||||
|
||||
// Wait for the app to settle
|
||||
await waitFor(() => {
|
||||
expect(fetchSettings).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Should redirect to board view since memory is disabled
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector(".board")).toBeTruthy();
|
||||
});
|
||||
|
||||
// Cleanup
|
||||
localStorage.removeItem(taskViewStorageKey());
|
||||
});
|
||||
});
|
||||
|
||||
describe("App GitHub import", () => {
|
||||
|
||||
@@ -495,6 +495,28 @@ describe("Header", () => {
|
||||
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
|
||||
expect(screen.queryByTestId("view-overflow-skills")).toBeNull();
|
||||
});
|
||||
|
||||
it("does not render memory overflow item when memoryView is not enabled", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} experimentalFeatures={{}} />);
|
||||
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
|
||||
expect(screen.queryByTestId("view-toggle-memory")).toBeNull();
|
||||
});
|
||||
|
||||
it("renders memory overflow item when memoryView is enabled", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} experimentalFeatures={{ memoryView: true }} />);
|
||||
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
|
||||
expect(screen.getByTestId("view-toggle-memory")).toBeDefined();
|
||||
});
|
||||
|
||||
it("calls onChangeView with 'memory' when Memory overflow item is clicked", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} experimentalFeatures={{ memoryView: true }} />);
|
||||
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
|
||||
fireEvent.click(screen.getByTestId("view-toggle-memory"));
|
||||
expect(onChangeView).toHaveBeenCalledWith("memory");
|
||||
});
|
||||
});
|
||||
|
||||
// ── Search Visibility by View ─────────────────────────────────────
|
||||
|
||||
@@ -241,6 +241,18 @@ describe("MobileNavBar", () => {
|
||||
expect(screen.getByTestId("mobile-more-item-insights")).toBeDefined();
|
||||
});
|
||||
|
||||
it("does not show memory in more sheet when memoryView is not enabled", () => {
|
||||
render(<MobileNavBar {...createDefaultProps()} experimentalFeatures={{}} />);
|
||||
fireEvent.click(screen.getByTestId("mobile-nav-tab-more"));
|
||||
expect(screen.queryByTestId("mobile-more-item-memory")).toBeNull();
|
||||
});
|
||||
|
||||
it("shows memory in more sheet when memoryView is enabled", () => {
|
||||
render(<MobileNavBar {...createDefaultProps()} experimentalFeatures={{ memoryView: true }} />);
|
||||
fireEvent.click(screen.getByTestId("mobile-nav-tab-more"));
|
||||
expect(screen.getByTestId("mobile-more-item-memory")).toBeDefined();
|
||||
});
|
||||
|
||||
it("insights item in more sheet calls onChangeView with 'insights'", () => {
|
||||
const props = createDefaultProps();
|
||||
const { container } = render(<MobileNavBar {...props} experimentalFeatures={{ insights: true }} />);
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface UseAppSettingsResult {
|
||||
experimentalFeatures: Record<string, boolean>;
|
||||
insightsEnabled: boolean;
|
||||
roadmapEnabled: boolean;
|
||||
memoryEnabled: boolean;
|
||||
toggleAutoMerge: () => Promise<void>;
|
||||
toggleGlobalPause: () => Promise<void>;
|
||||
toggleEnginePause: () => Promise<void>;
|
||||
@@ -39,6 +40,7 @@ export function useAppSettings(projectId?: string): UseAppSettingsResult {
|
||||
const [experimentalFeatures, setExperimentalFeatures] = useState<Record<string, boolean>>({});
|
||||
const [insightsEnabled, setInsightsEnabled] = useState(false);
|
||||
const [roadmapEnabled, setRoadmapEnabled] = useState(false);
|
||||
const [memoryEnabled, setMemoryEnabled] = useState(false);
|
||||
|
||||
/**
|
||||
* Fetches config and settings from the backend and updates local state.
|
||||
@@ -67,6 +69,7 @@ export function useAppSettings(projectId?: string): UseAppSettingsResult {
|
||||
const features = settings.experimentalFeatures ?? {};
|
||||
setInsightsEnabled(features.insights === true);
|
||||
setRoadmapEnabled(features.roadmap === true);
|
||||
setMemoryEnabled(features.memoryView === true);
|
||||
}
|
||||
}, [projectId]);
|
||||
|
||||
@@ -130,6 +133,7 @@ export function useAppSettings(projectId?: string): UseAppSettingsResult {
|
||||
experimentalFeatures,
|
||||
insightsEnabled,
|
||||
roadmapEnabled,
|
||||
memoryEnabled,
|
||||
toggleAutoMerge,
|
||||
toggleGlobalPause,
|
||||
toggleEnginePause,
|
||||
|
||||
Reference in New Issue
Block a user