fix(ui): toolbar CLI sans overlap + retrait du bouton History — #151 #160 (QA verte)

This commit is contained in:
2026-08-06 13:13:30 +02:00
parent 309c10e5e9
commit 03303aa276
2 changed files with 55 additions and 31 deletions

View File

@ -9,6 +9,7 @@
*/
import { describe, it, expect, vi } from "vitest";
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
import type { ComponentProps } from "react";
vi.mock("@xterm/xterm", () => ({
Terminal: class {
@ -54,6 +55,7 @@ import {
MockProfileGateway,
MockSystemGateway,
MockTerminalGateway,
MockWorkStateGateway,
} from "@/adapters/mock";
import { DIProvider } from "@/app/di";
import { AnnouncementsProvider } from "@/features/announcements";
@ -66,6 +68,7 @@ interface GridSetup {
gateways: Gateways;
layout: MockLayoutGateway;
system: MockSystemGateway;
workState: MockWorkStateGateway;
}
async function makeSplitAgentGrid(): Promise<GridSetup> {
@ -74,6 +77,7 @@ async function makeSplitAgentGrid(): Promise<GridSetup> {
const profileGateway = new MockProfileGateway();
const terminal = new MockTerminalGateway();
const system = new MockSystemGateway();
const workState = new MockWorkStateGateway();
await profileGateway.saveProfile(MOCK_REFERENCE_PROFILES[0]);
const agent = await agentGateway.createAgent("p1", {
@ -105,17 +109,22 @@ async function makeSplitAgentGrid(): Promise<GridSetup> {
profile: profileGateway,
terminal,
system,
workState,
} as unknown as Gateways,
layout,
system,
workState,
};
}
function renderGrid(gateways: Gateways) {
function renderGrid(
gateways: Gateways,
props: Partial<ComponentProps<typeof LayoutGrid>> = {},
) {
return render(
<DIProvider gateways={gateways}>
<AnnouncementsProvider>
<LayoutGrid projectId="p1" cwd="/home/me/proj" />
<LayoutGrid projectId="p1" cwd="/home/me/proj" {...props} />
</AnnouncementsProvider>
</DIProvider>,
);
@ -197,6 +206,44 @@ describe("LayoutGrid — ticket #48 cell control layering", () => {
expect(controls.style.overflow).toBe("hidden");
});
it("keeps busy Cancel above persistent cell controls and removes History (#151, #160)", async () => {
const setup = await makeSplitAgentGrid();
await setup.layout.mutateLayout("p1", {
type: "setCellConversation",
target: setup.bId,
conversationId: "prior-conv",
});
setup.workState._setProjectWorkState("p1", {
agents: [
{
agentId: setup.agentId,
name: "Worker",
profileId: "mock-claude",
busy: { state: "busy", sinceMs: 1 },
},
],
});
renderGrid(setup.gateways, { onOpenConversation: vi.fn() });
await flush();
act(() => setup.system.emit(busy(setup.agentId, true)));
const cancel = await screen.findByRole("button", {
name: `cancel current turn ${setup.bId}`,
});
const status = cancel.parentElement as HTMLElement;
const { controls } = controlsFor(setup.bId);
expect(status.style.top).toBe("24px");
expect(status.style.zIndex).toBe("6");
expect(Number(status.style.zIndex)).toBeGreaterThan(Number(controls.style.zIndex));
expect(screen.queryByRole("button", {
name: `open cell conversation ${setup.bId}`,
})).toBeNull();
expect(screen.queryByText("History")).toBeNull();
});
it.each<CellAction>(["selector", "split", "close"])(
"keeps %s above/clickable over a terminal launch error",
async (action) => {

View File

@ -200,7 +200,6 @@ function NodeView({
workState={workState}
refreshWorkState={refreshWorkState}
refitSignal={refitSignal}
onOpenConversation={onOpenConversation}
/>
);
case "split":
@ -247,7 +246,6 @@ interface LeafViewProps {
workState: ProjectWorkState | null;
refreshWorkState: () => Promise<void>;
refitSignal: number;
onOpenConversation?: (conversationId: string) => void;
}
/**
@ -339,11 +337,13 @@ function goToCell(nodeId: string): void {
* status/error 3 (in-cell status strip, launch-error banner, notices)
* full-cell veils 4 (write-portal + F3 overlays; mutually exclusive)
* cell controls 5 (always on top and clickable)
* turn actions 6 (busy-state actions; Cancel must stay visible)
*/
const CELL_Z = {
banner: 3,
veil: 4,
controls: 5,
turnActions: 6,
} as const;
function LeafView({
@ -359,7 +359,6 @@ function LeafView({
workState,
refreshWorkState,
refitSignal,
onOpenConversation,
}: LeafViewProps) {
// A cell can be closed only when it lives inside a (binary) split: closing it
// collapses the parent split, keeping the *sibling*. Splits are always binary
@ -679,9 +678,6 @@ function LeafView({
) ?? [];
const busyByWorkState = agentWork?.busy.state === "busy" || Boolean(activeTicket);
const delegatedTicket = activeTicket?.source.kind === "agent" ? activeTicket : undefined;
const historyConversationId =
activeTicket?.conversationId ?? conversationId ?? null;
async function interruptCurrentTurn(): Promise<void> {
if (!agentId || !input) return;
try {
@ -1176,20 +1172,21 @@ function LeafView({
</button>
)}
</div>
{agentId && (busyByWorkState || inboxDepth > 0 || completedBackgroundTasks.length > 0 || historyConversationId) && (
{agentId && (busyByWorkState || inboxDepth > 0 || completedBackgroundTasks.length > 0) && (
<div
role="status"
aria-live="polite"
style={{
position: "absolute",
top: 30,
top: 24,
left: 4,
zIndex: CELL_Z.banner,
zIndex: busyByWorkState ? CELL_Z.turnActions : CELL_Z.banner,
display: "flex",
maxWidth: "calc(100% - 8px)",
alignItems: "center",
gap: 4,
overflow: "hidden",
pointerEvents: "auto",
}}
>
{busyByWorkState && (
@ -1289,26 +1286,6 @@ function LeafView({
Cancel
</button>
)}
{historyConversationId && onOpenConversation && (
<button
type="button"
aria-label={`open cell conversation ${id}`}
title="Open conversation history"
onClick={() => onOpenConversation(historyConversationId)}
style={{
flexShrink: 0,
border: "1px solid var(--color-border, #3a3a3a)",
borderRadius: 3,
background: "var(--color-surface, #1e1e1e)",
color: "var(--color-content, #e0e0e0)",
cursor: "pointer",
fontSize: 11,
padding: "1px 6px",
}}
>
History
</button>
)}
</div>
)}
{/* Option 1 (Terminal + MCP): every cell — plain or agent — renders the