feat(FN-3899): add chat rooms with send routing and delete-room UI
This merge delivers chat rooms with room send routing and delete-room UI in the dashboard (FN-3899), including fixes for bundled plugin view imports and a release changeset. Supporting changes include a merger improvement that tightens scope-warning diff base when baseBranch is missing, companies.sh Fusion-Task-Id: FN-3899
This commit is contained in:
5
.changeset/default-verification-fix-retries-2.md
Normal file
5
.changeset/default-verification-fix-retries-2.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Change the default `verificationFixRetries` setting from 3 to 2 for new projects and fallback behavior when unset.
|
||||||
5
.changeset/fn-3899-chat-rooms-fixes.md
Normal file
5
.changeset/fn-3899-chat-rooms-fixes.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix chat rooms: pressing Enter in a room now posts to the room (previously routed to a 1-on-1 session), and rooms can now be deleted from the rooms sidebar with confirmation.
|
||||||
@@ -207,7 +207,7 @@ Defaults from `DEFAULT_PROJECT_SETTINGS`; key scope from `PROJECT_SETTINGS_KEYS`
|
|||||||
| `smartConflictResolution` | `boolean` | `true` | Alias/preferred flag for smart conflict handling. |
|
| `smartConflictResolution` | `boolean` | `true` | Alias/preferred flag for smart conflict handling. |
|
||||||
| `strictScopeEnforcement` | `boolean` | `false` | Block merges on out-of-scope file changes. |
|
| `strictScopeEnforcement` | `boolean` | `false` | Block merges on out-of-scope file changes. |
|
||||||
| `buildRetryCount` | `number` | `0` | Build retry attempts during merge. |
|
| `buildRetryCount` | `number` | `0` | Build retry attempts during merge. |
|
||||||
| `verificationFixRetries` | `number` | `3` | Auto-fix retry attempts when verification fails during merge. |
|
| `verificationFixRetries` | `number` | `2` | Auto-fix retry attempts when verification fails during merge. |
|
||||||
| `buildTimeoutMs` | `number` | `300000` | Build timeout in milliseconds (5 minutes). |
|
| `buildTimeoutMs` | `number` | `300000` | Build timeout in milliseconds (5 minutes). |
|
||||||
| `requirePlanApproval` | `boolean` | `false` | Require manual approval before planning → todo. |
|
| `requirePlanApproval` | `boolean` | `false` | Require manual approval before planning → todo. |
|
||||||
| `completionDocumentationMode` | `"off" \| "changeset" \| "changelog"` | `"off"` | Controls triage prompt injection for release-note artifacts in future task specs. `"changeset"` requires `.changeset/*.md` workflow guidance; `"changelog"` requires updating an existing changelog file (without inventing a new one); `"off"` disables this automation. |
|
| `completionDocumentationMode` | `"off" \| "changeset" \| "changelog"` | `"off"` | Controls triage prompt injection for release-note artifacts in future task specs. `"changeset"` requires `.changeset/*.md` workflow guidance; `"changelog"` requires updating an existing changelog file (without inventing a new one); `"off"` disables this automation. |
|
||||||
|
|||||||
@@ -109,15 +109,16 @@
|
|||||||
.chat-room-item {
|
.chat-room-item {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
align-items: center;
|
||||||
align-items: flex-start;
|
justify-content: space-between;
|
||||||
gap: var(--space-xs);
|
gap: var(--space-sm);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
padding: var(--space-sm) var(--space-md);
|
padding: var(--space-sm) var(--space-md);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-room-item:hover {
|
.chat-room-item:hover {
|
||||||
@@ -133,6 +134,13 @@
|
|||||||
background: color-mix(in srgb, var(--todo) 12%, transparent);
|
background: color-mix(in srgb, var(--todo) 12%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-room-item-details {
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
.chat-room-item-name {
|
.chat-room-item-name {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
@@ -142,6 +150,15 @@
|
|||||||
font-size: var(--space-sm);
|
font-size: var(--space-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-room-item-delete {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-room-item-delete:hover,
|
||||||
|
.chat-room-item-delete:focus-visible {
|
||||||
|
color: var(--color-error);
|
||||||
|
}
|
||||||
|
|
||||||
.chat-room-thread-header {
|
.chat-room-thread-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1402,6 +1419,14 @@
|
|||||||
min-width: 36px;
|
min-width: 36px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-room-item {
|
||||||
|
padding: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-room-item-delete {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.chat-room-thread-members {
|
.chat-room-thread-members {
|
||||||
max-width: 50%;
|
max-width: 50%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -736,6 +736,7 @@ export function ChatView({ projectId, addToast }: ChatViewProps) {
|
|||||||
const [messageInput, setMessageInput] = useState("");
|
const [messageInput, setMessageInput] = useState("");
|
||||||
const [contextMenu, setContextMenu] = useState<{ sessionId: string; x: number; y: number } | null>(null);
|
const [contextMenu, setContextMenu] = useState<{ sessionId: string; x: number; y: number } | null>(null);
|
||||||
const [confirmDelete, setConfirmDelete] = useState<string | null>(null);
|
const [confirmDelete, setConfirmDelete] = useState<string | null>(null);
|
||||||
|
const [confirmDeleteRoomId, setConfirmDeleteRoomId] = useState<string | null>(null);
|
||||||
const [sidebarVisible, setSidebarVisible] = useState(true);
|
const [sidebarVisible, setSidebarVisible] = useState(true);
|
||||||
const [sidebarWidth, setSidebarWidth] = useState(CHAT_SIDEBAR_DEFAULT_WIDTH);
|
const [sidebarWidth, setSidebarWidth] = useState(CHAT_SIDEBAR_DEFAULT_WIDTH);
|
||||||
const [chatScope, setChatScope] = useState<"direct" | "rooms">("direct");
|
const [chatScope, setChatScope] = useState<"direct" | "rooms">("direct");
|
||||||
@@ -1222,6 +1223,24 @@ export function ChatView({ projectId, addToast }: ChatViewProps) {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
const handleSendDispatch = useCallback(async () => {
|
||||||
|
const trimmed = messageInput.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chatScope === "rooms") {
|
||||||
|
if (!rooms.activeRoom) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await rooms.sendRoomMessage(trimmed);
|
||||||
|
setMessageInput("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSend();
|
||||||
|
}, [messageInput, chatScope, rooms, handleSend]);
|
||||||
|
|
||||||
const handleSkillSelect = useCallback(
|
const handleSkillSelect = useCallback(
|
||||||
(skill: DiscoveredSkill) => {
|
(skill: DiscoveredSkill) => {
|
||||||
setMessageInput((currentInput) => {
|
setMessageInput((currentInput) => {
|
||||||
@@ -1376,7 +1395,7 @@ export function ChatView({ projectId, addToast }: ChatViewProps) {
|
|||||||
|
|
||||||
if (e.key === "Enter" && !e.shiftKey) {
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
void handleSend();
|
void handleSendDispatch();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
@@ -1388,7 +1407,7 @@ export function ChatView({ projectId, addToast }: ChatViewProps) {
|
|||||||
filteredSkills,
|
filteredSkills,
|
||||||
highlightedSkillIndex,
|
highlightedSkillIndex,
|
||||||
handleSkillSelect,
|
handleSkillSelect,
|
||||||
handleSend,
|
handleSendDispatch,
|
||||||
fileMention,
|
fileMention,
|
||||||
messageInput,
|
messageInput,
|
||||||
],
|
],
|
||||||
@@ -1857,9 +1876,10 @@ export function ChatView({ projectId, addToast }: ChatViewProps) {
|
|||||||
const isActive = rooms.activeRoom?.id === room.id;
|
const isActive = rooms.activeRoom?.id === room.id;
|
||||||
const memberCount = isActive ? rooms.activeRoomMembers.length : "—";
|
const memberCount = isActive ? rooms.activeRoomMembers.length : "—";
|
||||||
return (
|
return (
|
||||||
<button
|
<div
|
||||||
key={room.id}
|
key={room.id}
|
||||||
type="button"
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
className={`chat-room-item${isActive ? " chat-room-item--active" : ""}`}
|
className={`chat-room-item${isActive ? " chat-room-item--active" : ""}`}
|
||||||
data-testid={`chat-room-item-${room.slug}`}
|
data-testid={`chat-room-item-${room.slug}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -1868,10 +1888,33 @@ export function ChatView({ projectId, addToast }: ChatViewProps) {
|
|||||||
setSidebarVisible(false);
|
setSidebarVisible(false);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
rooms.selectRoom(room.id);
|
||||||
|
if (isMobile) {
|
||||||
|
setSidebarVisible(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<span className="chat-room-item-name">#{room.name}</span>
|
<span className="chat-room-item-details">
|
||||||
<span className="chat-room-item-meta">{memberCount} {memberCount === 1 ? "member" : "members"}</span>
|
<span className="chat-room-item-name">#{room.name}</span>
|
||||||
</button>
|
<span className="chat-room-item-meta">{memberCount} {memberCount === 1 ? "member" : "members"}</span>
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-icon chat-room-item-delete"
|
||||||
|
data-testid={`chat-room-delete-${room.slug}`}
|
||||||
|
aria-label={`Delete room ${room.name}`}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
setConfirmDeleteRoomId(room.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 size={14} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@@ -1956,6 +1999,36 @@ export function ChatView({ projectId, addToast }: ChatViewProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{confirmDeleteRoomId && (
|
||||||
|
<div className="chat-new-dialog-backdrop chat-view-dialog-backdrop" onClick={() => setConfirmDeleteRoomId(null)}>
|
||||||
|
<div className="chat-new-dialog chat-view-dialog" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<h3>Delete Room?</h3>
|
||||||
|
<p className="chat-view-delete-dialog-copy">
|
||||||
|
This action cannot be undone. This room and all its messages will be permanently deleted.
|
||||||
|
</p>
|
||||||
|
<div className="chat-new-dialog-actions">
|
||||||
|
<button className="btn btn-sm" onClick={() => setConfirmDeleteRoomId(null)}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-danger"
|
||||||
|
onClick={() => {
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
await rooms.deleteRoom(confirmDeleteRoomId);
|
||||||
|
setConfirmDeleteRoomId(null);
|
||||||
|
} catch {
|
||||||
|
addToast("Failed to delete room", "error");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{/* Thread */}
|
{/* Thread */}
|
||||||
{chatScope === "rooms" ? (
|
{chatScope === "rooms" ? (
|
||||||
<div className="chat-thread">
|
<div className="chat-thread">
|
||||||
@@ -2038,11 +2111,7 @@ export function ChatView({ projectId, addToast }: ChatViewProps) {
|
|||||||
type="button"
|
type="button"
|
||||||
className="chat-input-send"
|
className="chat-input-send"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const trimmed = messageInput.trim();
|
void handleSendDispatch();
|
||||||
if (!trimmed) return;
|
|
||||||
void rooms.sendRoomMessage(trimmed).then(() => {
|
|
||||||
setMessageInput("");
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
disabled={!messageInput.trim()}
|
disabled={!messageInput.trim()}
|
||||||
data-testid="chat-send-btn"
|
data-testid="chat-send-btn"
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ const mockUseChat = vi.mocked(useChatModule.useChat);
|
|||||||
const mockUseChatRooms = vi.mocked(useChatRoomsModule.useChatRooms);
|
const mockUseChatRooms = vi.mocked(useChatRoomsModule.useChatRooms);
|
||||||
const mockUseViewportMode = vi.mocked(headerModule.useViewportMode);
|
const mockUseViewportMode = vi.mocked(headerModule.useViewportMode);
|
||||||
|
|
||||||
|
const mockCreateSession = vi.fn();
|
||||||
|
const mockSendMessage = vi.fn();
|
||||||
|
|
||||||
function buildRoomsMock(overrides: Partial<UseChatRoomsResult> = {}): UseChatRoomsResult {
|
function buildRoomsMock(overrides: Partial<UseChatRoomsResult> = {}): UseChatRoomsResult {
|
||||||
return {
|
return {
|
||||||
rooms: [],
|
rooms: [],
|
||||||
@@ -43,11 +46,13 @@ function buildRoomsMock(overrides: Partial<UseChatRoomsResult> = {}): UseChatRoo
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
mockUseViewportMode.mockReturnValue("desktop");
|
mockUseViewportMode.mockReturnValue("desktop");
|
||||||
|
mockCreateSession.mockReset();
|
||||||
|
mockSendMessage.mockReset();
|
||||||
mockUseChat.mockReturnValue({
|
mockUseChat.mockReturnValue({
|
||||||
sessions: [], activeSession: null, sessionsLoading: false, messages: [], messagesLoading: false,
|
sessions: [], activeSession: null, sessionsLoading: false, messages: [], messagesLoading: false,
|
||||||
isStreaming: false, streamingText: "", streamingThinking: "", streamingToolCalls: [],
|
isStreaming: false, streamingText: "", streamingThinking: "", streamingToolCalls: [],
|
||||||
selectSession: vi.fn(), createSession: vi.fn(), archiveSession: vi.fn(), deleteSession: vi.fn(),
|
selectSession: vi.fn(), createSession: mockCreateSession, archiveSession: vi.fn(), deleteSession: vi.fn(),
|
||||||
sendMessage: vi.fn(), stopStreaming: vi.fn(), pendingMessage: "", clearPendingMessage: vi.fn(),
|
sendMessage: mockSendMessage, stopStreaming: vi.fn(), pendingMessage: "", clearPendingMessage: vi.fn(),
|
||||||
loadMoreMessages: vi.fn(), hasMoreMessages: false, searchQuery: "", setSearchQuery: vi.fn(),
|
loadMoreMessages: vi.fn(), hasMoreMessages: false, searchQuery: "", setSearchQuery: vi.fn(),
|
||||||
filteredSessions: [], refreshSessions: vi.fn(), agentsMap: new Map(),
|
filteredSessions: [], refreshSessions: vi.fn(), agentsMap: new Map(),
|
||||||
} as any);
|
} as any);
|
||||||
@@ -117,6 +122,66 @@ describe("ChatView rooms", () => {
|
|||||||
await waitFor(() => expect(screen.getByTestId("chat-input")).toHaveValue(""));
|
await waitFor(() => expect(screen.getByTestId("chat-input")).toHaveValue(""));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("pressing Enter in rooms sends to room and not direct session path", async () => {
|
||||||
|
const roomsMock = buildRoomsMock({
|
||||||
|
activeRoom: { id: "room-1", name: "engineering", slug: "engineering", description: null, projectId: "proj-1", createdBy: null, status: "active", createdAt: "", updatedAt: "2026-05-09T00:00:00.000Z" },
|
||||||
|
});
|
||||||
|
mockUseChatRooms.mockReturnValue(roomsMock);
|
||||||
|
|
||||||
|
render(<ChatView addToast={vi.fn()} projectId="proj-1" />);
|
||||||
|
await userEvent.click(screen.getByTestId("chat-sidebar-scope-rooms"));
|
||||||
|
await userEvent.type(screen.getByTestId("chat-input"), " hello room from enter ");
|
||||||
|
await userEvent.keyboard("{Enter}");
|
||||||
|
|
||||||
|
await waitFor(() => expect(roomsMock.sendRoomMessage).toHaveBeenCalledWith("hello room from enter"));
|
||||||
|
expect(mockCreateSession).not.toHaveBeenCalled();
|
||||||
|
expect(mockSendMessage).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("opens room delete dialog without selecting room", async () => {
|
||||||
|
const roomsMock = buildRoomsMock({
|
||||||
|
rooms: [{ id: "room-1", name: "engineering", slug: "engineering", description: null, projectId: "proj-1", createdBy: null, status: "active", createdAt: "", updatedAt: "2026-05-09T00:00:00.000Z" }],
|
||||||
|
});
|
||||||
|
mockUseChatRooms.mockReturnValue(roomsMock);
|
||||||
|
|
||||||
|
render(<ChatView addToast={vi.fn()} projectId="proj-1" />);
|
||||||
|
await userEvent.click(screen.getByTestId("chat-sidebar-scope-rooms"));
|
||||||
|
await userEvent.click(screen.getByTestId("chat-room-delete-engineering"));
|
||||||
|
|
||||||
|
expect(screen.getByText("Delete Room?")).toBeInTheDocument();
|
||||||
|
expect(roomsMock.selectRoom).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("confirms room delete", async () => {
|
||||||
|
const roomsMock = buildRoomsMock({
|
||||||
|
rooms: [{ id: "room-1", name: "engineering", slug: "engineering", description: null, projectId: "proj-1", createdBy: null, status: "active", createdAt: "", updatedAt: "2026-05-09T00:00:00.000Z" }],
|
||||||
|
});
|
||||||
|
mockUseChatRooms.mockReturnValue(roomsMock);
|
||||||
|
|
||||||
|
render(<ChatView addToast={vi.fn()} projectId="proj-1" />);
|
||||||
|
await userEvent.click(screen.getByTestId("chat-sidebar-scope-rooms"));
|
||||||
|
await userEvent.click(screen.getByTestId("chat-room-delete-engineering"));
|
||||||
|
await userEvent.click(screen.getByRole("button", { name: "Delete" }));
|
||||||
|
|
||||||
|
await waitFor(() => expect(roomsMock.deleteRoom).toHaveBeenCalledWith("room-1"));
|
||||||
|
expect(roomsMock.deleteRoom).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cancels room delete without deleting", async () => {
|
||||||
|
const roomsMock = buildRoomsMock({
|
||||||
|
rooms: [{ id: "room-1", name: "engineering", slug: "engineering", description: null, projectId: "proj-1", createdBy: null, status: "active", createdAt: "", updatedAt: "2026-05-09T00:00:00.000Z" }],
|
||||||
|
});
|
||||||
|
mockUseChatRooms.mockReturnValue(roomsMock);
|
||||||
|
|
||||||
|
render(<ChatView addToast={vi.fn()} projectId="proj-1" />);
|
||||||
|
await userEvent.click(screen.getByTestId("chat-sidebar-scope-rooms"));
|
||||||
|
await userEvent.click(screen.getByTestId("chat-room-delete-engineering"));
|
||||||
|
await userEvent.click(screen.getByRole("button", { name: "Cancel" }));
|
||||||
|
|
||||||
|
expect(screen.queryByText("Delete Room?")).not.toBeInTheDocument();
|
||||||
|
expect(roomsMock.deleteRoom).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("renders newly appended messages from hook updates", async () => {
|
it("renders newly appended messages from hook updates", async () => {
|
||||||
const state = buildRoomsMock({
|
const state = buildRoomsMock({
|
||||||
activeRoom: { id: "room-1", name: "engineering", slug: "engineering", description: null, projectId: "proj-1", createdBy: null, status: "active", createdAt: "", updatedAt: "2026-05-09T00:00:00.000Z" },
|
activeRoom: { id: "room-1", name: "engineering", slug: "engineering", description: null, projectId: "proj-1", createdBy: null, status: "active", createdAt: "", updatedAt: "2026-05-09T00:00:00.000Z" },
|
||||||
|
|||||||
@@ -1,8 +1,29 @@
|
|||||||
import { lazy } from "react";
|
import { lazy } from "react";
|
||||||
|
import type { ComponentType } from "react";
|
||||||
import { registerPluginView } from "./pluginViewRegistry";
|
import { registerPluginView } from "./pluginViewRegistry";
|
||||||
|
|
||||||
let registered = false;
|
let registered = false;
|
||||||
|
|
||||||
|
function createMissingPluginView(moduleId: string): ComponentType {
|
||||||
|
return function MissingPluginView() {
|
||||||
|
return `Bundled plugin view unavailable: ${moduleId}`;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadBundledPluginView(moduleId: string, exportName: string) {
|
||||||
|
try {
|
||||||
|
const mod = await import(/* @vite-ignore */ moduleId) as Record<string, ComponentType>;
|
||||||
|
const component = mod[exportName];
|
||||||
|
if (component) {
|
||||||
|
return { default: component };
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Fall back to placeholder view when optional bundled plugin examples are unavailable.
|
||||||
|
}
|
||||||
|
|
||||||
|
return { default: createMissingPluginView(moduleId) };
|
||||||
|
}
|
||||||
|
|
||||||
export function registerBundledPluginViews(): void {
|
export function registerBundledPluginViews(): void {
|
||||||
if (registered) return;
|
if (registered) return;
|
||||||
registered = true;
|
registered = true;
|
||||||
@@ -10,18 +31,12 @@ export function registerBundledPluginViews(): void {
|
|||||||
registerPluginView(
|
registerPluginView(
|
||||||
"fusion-plugin-dependency-graph",
|
"fusion-plugin-dependency-graph",
|
||||||
"graph",
|
"graph",
|
||||||
lazy(async () => {
|
lazy(() => loadBundledPluginView("@fusion-plugin-examples/dependency-graph/dashboard-view", "DependencyGraphDashboardView")),
|
||||||
const mod = await import("@fusion-plugin-examples/dependency-graph/dashboard-view");
|
|
||||||
return { default: mod.DependencyGraphDashboardView };
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
registerPluginView(
|
registerPluginView(
|
||||||
"roadmap-planner",
|
"roadmap-planner",
|
||||||
"roadmaps",
|
"roadmaps",
|
||||||
lazy(async () => {
|
lazy(() => loadBundledPluginView("@fusion-plugin-examples/roadmap/dashboard-view", "RoadmapDashboardView")),
|
||||||
const mod = await import("@fusion-plugin-examples/roadmap/dashboard-view");
|
|
||||||
return { default: mod.RoadmapDashboardView };
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7618,7 +7618,7 @@ describe("aiMergeTask — in-merge verification fix", () => {
|
|||||||
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(4);
|
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(4);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("default verificationFixRetries (omitted) results in 3 fix attempts", async () => {
|
it("default verificationFixRetries (omitted) results in 2 fix attempts", async () => {
|
||||||
mockedExecSync.mockImplementation((cmd: any) => {
|
mockedExecSync.mockImplementation((cmd: any) => {
|
||||||
const cmdStr = String(cmd);
|
const cmdStr = String(cmd);
|
||||||
if (cmdStr.includes("rev-parse --verify")) return Buffer.from("abc123");
|
if (cmdStr.includes("rev-parse --verify")) return Buffer.from("abc123");
|
||||||
@@ -7658,26 +7658,25 @@ describe("aiMergeTask — in-merge verification fix", () => {
|
|||||||
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||||
...DEFAULT_SETTINGS,
|
...DEFAULT_SETTINGS,
|
||||||
testCommand: "vitest run",
|
testCommand: "vitest run",
|
||||||
// verificationFixRetries is NOT set — should default to 3
|
// verificationFixRetries is NOT set — should default to 2
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(aiMergeTask(store, "/tmp/root", "FN-050")).rejects.toMatchObject({
|
await expect(aiMergeTask(store, "/tmp/root", "FN-050")).rejects.toMatchObject({
|
||||||
name: "VerificationError",
|
name: "VerificationError",
|
||||||
});
|
});
|
||||||
|
|
||||||
// 1 merger AI agent (attempt 1) + 3 fix agent attempts (default) = 4 calls
|
// 1 merger AI agent (attempt 1) + 2 fix agent attempts (default) = 3 calls
|
||||||
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(4);
|
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(3);
|
||||||
|
|
||||||
// Verify the log shows 3 fix attempts (2 log entries per attempt: start + failure)
|
// Verify the log shows 2 fix attempts (2 log entries per attempt: start + failure)
|
||||||
const logCalls = (store.logEntry as ReturnType<typeof vi.fn>).mock.calls;
|
const logCalls = (store.logEntry as ReturnType<typeof vi.fn>).mock.calls;
|
||||||
const fixAttempts = logCalls.filter((call: any[]) =>
|
const fixAttempts = logCalls.filter((call: any[]) =>
|
||||||
typeof call[1] === "string" && call[1].includes("In-merge verification fix attempt"),
|
typeof call[1] === "string" && call[1].includes("In-merge verification fix attempt"),
|
||||||
);
|
);
|
||||||
// Each attempt produces 2 log entries: "attempt X/3" and "attempt X — verification still fails"
|
// Each attempt produces 2 log entries: "attempt X/2" and "attempt X — verification still fails"
|
||||||
expect(fixAttempts).toHaveLength(6);
|
expect(fixAttempts).toHaveLength(4);
|
||||||
expect(fixAttempts[0][1]).toContain("attempt 1/3");
|
expect(fixAttempts[0][1]).toContain("attempt 1/2");
|
||||||
expect(fixAttempts[2][1]).toContain("attempt 2/3");
|
expect(fixAttempts[2][1]).toContain("attempt 2/2");
|
||||||
expect(fixAttempts[4][1]).toContain("attempt 3/3");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5085,7 +5085,7 @@ export async function aiMergeTask(
|
|||||||
// Try in-merge fix attempts before propagating
|
// Try in-merge fix attempts before propagating
|
||||||
if (error.name === "VerificationError") {
|
if (error.name === "VerificationError") {
|
||||||
const verificationErr = error as VerificationError;
|
const verificationErr = error as VerificationError;
|
||||||
const maxFixRetries = Math.min(settings.verificationFixRetries ?? 3, 3);
|
const maxFixRetries = Math.min(settings.verificationFixRetries ?? 2, 3);
|
||||||
|
|
||||||
if (maxFixRetries > 0 && (verificationErr.verificationResult.testResult || verificationErr.verificationResult.buildResult)) {
|
if (maxFixRetries > 0 && (verificationErr.verificationResult.testResult || verificationErr.verificationResult.buildResult)) {
|
||||||
mergerLog.log(`${taskId}: deterministic verification failed — attempting in-merge fix (up to ${maxFixRetries} attempts)`);
|
mergerLog.log(`${taskId}: deterministic verification failed — attempting in-merge fix (up to ${maxFixRetries} attempts)`);
|
||||||
@@ -5216,7 +5216,7 @@ export async function aiMergeTask(
|
|||||||
|
|
||||||
// Check if it's a build verification failure
|
// Check if it's a build verification failure
|
||||||
if (error.message?.includes("Build verification failed")) {
|
if (error.message?.includes("Build verification failed")) {
|
||||||
const maxFixRetries = Math.min(settings.verificationFixRetries ?? 3, 3);
|
const maxFixRetries = Math.min(settings.verificationFixRetries ?? 2, 3);
|
||||||
|
|
||||||
// Try in-merge fix before falling back to build retry
|
// Try in-merge fix before falling back to build retry
|
||||||
if (maxFixRetries > 0 && (effectiveTestCommand || effectiveBuildCommand)) {
|
if (maxFixRetries > 0 && (effectiveTestCommand || effectiveBuildCommand)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user