feat(FN-1292): consolidate InlineCreateCard controls into single row

- Refactor InlineCreateCard to display all action buttons (Plan, Subtask, Deps, Agent, Browser Verify, Preset, Models, Save) in one consolidated footer row
- Remove the separate inline-create-description-actions CSS class and associated styles
- Update InlineCreateCard tests to reflect the new consolidated control layout
- Update AGENTS.md documentation for InlineCreateCard controls description
- Update memory file with implementation notes
This commit is contained in:
gsxdsm
2026-04-08 20:48:08 -07:00
parent f2829c7634
commit 50e572b472
5 changed files with 118 additions and 74 deletions

53
.fusion/memory.md Normal file
View File

@@ -0,0 +1,53 @@
# Project Memory
## Architecture
- `CronRunner` uses dependency injection for AI prompt execution: an `AiPromptExecutor` function is injected via options. This keeps it decoupled from `createKbAgent` and testable without real agent sessions.
- `createAiPromptExecutor(cwd)` is an async factory function that creates a new agent session per call, uses `onText` for text accumulation, and disposes sessions in a `finally` block.
- The factory uses lazy `import("./pi.js")` to avoid pulling the pi SDK into the module graph when AI execution isn't needed.
- `HeartbeatMonitor.executeHeartbeat()` uses the Paperclip wake→check→work→exit model. The lazy `import("./pi.js")` pattern keeps pi SDK out of the module graph when only monitoring (not execution) is needed.
- Agent tool factories (`createTaskCreateTool`, `createTaskLogTool`) live in `agent-tools.ts` and are shared between `TaskExecutor` and `HeartbeatMonitor` to avoid duplication.
- Dashboard SSE clients (planning/subtask/mission interview) now use a shared keep-alive pattern: start a 25s `setInterval` in stream `onOpen` that `POST`s `/api/ai-sessions/:id/ping`, and always stop it on stream `close`, `complete`, and fatal errors.
## Conventions
- When mocking function types with Vitest for the build (tsc), use `vi.fn().mockResolvedValue(x) as unknown as T` instead of `vi.fn<Parameters<T>, ReturnType<T>>()`. The generic syntax works at runtime but fails during `tsc` build.
- When mocking `AgentStore` for heartbeat execution tests, track `saveRun` calls in a local `Map<string, AgentHeartbeatRun>` and have `getRunDetail` read from it — this way `completeRun`'s saved state is reflected in the returned run.
- When `HeartbeatMonitorOptions` has optional fields (`taskStore?`, `rootDir?`), capture them in local `const` variables after the early-return validation check to avoid `Object is possibly 'undefined'` TypeScript errors in the closure.
- For package-scoped single-file test runs, prefer `pnpm --filter <pkg> exec vitest run <file>` over `pnpm --filter <pkg> test -- <file>` when the package test script already hardcodes positional args.
- In dashboard task-creation forms, avoid special-casing built-in workflow template IDs in UI state; render from fetched `workflowSteps` IDs and let store-side template materialization resolve template IDs (`browser-verification``WS-XXX`).
- When a package mixes Electron main-process `.ts` files with renderer `.tsx` files, use `moduleResolution: "bundler"` plus `lib: ["DOM", "DOM.Iterable"]` in that package tsconfig; Node16 resolution will otherwise force `.js` extensions and break renderer imports during `tsc`.
- For React component tests in the desktop package, include `.test.tsx` in Vitest discovery and call `cleanup()` in `afterEach` to avoid cross-test DOM leakage that causes duplicate-element query failures.
- When extracting App-level async handlers into hooks, keep error/toast behavior inside the hook and wire passthrough handlers in `App.tsx` (`const handler = hookAction`) to avoid duplicate rollback/toast logic.
- For deep-link modal behavior (`?task=`), preserve one-time open semantics with internal refs in the hook so closing the modal can safely strip only the `task` query param while preserving other params (like `project`).
- When deprecating fields from `BoardConfig` but tests/internal flows still poke private config methods, keep temporary compatibility fields non-enumerable in `readConfig()` so `writeConfig()` can omit them from `config.json` while legacy tests can still mutate them.
- For dashboard route tests that mock `@fusion/core`, keep the mock export list in sync with the real route imports (for example `parseCompanyArchive`); missing one export silently changes route behavior and causes hard-to-diagnose failures.
- Browser directory pickers (`webkitdirectory`) cannot provide a server filesystem path; for dashboard import flows, parse selected `AGENTS.md` files client-side and send `{ agents }` payloads instead of trying to submit a directory `source` path.
- For conditionally rendered mobile inputs in dashboard components, prefer React `autoFocus` on the input over effect+`setTimeout` focus logic keyed to open-state booleans; mount timing is more reliable and simpler.
- Checkout leasing is explicit: use `checkoutTask`/`releaseTask` (or `/api/tasks/:id/checkout` + `/release`) for ownership, treat 409 conflicts as non-retryable contention, and let `HeartbeatMonitor.executeHeartbeat()` only validate `checkedOutBy` (never auto-acquire leases).
## Pitfalls
- `vi.fn<Parameters<SomeType>, ReturnType<SomeType>>()` works in Vitest runtime but causes TypeScript build errors (`TS2558: Expected 0-1 type arguments, but got 2`). Always use the cast pattern instead.
- When adding new exports to `@fusion/engine`, update the mock in `packages/cli/src/commands/__tests__/dashboard.test.ts` to include the new export, otherwise the test may fail with mysterious errors.
- Test `describe` blocks in Vitest can't access helper functions defined in sibling describe blocks. Place shared helpers in the parent scope or within the same describe block.
- When extracting shared code from `executor.ts` (e.g., tool factories), move the parameter schemas (`taskCreateParams`, `taskLogParams`) to the shared module too — keep them canonical in one place to avoid duplication.
- When changing API function signatures (e.g., `startAgentRun`), add new params at the END to preserve backward compatibility. Existing callers passing positional args will break if you insert a new param before existing ones.
- `HeartbeatMonitor.executeHeartbeat()` calls `startRun()` internally — do NOT call both `startRun()` and `executeHeartbeat()` for the same run, or you'll get duplicate runs. Use `startRun()` alone for record-only, or `executeHeartbeat()` for full execution.
- When RunsTab loads data via API calls instead of props, tests must mock the API functions (`fetchAgentRuns`, `fetchAgentRunDetail`) in addition to existing mocks, and set up defaults in `beforeEach`.
- In UI static analysis tests, avoid regex that spans multiple lines for code patterns (e.g., `setInterval.*5000`). Use separate `toContain()` assertions instead since the code is multi-line.
- In large inline mock objects, duplicate property keys are only warned by esbuild and the last declaration silently wins, which can hide the real mock implementation during route tests.
- For hardcoded workflow-step shortcuts in dashboard forms (like `"browser-verification"`), checked/toggle logic must reconcile both the literal template ID and resolved `WS-XXX` IDs by matching `workflowStep.templateId`.
- When using `import.meta.env` in `packages/dashboard/app/*`, ensure `packages/dashboard/tsconfig.app.json` includes `"vite/client"` in `compilerOptions.types`, or the dashboard typecheck test will fail with `Property 'env' does not exist on type 'ImportMeta'`.
- In dashboard app tests under `app/__tests__`, the built client output directory resolves to `../../dist/client` (not `../../../dist/client`).
- Fresh worktrees may miss linked Capacitor plugin packages until dependencies are installed; if dashboard tests/typecheck fail with unresolved `@capacitor/*` imports, run `pnpm install` at repo root first.
- When dashboard components add new `lucide-react` icons or new API functions, update the component test mocks (`vi.mock("lucide-react")` and `vi.mock("../../api")`) immediately; missing mock exports cause cascading runtime failures (`No "X" export is defined`) across otherwise unrelated tests.
- In fresh worktrees, workspace dependency links can be stale enough that dashboard/core tests fail resolving `yaml` from `@fusion/core`; run `pnpm install` at repo root before chasing false test failures.
- `pnpm test` at repo root runs dashboard's clean-checkout typecheck test; App-level TS issues (like duplicate imports or bad hook call signatures) may pass targeted Vitest runs but still fail the full suite.
- In executor worktrees, task attachment files referenced in PROMPT may exist only under the main repo path (`/Users/.../Projects/kb/.fusion/tasks/...`); if relative `.fusion/tasks/...` paths are missing, read the absolute attachment path directly.
- SQLite `ORDER BY timestamp DESC` alone can be nondeterministic when multiple rows share the same millisecond timestamp; add a stable tiebreaker (for example `rowid DESC`) when selecting a "latest" event.
- In `TaskCard.tsx`, `isInteractiveTarget` must check `target instanceof Element` (not `HTMLElement`) so SVG elements from lucide-react icons are correctly detected as interactive when inside buttons.
- If root `pnpm test` fails in `@gsxdsm/fusion` with `No matching export ... exportAgentsToDirectory` from `@fusion/core/dist/index.js`, run `pnpm --filter @fusion/core build` before rerunning tests so the core dist exports are refreshed for Bun compile tests.
- QuickEntryBox control test IDs are reused in `ListView` integration tests; when control layout changes (for example nested menu → inline buttons), update both `QuickEntryBox.test.tsx` and `ListView.test.tsx` together to avoid cascading failures.
- When `InlineCreateCard` layout changes, also check `Column.test.tsx` and `board-mobile.test.tsx` for references to moved/removed test IDs like `inline-create-description-actions`.
- `mission-store.test.ts` has a flaky test (`getMissionHealth computes mission metrics and latest error context`) that fails intermittently when timestamps collide in the same millisecond — this is pre-existing and not related to dashboard changes.

View File

@@ -749,20 +749,35 @@ The dashboard provides two UI surfaces for creating tasks:
### QuickEntryBox (List View) and InlineCreateCard (Board View)
Both components provide the same task creation experience with the following options:
Both components provide the same task creation experience. The chevron toggle is the single disclosure mechanism: collapsed = textarea only, expanded = all options visible in the controls panel.
**QuickEntryBox controls (all in expanded panel):**
- **Description input** — Type the task description. Press Enter to create immediately, or use the action buttons for AI-assisted creation.
- **Plan button** (Lightbulb icon) — Opens the AI Planning Mode modal with the current description pre-filled. This allows refining the task through an interactive Q&A before creation.
- **Subtask button** (ListTree icon) — Opens the subtask breakdown dialog with the current description pre-filled. The dialog generates 25 AI-suggested subtasks, lets the user edit titles, descriptions, sizes, and dependencies, and then creates all subtasks in one action.
- **Deps button** (Link icon) — Directly visible in the expanded controls panel. Opens the dependency picker to add task dependencies before creation.
- **Models button** (Brain icon) — Directly visible in the expanded controls panel. Opens a nested menu with Plan, Executor, and Validator roles; each role opens a submenu with a model dropdown for per-task overrides.
- **Save button** (Save icon) — Directly visible in the expanded controls panel. Manually creates/saves the task (alternative to pressing Enter).
- **Refine button** (Sparkles icon) — Opens a dropdown with Clarify, Add details, Expand, and Simplify options to refine the description with AI.
- **Deps button** (Link icon) — Opens the dependency picker to add task dependencies before creation.
- **Attach button** (Paperclip icon) — Attaches image files to the task.
- **Models button** (Brain icon) — Opens a nested menu with Plan, Executor, and Validator roles; each role opens a submenu with a model dropdown for per-task overrides.
- **Agent button** (Bot icon) — Opens the agent picker to assign the task to a specific agent.
- **Save button** (Save icon) — Manually creates/saves the task (alternative to pressing Enter).
**InlineCreateCard controls (all in expanded footer):**
- **Description input** — Type the task description. Press Enter to create immediately.
- **Plan button** (Lightbulb icon) — Opens the AI Planning Mode modal with the current description pre-filled.
- **Subtask button** (ListTree icon) — Opens the subtask breakdown dialog with the current description pre-filled.
- **Deps button** (Link icon) — Opens the dependency picker to add task dependencies before creation.
- **Agent button** (Bot icon) — Opens the agent picker to assign the task to a specific agent.
- **Browser Verify button** — Toggles the browser verification workflow step.
- **Preset button** (Zap icon) — Opens a dropdown to select a model preset or use custom models.
- **Models button** (Brain icon) — Opens the ModelSelectionModal for per-task model overrides.
- **Save button** — In the footer actions area (right-aligned). Manually creates/saves the task.
**Behavior:**
- Both Plan and Subtask buttons are disabled when no description is entered.
- Clicking either button clears the input after triggering the action.
- Regular task creation (Enter key) works as before without AI assistance.
- Escape dismisses overlays in order: model submenu → model menu → dependency picker → input clear/collapse.
- Escape dismisses overlays in order: model submenu → model menu → agent picker → deps popover → refine menu → input clear/collapse.
### Subtask Breakdown Dialog

View File

@@ -612,33 +612,6 @@ export function InlineCreateCard({
{isExpanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
</button>
</div>
{/* AI-assisted refinement actions — always visible when expanded */}
{isExpanded && !submitting && (
<div className="inline-create-description-actions" data-testid="inline-create-description-actions">
<button
type="button"
className="btn btn-sm"
onClick={handlePlanClick}
disabled={!description.trim()}
data-testid="plan-button"
title="Open planning mode with current description"
>
<Lightbulb size={12} style={{ verticalAlign: "middle", marginRight: 4 }} />
Plan
</button>
<button
type="button"
className="btn btn-sm"
onClick={handleSubtaskClick}
disabled={!description.trim()}
data-testid="subtask-button"
title="Break down into AI-generated subtasks"
>
<ListTree size={12} style={{ verticalAlign: "middle", marginRight: 4 }} />
Subtask
</button>
</div>
)}
{pendingImages.length > 0 && (
<div className="inline-create-previews">
{pendingImages.map((img, i) => (
@@ -660,6 +633,30 @@ export function InlineCreateCard({
{isExpanded && (
<div id="inline-create-controls" className="inline-create-footer">
<div className="inline-create-controls">
<button
type="button"
className="btn btn-sm"
onClick={handlePlanClick}
onMouseDown={(e) => e.preventDefault()}
disabled={!description.trim()}
data-testid="plan-button"
title="Open planning mode with current description"
>
<Lightbulb size={12} style={{ verticalAlign: "middle", marginRight: 4 }} />
Plan
</button>
<button
type="button"
className="btn btn-sm"
onClick={handleSubtaskClick}
onMouseDown={(e) => e.preventDefault()}
disabled={!description.trim()}
data-testid="subtask-button"
title="Break down into AI-generated subtasks"
>
<ListTree size={12} style={{ verticalAlign: "middle", marginRight: 4 }} />
Subtask
</button>
<div className="dep-trigger-wrap">
<button
type="button"

View File

@@ -857,52 +857,44 @@ describe("InlineCreateCard button visibility when collapsed", () => {
});
});
describe("Description-adjacent actions layout (FN-781)", () => {
it("renders Plan and Subtask in description-actions area when expanded", () => {
describe("Consolidated controls layout (FN-781, FN-1292)", () => {
it("renders Plan, Subtask, Deps, Agent, and Models together in footer controls when expanded", () => {
renderCard();
expandCard();
// The description-actions container should exist
expect(screen.getByTestId("inline-create-description-actions")).toBeTruthy();
// All buttons should be in the footer controls row
const controlsRow = document.querySelector(".inline-create-controls");
expect(controlsRow).toBeTruthy();
// Plan and Subtask buttons should be inside it
const actionsContainer = screen.getByTestId("inline-create-description-actions");
expect(actionsContainer.contains(screen.getByTestId("plan-button"))).toBe(true);
expect(actionsContainer.contains(screen.getByTestId("subtask-button"))).toBe(true);
// Plan, Subtask, Deps, Agent, Browser Verify, Preset, Models all in one row
expect(controlsRow!.contains(screen.getByTestId("plan-button"))).toBe(true);
expect(controlsRow!.contains(screen.getByTestId("subtask-button"))).toBe(true);
expect(controlsRow!.contains(screen.getByTestId("inline-create-agent-button"))).toBe(true);
const depsButton = screen.getByText(/Deps/);
expect(controlsRow!.contains(depsButton)).toBe(true);
const modelsButton = screen.getByRole("button", { name: /Models/i });
expect(controlsRow!.contains(modelsButton)).toBe(true);
});
it("does not render description-actions when not expanded", () => {
it("does not render footer controls when not expanded", () => {
renderCard();
// Description actions should not exist when collapsed
expect(screen.queryByTestId("inline-create-description-actions")).toBeNull();
// Footer controls should not exist when collapsed
expect(document.querySelector(".inline-create-controls")).toBeNull();
});
it("Save button remains in footer area, not description-actions", () => {
it("Save button remains in footer actions area, separate from controls row", () => {
renderCard();
expandCard();
const actionsContainer = screen.getByTestId("inline-create-description-actions");
const controlsRow = document.querySelector(".inline-create-controls");
const saveButton = screen.getByTestId("save-button");
// Save button should NOT be in the description-actions area
expect(actionsContainer.contains(saveButton)).toBe(false);
// Save button should NOT be in the controls row (it's in inline-create-actions)
expect(controlsRow!.contains(saveButton)).toBe(false);
});
it("Deps, Preset, Models buttons remain in footer area, not description-actions", () => {
renderCard();
expandCard();
const actionsContainer = screen.getByTestId("inline-create-description-actions");
const depsButton = screen.getByText(/Deps/);
const modelsButton = screen.getByRole("button", { name: /Models/i });
// Deps and Models should NOT be in the description-actions area
expect(actionsContainer.contains(depsButton)).toBe(false);
expect(actionsContainer.contains(modelsButton)).toBe(false);
});
it("Plan and Subtask disabled state still works in description-actions", () => {
it("Plan and Subtask disabled state still works in consolidated controls", () => {
renderCard();
expandCard();
const textarea = screen.getByPlaceholderText("What needs to be done?");

View File

@@ -5083,14 +5083,6 @@ body {
flex-wrap: wrap;
}
/* AI-assisted description actions — sits between textarea and expanded controls */
.inline-create-description-actions {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.inline-create-controls {
display: flex;
align-items: center;
@@ -6093,11 +6085,6 @@ body {
flex-shrink: 0;
}
.inline-create-description-actions .btn {
min-height: 44px;
padding: var(--space-xs) var(--space-md);
}
/* Inline create: wrap footer controls at 280px */
.inline-create-controls {
flex-wrap: wrap;