feat(FN-2579): merge fusion/fn-2579 (auto-resolved)
- fix(FN-2579): complete Step 3 — refine split-button accessibility - fix(FN-2579): resolve workspace lint regression in provider - test(FN-2579): complete Step 2 — update header engine control tests - feat(FN-2579): complete Step 1 — style engine split-button dropdown - feat(FN-2579): complete Step 0 — add engine control split-button behavior - chore(release): v1.0.0
This commit is contained in:
66
CHANGELOG.md
66
CHANGELOG.md
@@ -2,6 +2,72 @@
|
|||||||
|
|
||||||
User-facing release notes aggregated across all packages. This file is auto-synced from each `packages/*/CHANGELOG.md` by `scripts/release.mjs` — do not edit by hand.
|
User-facing release notes aggregated across all packages. This file is auto-synced from each `packages/*/CHANGELOG.md` by `scripts/release.mjs` — do not edit by hand.
|
||||||
|
|
||||||
|
## 1.0.0
|
||||||
|
|
||||||
|
### @fusion/dashboard
|
||||||
|
|
||||||
|
#### Patch Changes
|
||||||
|
|
||||||
|
- @fusion/core@1.0.0
|
||||||
|
- @fusion/engine@1.0.0
|
||||||
|
|
||||||
|
### @fusion/engine
|
||||||
|
|
||||||
|
#### Patch Changes
|
||||||
|
|
||||||
|
- @fusion/core@1.0.0
|
||||||
|
- @fusion/pi-claude-cli@1.0.0
|
||||||
|
|
||||||
|
### @fusion/plugin-sdk
|
||||||
|
|
||||||
|
#### Patch Changes
|
||||||
|
|
||||||
|
- @fusion/core@1.0.0
|
||||||
|
|
||||||
|
### @runfusion/fusion
|
||||||
|
|
||||||
|
#### Minor Changes
|
||||||
|
|
||||||
|
- 9d8852e: Add project-level overlap ignore paths so teams can exempt safe shared files/directories from overlap-based task serialization while keeping overlap protection enabled for the rest of the repo.
|
||||||
|
|
||||||
|
#### Patch Changes
|
||||||
|
|
||||||
|
- f560af5: Fix dashboard TUI agents view run history rendering to use readable status labels and allow opening selected run logs reliably.
|
||||||
|
- cd4cef3: Fix dashboard TUI agent run-log opening so Enter key presses sent as carriage-return/newline characters are recognized reliably.
|
||||||
|
- 7e05a20: Speed up `fn init` project-name detection by skipping git remote lookup when the target directory is not a git repository. This avoids unnecessary subprocess work and reduces timeout risk in test/CI environments.
|
||||||
|
- c818d71: Inset plugin manager cards from the modal edges on mobile. The plugins subsection panel had no horizontal padding while its heading and toggle were already inset, leaving cards flush with the modal frame on small screens.
|
||||||
|
- c818d71: Fix triage hangs when using pi-claude-cli with claude-sonnet-4-6. Parameterless custom tools (e.g. `fn_review_spec`) emit zero `input_json_delta` events from the Claude CLI, so the event bridge previously fell through to a raw empty-string fallback and pi's TypeBox validator rejected the call with "root: must be object" — looping the agent indefinitely. Defaults empty `partialJson` to `{}`. Also adds a reminder loop before the planning fallback model engages, propagates the bundled `@runfusion/fusion` extension into engine sessions so `fn_*` tools register without `pi install`, and drops the "historical" qualifier from replayed tool labels that was confusing models into treating their own prior turns as a previous session.
|
||||||
|
- ff6a68b: Fix Skills Catalog initial-load failures by preventing unauthenticated public search requests for empty or too-short queries. The dashboard now returns a successful empty catalog result for short-query unauthenticated/fallback states instead of surfacing upstream 400 errors.
|
||||||
|
- 1b3994f: Fix the dashboard terminal modal desktop width contract so large displays use a broad viewport-based layout, and harden terminal input lifecycle handling so xterm keyboard input continues forwarding reliably after rerenders.
|
||||||
|
- 1a8058f: Make agent pause/resume state transitions act immediately by stopping active heartbeat runs on pause and triggering an on-demand heartbeat on resume.
|
||||||
|
- 39622f0: Fix scheduled automations so overdue runs catch up reliably after server downtime. Startup/settings sync no longer pushes unchanged overdue schedules into the future, and memory dreams automation is now synchronized during engine startup before cron begins ticking.
|
||||||
|
- 26f9c74: Synchronize Fusion skill documentation from `extension.ts` across `SKILL.md`, `references/extension-tools.md`, and `references/fusion-capabilities.md`, and document engine session-scoped runtime tools in a new `references/engine-tools.md` reference.
|
||||||
|
|
||||||
|
### runfusion.ai
|
||||||
|
|
||||||
|
#### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [f560af5]
|
||||||
|
- Updated dependencies [cd4cef3]
|
||||||
|
- Updated dependencies [7e05a20]
|
||||||
|
- Updated dependencies [c818d71]
|
||||||
|
- Updated dependencies [c818d71]
|
||||||
|
- Updated dependencies [ff6a68b]
|
||||||
|
- Updated dependencies [1b3994f]
|
||||||
|
- Updated dependencies [1a8058f]
|
||||||
|
- Updated dependencies [39622f0]
|
||||||
|
- Updated dependencies [26f9c74]
|
||||||
|
- Updated dependencies [9d8852e]
|
||||||
|
- @runfusion/fusion@1.0.0
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
### @runfusion/fusion
|
||||||
|
|
||||||
|
#### Patch Changes
|
||||||
|
|
||||||
|
- FN-2501: Agent pause/resume controls now act immediately. Pausing stops an active heartbeat run right away, and resuming to `active` triggers an immediate on-demand heartbeat instead of waiting for the next timer tick.
|
||||||
|
|
||||||
## 0.2.7
|
## 0.2.7
|
||||||
|
|
||||||
### @fusion/dashboard
|
### @fusion/dashboard
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# runfusion.ai
|
# runfusion.ai
|
||||||
|
|
||||||
## 0.3.0
|
## 1.0.0
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
- Updated dependencies [39622f0]
|
- Updated dependencies [39622f0]
|
||||||
- Updated dependencies [26f9c74]
|
- Updated dependencies [26f9c74]
|
||||||
- Updated dependencies [9d8852e]
|
- Updated dependencies [9d8852e]
|
||||||
- @runfusion/fusion@0.3.0
|
- @runfusion/fusion@1.0.0
|
||||||
|
|
||||||
## 0.2.7
|
## 0.2.7
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "runfusion.ai",
|
"name": "runfusion.ai",
|
||||||
"version": "0.3.0",
|
"version": "1.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"description": "Launch Fusion with `npx runfusion.ai` — tiny alias for @runfusion/fusion.",
|
"description": "Launch Fusion with `npx runfusion.ai` — tiny alias for @runfusion/fusion.",
|
||||||
"homepage": "https://runfusion.ai",
|
"homepage": "https://runfusion.ai",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# @runfusion/fusion
|
# @runfusion/fusion
|
||||||
|
|
||||||
## 0.3.0
|
## 1.0.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@runfusion/fusion",
|
"name": "@runfusion/fusion",
|
||||||
"version": "0.3.0",
|
"version": "1.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"description": "Fusion CLI: HTTP API server, daemon, dashboard launcher, and task tooling for the Fusion AI coding agent.",
|
"description": "Fusion CLI: HTTP API server, daemon, dashboard launcher, and task tooling for the Fusion AI coding agent.",
|
||||||
"homepage": "https://github.com/Runfusion/Fusion#readme",
|
"homepage": "https://github.com/Runfusion/Fusion#readme",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# @fusion/core
|
# @fusion/core
|
||||||
|
|
||||||
## 0.3.0
|
## 1.0.0
|
||||||
|
|
||||||
## 0.2.7
|
## 0.2.7
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@fusion/core",
|
"name": "@fusion/core",
|
||||||
"version": "0.3.0",
|
"version": "1.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"description": "Fusion core: task store, scheduler, settings, and shared domain types backing the Fusion AI coding agent.",
|
"description": "Fusion core: task store, scheduler, settings, and shared domain types backing the Fusion AI coding agent.",
|
||||||
"homepage": "https://github.com/Runfusion/Fusion#readme",
|
"homepage": "https://github.com/Runfusion/Fusion#readme",
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
# @fusion/dashboard
|
# @fusion/dashboard
|
||||||
|
|
||||||
## 0.3.0
|
## 1.0.0
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|
||||||
- @fusion/core@0.3.0
|
- @fusion/core@1.0.0
|
||||||
- @fusion/engine@0.3.0
|
- @fusion/engine@1.0.0
|
||||||
|
|
||||||
## 0.2.7
|
## 0.2.7
|
||||||
|
|
||||||
|
|||||||
@@ -83,14 +83,10 @@ describe("tablet header controls", () => {
|
|||||||
|
|
||||||
// ── Engine controls stay inline on tablet ──────────────────────
|
// ── Engine controls stay inline on tablet ──────────────────────
|
||||||
|
|
||||||
it("renders pause scheduling button inline on tablet", () => {
|
it("renders engine control split-button inline on tablet", () => {
|
||||||
renderTabletHeader();
|
renderTabletHeader();
|
||||||
expect(screen.getByTitle("Pause scheduling")).toBeDefined();
|
expect(screen.getByTestId("engine-control-main-btn")).toBeDefined();
|
||||||
});
|
expect(screen.getByTestId("engine-control-chevron-btn")).toBeDefined();
|
||||||
|
|
||||||
it("renders stop AI engine button inline on tablet", () => {
|
|
||||||
renderTabletHeader();
|
|
||||||
expect(screen.getByTitle("Stop AI engine")).toBeDefined();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders view toggle inline on tablet", () => {
|
it("renders view toggle inline on tablet", () => {
|
||||||
|
|||||||
@@ -285,6 +285,90 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Split-button for engine controls */
|
||||||
|
.engine-control-split-btn {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.engine-control-split-btn__main {
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engine-control-split-btn__chevron {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
min-width: 28px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engine-control-split-btn__divider {
|
||||||
|
width: 1px;
|
||||||
|
height: 16px;
|
||||||
|
background: var(--border);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engine-control-split-btn__menu {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 4px);
|
||||||
|
right: 0;
|
||||||
|
min-width: 160px;
|
||||||
|
background: var(--card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
z-index: 200;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engine-control-split-btn__menu-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
width: 100%;
|
||||||
|
padding: var(--space-xs) var(--space-sm);
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
transition: background var(--transition-fast), color var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.engine-control-split-btn__menu-item:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engine-control-split-btn__menu-item:focus-visible {
|
||||||
|
background: var(--card-hover);
|
||||||
|
box-shadow: var(--focus-ring-strong);
|
||||||
|
color: var(--text);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
.engine-control-split-btn__menu-item:not(:disabled):hover {
|
||||||
|
background: var(--card-hover);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.engine-control-split-btn__menu-item--active {
|
||||||
|
color: var(--triage);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
.engine-control-split-btn__menu-item--active:not(:disabled):hover {
|
||||||
|
color: var(--triage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Header badge for active sessions */
|
/* Header badge for active sessions */
|
||||||
.btn-icon--has-indicator {
|
.btn-icon--has-indicator {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -857,6 +941,12 @@
|
|||||||
min-height: 36px;
|
min-height: 36px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.engine-control-split-btn__main,
|
||||||
|
.engine-control-split-btn__chevron {
|
||||||
|
min-width: 36px;
|
||||||
|
min-height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Mobile header: collapsible search trigger */
|
/* Mobile header: collapsible search trigger */
|
||||||
.mobile-search-trigger {
|
.mobile-search-trigger {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -272,6 +272,7 @@ export function Header({
|
|||||||
const [isMobileProjectSwitchOpen, setIsMobileProjectSwitchOpen] = useState(false);
|
const [isMobileProjectSwitchOpen, setIsMobileProjectSwitchOpen] = useState(false);
|
||||||
const [isViewOverflowOpen, setIsViewOverflowOpen] = useState(false);
|
const [isViewOverflowOpen, setIsViewOverflowOpen] = useState(false);
|
||||||
const [isDesktopOverflowOpen, setIsDesktopOverflowOpen] = useState(false);
|
const [isDesktopOverflowOpen, setIsDesktopOverflowOpen] = useState(false);
|
||||||
|
const [isEngineMenuOpen, setIsEngineMenuOpen] = useState(false);
|
||||||
const [overflowScripts, setOverflowScripts] = useState<Record<string, string>>({});
|
const [overflowScripts, setOverflowScripts] = useState<Record<string, string>>({});
|
||||||
const [overflowScriptsLoading, setOverflowScriptsLoading] = useState(false);
|
const [overflowScriptsLoading, setOverflowScriptsLoading] = useState(false);
|
||||||
const overflowButtonRef = useRef<HTMLButtonElement>(null);
|
const overflowButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
@@ -285,6 +286,7 @@ export function Header({
|
|||||||
const mobileProjectSwitchRef = useRef<HTMLDivElement>(null);
|
const mobileProjectSwitchRef = useRef<HTMLDivElement>(null);
|
||||||
const viewOverflowRef = useRef<HTMLDivElement>(null);
|
const viewOverflowRef = useRef<HTMLDivElement>(null);
|
||||||
const viewOverflowTriggerRef = useRef<HTMLButtonElement>(null);
|
const viewOverflowTriggerRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const engineMenuRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Get remote nodes only (exclude local node type)
|
// Get remote nodes only (exclude local node type)
|
||||||
const remoteNodes = useMemo(() =>
|
const remoteNodes = useMemo(() =>
|
||||||
@@ -451,6 +453,32 @@ export function Header({
|
|||||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||||
}, [isMobileProjectSwitchOpen]);
|
}, [isMobileProjectSwitchOpen]);
|
||||||
|
|
||||||
|
// Close engine controls dropdown on outside click
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isEngineMenuOpen) return;
|
||||||
|
|
||||||
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
|
if (engineMenuRef.current && !engineMenuRef.current.contains(e.target as Node)) {
|
||||||
|
setIsEngineMenuOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
}, [isEngineMenuOpen]);
|
||||||
|
|
||||||
|
// Close engine controls dropdown on Escape
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isEngineMenuOpen) return;
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") setIsEngineMenuOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||||
|
}, [isEngineMenuOpen]);
|
||||||
|
|
||||||
// Close view toggle overflow on outside click
|
// Close view toggle overflow on outside click
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isViewOverflowOpen) return;
|
if (!isViewOverflowOpen) return;
|
||||||
@@ -1058,24 +1086,46 @@ export function Header({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Pause button (soft pause) - always inline */}
|
{/* Engine control split-button: main=stop/start, chevron dropdown=pause triage */}
|
||||||
<button
|
<div className="engine-control-split-btn" ref={engineMenuRef}>
|
||||||
className={`btn-icon${enginePaused ? " btn-icon--paused" : ""}`}
|
<button
|
||||||
onClick={onToggleEnginePause}
|
className={`btn-icon engine-control-split-btn__main${globalPaused ? " btn-icon--stopped" : ""}`}
|
||||||
title={enginePaused ? "Resume scheduling" : "Pause scheduling"}
|
onClick={onToggleGlobalPause}
|
||||||
disabled={!!globalPaused}
|
title={globalPaused ? "Start AI engine" : "Stop AI engine"}
|
||||||
>
|
data-testid="engine-control-main-btn"
|
||||||
{enginePaused ? <Play size={16} /> : <Pause size={16} />}
|
>
|
||||||
</button>
|
{globalPaused ? <Play size={16} /> : <Square size={16} />}
|
||||||
|
</button>
|
||||||
{/* Stop button (hard stop) - always inline */}
|
<span className="engine-control-split-btn__divider" />
|
||||||
<button
|
<button
|
||||||
className={`btn-icon${globalPaused ? " btn-icon--stopped" : ""}`}
|
className={`btn-icon engine-control-split-btn__chevron${isEngineMenuOpen ? " btn-icon--active" : ""}`}
|
||||||
onClick={onToggleGlobalPause}
|
onClick={() => setIsEngineMenuOpen((prev) => !prev)}
|
||||||
title={globalPaused ? "Start AI engine" : "Stop AI engine"}
|
title="Engine options"
|
||||||
>
|
aria-haspopup="menu"
|
||||||
{globalPaused ? <Play size={16} /> : <Square size={16} />}
|
aria-expanded={isEngineMenuOpen}
|
||||||
</button>
|
data-testid="engine-control-chevron-btn"
|
||||||
|
>
|
||||||
|
<ChevronDown size={12} />
|
||||||
|
</button>
|
||||||
|
{isEngineMenuOpen && (
|
||||||
|
<div className="engine-control-split-btn__menu" role="menu">
|
||||||
|
<button
|
||||||
|
className={`engine-control-split-btn__menu-item${enginePaused ? " engine-control-split-btn__menu-item--active" : ""}`}
|
||||||
|
onClick={() => {
|
||||||
|
onToggleEnginePause?.();
|
||||||
|
setIsEngineMenuOpen(false);
|
||||||
|
}}
|
||||||
|
role="menuitem"
|
||||||
|
title={enginePaused ? "Resume scheduling" : "Pause triage"}
|
||||||
|
disabled={!!globalPaused}
|
||||||
|
data-testid="engine-control-pause-triage-btn"
|
||||||
|
>
|
||||||
|
{enginePaused ? <Play size={14} /> : <Pause size={14} />}
|
||||||
|
<span>{enginePaused ? "Resume scheduling" : "Pause triage"}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Settings - always inline on desktop, placed after engine controls */}
|
{/* Settings - always inline on desktop, placed after engine controls */}
|
||||||
{!isCompact && (
|
{!isCompact && (
|
||||||
|
|||||||
@@ -1018,7 +1018,7 @@ describe("App global pause (hard stop)", () => {
|
|||||||
render(<App />);
|
render(<App />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByTitle("Stop AI engine")).toBeTruthy();
|
expect(screen.getByTestId("engine-control-main-btn")).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1032,11 +1032,11 @@ describe("App global pause (hard stop)", () => {
|
|||||||
|
|
||||||
// Wait for initial render
|
// Wait for initial render
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByTitle("Stop AI engine")).toBeTruthy();
|
expect(screen.getByTestId("engine-control-main-btn")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Click the stop button
|
// Click the stop button
|
||||||
fireEvent.click(screen.getByTitle("Stop AI engine"));
|
fireEvent.click(screen.getByTestId("engine-control-main-btn"));
|
||||||
|
|
||||||
// Should optimistically switch to "Start" state
|
// Should optimistically switch to "Start" state
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -1060,15 +1060,15 @@ describe("App global pause (hard stop)", () => {
|
|||||||
render(<App />);
|
render(<App />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByTitle("Stop AI engine")).toBeTruthy();
|
expect(screen.getByTestId("engine-control-main-btn")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Click the stop button — will fail
|
// Click the stop button — will fail
|
||||||
fireEvent.click(screen.getByTitle("Stop AI engine"));
|
fireEvent.click(screen.getByTestId("engine-control-main-btn"));
|
||||||
|
|
||||||
// Should revert back to "Stop" state after failure
|
// Should revert back to "Stop" state after failure
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByTitle("Stop AI engine")).toBeTruthy();
|
expect(screen.getByTestId("engine-control-main-btn")).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1082,10 +1082,11 @@ describe("App engine pause (soft pause)", () => {
|
|||||||
|
|
||||||
render(<App />);
|
render(<App />);
|
||||||
|
|
||||||
// When engine is paused, the pause button should show "Resume scheduling"
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByTitle("Resume scheduling")).toBeTruthy();
|
expect(screen.getByTestId("engine-control-chevron-btn")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
fireEvent.click(screen.getByTestId("engine-control-chevron-btn"));
|
||||||
|
expect(screen.getByTitle("Resume scheduling")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows Pause button when enginePaused is false", async () => {
|
it("shows Pause button when enginePaused is false", async () => {
|
||||||
@@ -1097,7 +1098,7 @@ describe("App engine pause (soft pause)", () => {
|
|||||||
render(<App />);
|
render(<App />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByTitle("Pause scheduling")).toBeTruthy();
|
expect(screen.getByTestId("engine-control-chevron-btn")).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1110,19 +1111,18 @@ describe("App engine pause (soft pause)", () => {
|
|||||||
render(<App />);
|
render(<App />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByTitle("Pause scheduling")).toBeTruthy();
|
expect(screen.getByTestId("engine-control-chevron-btn")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Click the pause button
|
// Click the pause button
|
||||||
fireEvent.click(screen.getByTitle("Pause scheduling"));
|
fireEvent.click(screen.getByTestId("engine-control-chevron-btn"));
|
||||||
|
fireEvent.click(screen.getByTestId("engine-control-pause-triage-btn"));
|
||||||
|
|
||||||
// Should optimistically switch to "Resume" state
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByTitle("Resume scheduling")).toBeTruthy();
|
expect(updateSettings).toHaveBeenCalledWith({ enginePaused: true }, "proj_123");
|
||||||
});
|
});
|
||||||
|
fireEvent.click(screen.getByTestId("engine-control-chevron-btn"));
|
||||||
// Should call updateSettings with enginePaused: true
|
expect(screen.getByTitle("Resume scheduling")).toBeTruthy();
|
||||||
expect(updateSettings).toHaveBeenCalledWith({ enginePaused: true }, "proj_123");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -223,36 +223,40 @@ describe("Header", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("pause controls", () => {
|
describe("pause controls", () => {
|
||||||
it("renders pause button for engine pause", () => {
|
it("renders engine control split-button", () => {
|
||||||
renderHeader();
|
renderHeader();
|
||||||
expect(screen.getByTitle("Pause scheduling")).toBeDefined();
|
expect(screen.getByTestId("engine-control-main-btn")).toBeDefined();
|
||||||
|
expect(screen.getByTestId("engine-control-chevron-btn")).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders stop button for global pause", () => {
|
it("renders pause triage option in dropdown", () => {
|
||||||
renderHeader();
|
renderHeader();
|
||||||
expect(screen.getByTitle("Stop AI engine")).toBeDefined();
|
fireEvent.click(screen.getByTestId("engine-control-chevron-btn"));
|
||||||
|
expect(screen.getByTestId("engine-control-pause-triage-btn")).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("calls onToggleEnginePause when pause button is clicked", () => {
|
it("calls onToggleEnginePause when pause triage item is clicked", () => {
|
||||||
const onToggleEnginePause = vi.fn();
|
const onToggleEnginePause = vi.fn();
|
||||||
renderHeader({ onToggleEnginePause });
|
renderHeader({ onToggleEnginePause });
|
||||||
fireEvent.click(screen.getByTitle("Pause scheduling"));
|
fireEvent.click(screen.getByTestId("engine-control-chevron-btn"));
|
||||||
|
fireEvent.click(screen.getByTestId("engine-control-pause-triage-btn"));
|
||||||
expect(onToggleEnginePause).toHaveBeenCalled();
|
expect(onToggleEnginePause).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("calls onToggleGlobalPause when stop button is clicked", () => {
|
it("calls onToggleGlobalPause when main button is clicked", () => {
|
||||||
const onToggleGlobalPause = vi.fn();
|
const onToggleGlobalPause = vi.fn();
|
||||||
renderHeader({ onToggleGlobalPause });
|
renderHeader({ onToggleGlobalPause });
|
||||||
fireEvent.click(screen.getByTitle("Stop AI engine"));
|
fireEvent.click(screen.getByTestId("engine-control-main-btn"));
|
||||||
expect(onToggleGlobalPause).toHaveBeenCalled();
|
expect(onToggleGlobalPause).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows resume text when engine is paused", () => {
|
it("shows resume text in dropdown when engine is paused", () => {
|
||||||
renderHeader({ enginePaused: true });
|
renderHeader({ enginePaused: true });
|
||||||
|
fireEvent.click(screen.getByTestId("engine-control-chevron-btn"));
|
||||||
expect(screen.getByTitle("Resume scheduling")).toBeDefined();
|
expect(screen.getByTitle("Resume scheduling")).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows start text when global is paused", () => {
|
it("shows start AI engine title on main button when global is paused", () => {
|
||||||
renderHeader({ globalPaused: true });
|
renderHeader({ globalPaused: true });
|
||||||
expect(screen.getByTitle("Start AI engine")).toBeDefined();
|
expect(screen.getByTitle("Start AI engine")).toBeDefined();
|
||||||
});
|
});
|
||||||
@@ -1171,25 +1175,27 @@ describe("Header", () => {
|
|||||||
onRunScript: noop,
|
onRunScript: noop,
|
||||||
}, "desktop");
|
}, "desktop");
|
||||||
|
|
||||||
// Get all inline btn-icon buttons inside header-actions
|
// Get direct children of header-actions: top-level btn-icon buttons AND the split-button container
|
||||||
const headerActions = container.querySelector(".header-actions")!;
|
const headerActions = container.querySelector(".header-actions")!;
|
||||||
const inlineButtons = Array.from(headerActions.querySelectorAll<HTMLButtonElement>(":scope > button.btn-icon"));
|
const inlineItems = Array.from(
|
||||||
|
headerActions.querySelectorAll<HTMLElement>(
|
||||||
|
":scope > button.btn-icon, :scope > .engine-control-split-btn"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
// Find the Settings button index and the Pause/Stop button indices
|
const settingsIdx = inlineItems.findIndex(
|
||||||
const settingsIdx = inlineButtons.findIndex((btn) => btn.title === "Settings");
|
(el) => el instanceof HTMLButtonElement && el.title === "Settings"
|
||||||
const pauseIdx = inlineButtons.findIndex((btn) => btn.title === "Pause scheduling" || btn.title === "Resume scheduling");
|
);
|
||||||
const stopIdx = inlineButtons.findIndex((btn) => btn.title === "Stop AI engine" || btn.title === "Start AI engine");
|
const splitBtnIdx = inlineItems.findIndex((el) =>
|
||||||
|
el.classList.contains("engine-control-split-btn")
|
||||||
|
);
|
||||||
|
|
||||||
// Settings must exist
|
|
||||||
expect(settingsIdx).toBeGreaterThanOrEqual(0);
|
expect(settingsIdx).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(splitBtnIdx).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(settingsIdx).toBeGreaterThan(splitBtnIdx);
|
||||||
|
|
||||||
// Settings must come after pause and stop (engine controls come before Settings)
|
const itemsAfterSettings = inlineItems.slice(settingsIdx + 1);
|
||||||
expect(settingsIdx).toBeGreaterThan(pauseIdx);
|
expect(itemsAfterSettings).toHaveLength(0);
|
||||||
expect(settingsIdx).toBeGreaterThan(stopIdx);
|
|
||||||
|
|
||||||
// Settings must be the very last button — no buttons after it
|
|
||||||
const buttonsAfterSettings = inlineButtons.slice(settingsIdx + 1);
|
|
||||||
expect(buttonsAfterSettings).toHaveLength(0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Settings is the last item in the mobile overflow menu", () => {
|
it("Settings is the last item in the mobile overflow menu", () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@fusion/dashboard",
|
"name": "@fusion/dashboard",
|
||||||
"version": "0.3.0",
|
"version": "1.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"description": "Fusion dashboard: React UI and HTTP API server for monitoring and controlling the Fusion AI coding agent.",
|
"description": "Fusion dashboard: React UI and HTTP API server for monitoring and controlling the Fusion AI coding agent.",
|
||||||
"homepage": "https://github.com/Runfusion/Fusion#readme",
|
"homepage": "https://github.com/Runfusion/Fusion#readme",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# @fusion/desktop
|
# @fusion/desktop
|
||||||
|
|
||||||
## 0.3.0
|
## 1.0.0
|
||||||
|
|
||||||
## 0.2.7
|
## 0.2.7
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@fusion/desktop",
|
"name": "@fusion/desktop",
|
||||||
"version": "0.3.0",
|
"version": "1.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"description": "Fusion desktop: Electron wrapper around the Fusion dashboard for macOS, Windows, and Linux.",
|
"description": "Fusion desktop: Electron wrapper around the Fusion dashboard for macOS, Windows, and Linux.",
|
||||||
"homepage": "https://github.com/Runfusion/Fusion#readme",
|
"homepage": "https://github.com/Runfusion/Fusion#readme",
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
# @fusion/engine
|
# @fusion/engine
|
||||||
|
|
||||||
## 0.3.0
|
## 1.0.0
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|
||||||
- @fusion/core@0.3.0
|
- @fusion/core@1.0.0
|
||||||
- @fusion/pi-claude-cli@0.3.0
|
- @fusion/pi-claude-cli@1.0.0
|
||||||
|
|
||||||
## 0.2.7
|
## 0.2.7
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@fusion/engine",
|
"name": "@fusion/engine",
|
||||||
"version": "0.3.0",
|
"version": "1.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"description": "Fusion engine: executor, merger, scheduler, and automation runtime for the Fusion AI coding agent.",
|
"description": "Fusion engine: executor, merger, scheduler, and automation runtime for the Fusion AI coding agent.",
|
||||||
"homepage": "https://github.com/Runfusion/Fusion#readme",
|
"homepage": "https://github.com/Runfusion/Fusion#readme",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# @fusion/mobile
|
# @fusion/mobile
|
||||||
|
|
||||||
## 0.3.0
|
## 1.0.0
|
||||||
|
|
||||||
## 0.2.7
|
## 0.2.7
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@fusion/mobile",
|
"name": "@fusion/mobile",
|
||||||
"version": "0.3.0",
|
"version": "1.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"description": "Fusion mobile: Capacitor wrapper around the Fusion dashboard for iOS and Android.",
|
"description": "Fusion mobile: Capacitor wrapper around the Fusion dashboard for iOS and Android.",
|
||||||
"homepage": "https://github.com/Runfusion/Fusion#readme",
|
"homepage": "https://github.com/Runfusion/Fusion#readme",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# @fusion/pi-claude-cli
|
# @fusion/pi-claude-cli
|
||||||
|
|
||||||
## 0.3.0
|
## 1.0.0
|
||||||
|
|
||||||
## 0.2.7
|
## 0.2.7
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@fusion/pi-claude-cli",
|
"name": "@fusion/pi-claude-cli",
|
||||||
"version": "0.3.0",
|
"version": "1.0.0",
|
||||||
"description": "Fusion vendored fork: pi coding-agent extension that routes LLM calls through the Claude Code CLI. Forked from rchern/pi-claude-cli (MIT). See UPSTREAM.md.",
|
"description": "Fusion vendored fork: pi coding-agent extension that routes LLM calls through the Claude Code CLI. Forked from rchern/pi-claude-cli (MIT). See UPSTREAM.md.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
# @fusion/plugin-sdk
|
# @fusion/plugin-sdk
|
||||||
|
|
||||||
## 0.3.0
|
## 1.0.0
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|
||||||
- @fusion/core@0.3.0
|
- @fusion/core@1.0.0
|
||||||
|
|
||||||
## 0.2.7
|
## 0.2.7
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@fusion/plugin-sdk",
|
"name": "@fusion/plugin-sdk",
|
||||||
"version": "0.3.0",
|
"version": "1.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"description": "Fusion plugin SDK: types and helpers for authoring third-party plugins that extend the Fusion dashboard and engine.",
|
"description": "Fusion plugin SDK: types and helpers for authoring third-party plugins that extend the Fusion dashboard and engine.",
|
||||||
"homepage": "https://github.com/Runfusion/Fusion#readme",
|
"homepage": "https://github.com/Runfusion/Fusion#readme",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|
||||||
- @fusion/plugin-sdk@0.3.0
|
- @fusion/plugin-sdk@1.0.0
|
||||||
|
|
||||||
## 0.1.2
|
## 0.1.2
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|
||||||
- @fusion/plugin-sdk@0.3.0
|
- @fusion/plugin-sdk@1.0.0
|
||||||
|
|
||||||
## 0.1.2
|
## 0.1.2
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|
||||||
- @fusion/plugin-sdk@0.3.0
|
- @fusion/plugin-sdk@1.0.0
|
||||||
|
|
||||||
## 0.1.2
|
## 0.1.2
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|
||||||
- @fusion/plugin-sdk@0.3.0
|
- @fusion/plugin-sdk@1.0.0
|
||||||
|
|
||||||
## 0.1.2
|
## 0.1.2
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|
||||||
- @fusion/engine@0.3.0
|
- @fusion/engine@1.0.0
|
||||||
- @fusion/plugin-sdk@0.3.0
|
- @fusion/plugin-sdk@1.0.0
|
||||||
|
|
||||||
## 0.1.2
|
## 0.1.2
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|
||||||
- @fusion/engine@0.3.0
|
- @fusion/engine@1.0.0
|
||||||
- @fusion/plugin-sdk@0.3.0
|
- @fusion/plugin-sdk@1.0.0
|
||||||
|
|
||||||
## 0.1.2
|
## 0.1.2
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|
||||||
- @fusion/engine@0.3.0
|
- @fusion/engine@1.0.0
|
||||||
- @fusion/plugin-sdk@0.3.0
|
- @fusion/plugin-sdk@1.0.0
|
||||||
|
|
||||||
## 0.1.2
|
## 0.1.2
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user