fix(ui): Lot A #151 #158 #159 (QA verte)

- #151: chevauchement des boutons dans la barre supérieure pendant une conversation agent

- #158: popup de reprise de conversation intempestive (ajout cellule / changement layout / projet)

- #159: pastille d'activité de projet
This commit is contained in:
2026-08-06 10:23:23 +02:00
parent fb19ee48dc
commit c5d6c3b98e
10 changed files with 351 additions and 6 deletions

View File

@ -48,8 +48,10 @@ if (typeof globalThis.ResizeObserver === "undefined") {
import type { DomainEvent } from "@/domain";
import type { Gateways } from "@/ports";
import {
MOCK_REFERENCE_PROFILES,
MockAgentGateway,
MockLayoutGateway,
MockProfileGateway,
MockSystemGateway,
MockTerminalGateway,
} from "@/adapters/mock";
@ -69,12 +71,14 @@ interface GridSetup {
async function makeSplitAgentGrid(): Promise<GridSetup> {
const layout = new MockLayoutGateway();
const agentGateway = new MockAgentGateway();
const profileGateway = new MockProfileGateway();
const terminal = new MockTerminalGateway();
const system = new MockSystemGateway();
await profileGateway.saveProfile(MOCK_REFERENCE_PROFILES[0]);
const agent = await agentGateway.createAgent("p1", {
name: "Worker",
profileId: "claude",
profileId: "mock-claude",
});
const initial = await layout.loadLayout("p1");
@ -98,6 +102,7 @@ async function makeSplitAgentGrid(): Promise<GridSetup> {
gateways: {
layout,
agent: agentGateway,
profile: profileGateway,
terminal,
system,
} as unknown as Gateways,
@ -174,6 +179,24 @@ function expectedOperation(action: CellAction, cellId: string) {
}
describe("LayoutGrid — ticket #48 cell control layering", () => {
it("keeps custom CLI mode controls compact inside the cell toolbar (#151)", async () => {
const setup = await makeSplitAgentGrid();
renderGrid(setup.gateways);
const group = await screen.findByRole("group", {
name: `mode CLI agent ${setup.bId}`,
});
const { controls } = controlsFor(setup.bId);
expect(group.textContent).toBe("TUI nativeCLI custom");
expect(group.style.maxWidth).toBe("128px");
expect(controls.style.left).toBe("4px");
expect(controls.style.right).toBe("16px");
expect(controls.style.justifyContent).toBe("flex-end");
expect(controls.style.overflow).toBe("hidden");
});
it.each<CellAction>(["selector", "split", "close"])(
"keeps %s above/clickable over a terminal launch error",
async (action) => {

View File

@ -922,6 +922,13 @@ function LeafView({
if (!conversationId) {
return doLaunch(opts, onData, undefined);
}
// If the cell was closed/remounted while the agent was running, resume
// silently. Layout/project changes must not interrupt an ongoing turn
// with a modal choice; the persisted conversation id is the continuity
// signal and `agentWasRunning` tells us this was not an intentional stop.
if (agentWasRunning) {
return doLaunch(opts, onData, conversationId);
}
// Resume case: defer the launch behind the popup. Fetch the best-effort
// enriched details (last topic + tokens) to enrich it; failure or empty
// ⇒ degraded mode (status only). Inspection never blocks the resume.
@ -982,6 +989,7 @@ function LeafView({
style={{
position: "absolute",
top: 2,
left: 4,
// Clear the xterm viewport scrollbar (rendered flush against the right
// edge, ~15px wide). Without this offset the right-most control — the
// close button — sits *behind* the scrollbar and is hard to click.
@ -992,6 +1000,8 @@ function LeafView({
display: "flex",
gap: 2,
alignItems: "center",
justifyContent: "flex-end",
overflow: "hidden",
}}
>
{/* Agent selector */}
@ -1053,7 +1063,8 @@ function LeafView({
border: "1px solid var(--color-border, #3a3a3a)",
borderRadius: 3,
padding: "1px 2px",
maxWidth: 100,
width: 96,
minWidth: 56,
}}
>
<option value="">Plain</option>
@ -1079,6 +1090,7 @@ function LeafView({
border: "1px solid var(--color-border, #3a3a3a)",
borderRadius: 3,
background: "var(--color-surface, #1e1e1e)",
maxWidth: 128,
}}
>
<button
@ -1100,6 +1112,10 @@ function LeafView({
fontSize: 11,
padding: "1px 6px",
cursor: "pointer",
maxWidth: 66,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
TUI native
@ -1122,6 +1138,10 @@ function LeafView({
fontSize: 11,
padding: "1px 6px",
cursor: "pointer",
maxWidth: 70,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
CLI custom

View File

@ -313,4 +313,40 @@ describe("LayoutGrid — resume popup wiring (T7)", () => {
expect(screen.queryByTestId("resume-conversation-popup")).toBeNull();
expect(inspectConversation).not.toHaveBeenCalled();
});
it("does not show the popup for a remounted cell whose agent was still running (#158)", async () => {
const layout = new MockLayoutGateway();
const agent = new MockAgentGateway();
const a = await agent.createAgent("p1", { name: "MyAgent", profileId: "p" });
const leafId = await seedResumeLeaf(layout, a.id);
await layout.mutateLayout("p1", {
type: "setAgentRunning",
target: leafId,
running: true,
});
const launchAgent = vi.fn(
async (
_p: string,
_ag: string,
_opts: OpenTerminalOptions,
_onData: (b: Uint8Array) => void,
) => makeHandle("sess-running-resume"),
);
const inspectConversation = vi.fn();
const stubAgent = {
listAgents: () => Promise.resolve([a]),
launchAgent,
reattach: vi.fn(),
inspectConversation,
};
renderGrid(layout, stubAgent);
await waitFor(() => expect(launchAgent).toHaveBeenCalledTimes(1));
expect(screen.queryByTestId("resume-conversation-popup")).toBeNull();
expect(inspectConversation).not.toHaveBeenCalled();
const opts = launchAgent.mock.calls[0][2] as OpenTerminalOptions;
expect(opts.conversationId).toBe("prior-conv");
});
});