This commit is contained in:
@ -9,6 +9,7 @@
|
|||||||
*/
|
*/
|
||||||
import { describe, it, expect, vi } from "vitest";
|
import { describe, it, expect, vi } from "vitest";
|
||||||
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||||
|
import type { ComponentProps } from "react";
|
||||||
|
|
||||||
vi.mock("@xterm/xterm", () => ({
|
vi.mock("@xterm/xterm", () => ({
|
||||||
Terminal: class {
|
Terminal: class {
|
||||||
@ -54,6 +55,7 @@ import {
|
|||||||
MockProfileGateway,
|
MockProfileGateway,
|
||||||
MockSystemGateway,
|
MockSystemGateway,
|
||||||
MockTerminalGateway,
|
MockTerminalGateway,
|
||||||
|
MockWorkStateGateway,
|
||||||
} from "@/adapters/mock";
|
} from "@/adapters/mock";
|
||||||
import { DIProvider } from "@/app/di";
|
import { DIProvider } from "@/app/di";
|
||||||
import { AnnouncementsProvider } from "@/features/announcements";
|
import { AnnouncementsProvider } from "@/features/announcements";
|
||||||
@ -66,6 +68,7 @@ interface GridSetup {
|
|||||||
gateways: Gateways;
|
gateways: Gateways;
|
||||||
layout: MockLayoutGateway;
|
layout: MockLayoutGateway;
|
||||||
system: MockSystemGateway;
|
system: MockSystemGateway;
|
||||||
|
workState: MockWorkStateGateway;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function makeSplitAgentGrid(): Promise<GridSetup> {
|
async function makeSplitAgentGrid(): Promise<GridSetup> {
|
||||||
@ -74,6 +77,7 @@ async function makeSplitAgentGrid(): Promise<GridSetup> {
|
|||||||
const profileGateway = new MockProfileGateway();
|
const profileGateway = new MockProfileGateway();
|
||||||
const terminal = new MockTerminalGateway();
|
const terminal = new MockTerminalGateway();
|
||||||
const system = new MockSystemGateway();
|
const system = new MockSystemGateway();
|
||||||
|
const workState = new MockWorkStateGateway();
|
||||||
await profileGateway.saveProfile(MOCK_REFERENCE_PROFILES[0]);
|
await profileGateway.saveProfile(MOCK_REFERENCE_PROFILES[0]);
|
||||||
|
|
||||||
const agent = await agentGateway.createAgent("p1", {
|
const agent = await agentGateway.createAgent("p1", {
|
||||||
@ -105,17 +109,22 @@ async function makeSplitAgentGrid(): Promise<GridSetup> {
|
|||||||
profile: profileGateway,
|
profile: profileGateway,
|
||||||
terminal,
|
terminal,
|
||||||
system,
|
system,
|
||||||
|
workState,
|
||||||
} as unknown as Gateways,
|
} as unknown as Gateways,
|
||||||
layout,
|
layout,
|
||||||
system,
|
system,
|
||||||
|
workState,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderGrid(gateways: Gateways) {
|
function renderGrid(
|
||||||
|
gateways: Gateways,
|
||||||
|
props: Partial<ComponentProps<typeof LayoutGrid>> = {},
|
||||||
|
) {
|
||||||
return render(
|
return render(
|
||||||
<DIProvider gateways={gateways}>
|
<DIProvider gateways={gateways}>
|
||||||
<AnnouncementsProvider>
|
<AnnouncementsProvider>
|
||||||
<LayoutGrid projectId="p1" cwd="/home/me/proj" />
|
<LayoutGrid projectId="p1" cwd="/home/me/proj" {...props} />
|
||||||
</AnnouncementsProvider>
|
</AnnouncementsProvider>
|
||||||
</DIProvider>,
|
</DIProvider>,
|
||||||
);
|
);
|
||||||
@ -197,6 +206,44 @@ describe("LayoutGrid — ticket #48 cell control layering", () => {
|
|||||||
expect(controls.style.overflow).toBe("hidden");
|
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"])(
|
it.each<CellAction>(["selector", "split", "close"])(
|
||||||
"keeps %s above/clickable over a terminal launch error",
|
"keeps %s above/clickable over a terminal launch error",
|
||||||
async (action) => {
|
async (action) => {
|
||||||
|
|||||||
@ -200,7 +200,6 @@ function NodeView({
|
|||||||
workState={workState}
|
workState={workState}
|
||||||
refreshWorkState={refreshWorkState}
|
refreshWorkState={refreshWorkState}
|
||||||
refitSignal={refitSignal}
|
refitSignal={refitSignal}
|
||||||
onOpenConversation={onOpenConversation}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "split":
|
case "split":
|
||||||
@ -247,7 +246,6 @@ interface LeafViewProps {
|
|||||||
workState: ProjectWorkState | null;
|
workState: ProjectWorkState | null;
|
||||||
refreshWorkState: () => Promise<void>;
|
refreshWorkState: () => Promise<void>;
|
||||||
refitSignal: number;
|
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)
|
* status/error 3 (in-cell status strip, launch-error banner, notices)
|
||||||
* full-cell veils 4 (write-portal + F3 overlays; mutually exclusive)
|
* full-cell veils 4 (write-portal + F3 overlays; mutually exclusive)
|
||||||
* cell controls 5 (always on top and clickable)
|
* cell controls 5 (always on top and clickable)
|
||||||
|
* turn actions 6 (busy-state actions; Cancel must stay visible)
|
||||||
*/
|
*/
|
||||||
const CELL_Z = {
|
const CELL_Z = {
|
||||||
banner: 3,
|
banner: 3,
|
||||||
veil: 4,
|
veil: 4,
|
||||||
controls: 5,
|
controls: 5,
|
||||||
|
turnActions: 6,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
function LeafView({
|
function LeafView({
|
||||||
@ -359,7 +359,6 @@ function LeafView({
|
|||||||
workState,
|
workState,
|
||||||
refreshWorkState,
|
refreshWorkState,
|
||||||
refitSignal,
|
refitSignal,
|
||||||
onOpenConversation,
|
|
||||||
}: LeafViewProps) {
|
}: LeafViewProps) {
|
||||||
// A cell can be closed only when it lives inside a (binary) split: closing it
|
// 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
|
// 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 busyByWorkState = agentWork?.busy.state === "busy" || Boolean(activeTicket);
|
||||||
const delegatedTicket = activeTicket?.source.kind === "agent" ? activeTicket : undefined;
|
const delegatedTicket = activeTicket?.source.kind === "agent" ? activeTicket : undefined;
|
||||||
const historyConversationId =
|
|
||||||
activeTicket?.conversationId ?? conversationId ?? null;
|
|
||||||
|
|
||||||
async function interruptCurrentTurn(): Promise<void> {
|
async function interruptCurrentTurn(): Promise<void> {
|
||||||
if (!agentId || !input) return;
|
if (!agentId || !input) return;
|
||||||
try {
|
try {
|
||||||
@ -1176,20 +1172,21 @@ function LeafView({
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{agentId && (busyByWorkState || inboxDepth > 0 || completedBackgroundTasks.length > 0 || historyConversationId) && (
|
{agentId && (busyByWorkState || inboxDepth > 0 || completedBackgroundTasks.length > 0) && (
|
||||||
<div
|
<div
|
||||||
role="status"
|
role="status"
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: 30,
|
top: 24,
|
||||||
left: 4,
|
left: 4,
|
||||||
zIndex: CELL_Z.banner,
|
zIndex: busyByWorkState ? CELL_Z.turnActions : CELL_Z.banner,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
maxWidth: "calc(100% - 8px)",
|
maxWidth: "calc(100% - 8px)",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: 4,
|
gap: 4,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
|
pointerEvents: "auto",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{busyByWorkState && (
|
{busyByWorkState && (
|
||||||
@ -1289,26 +1286,6 @@ function LeafView({
|
|||||||
Cancel
|
Cancel
|
||||||
</button>
|
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* Option 1 (Terminal + MCP): every cell — plain or agent — renders the
|
{/* Option 1 (Terminal + MCP): every cell — plain or agent — renders the
|
||||||
|
|||||||
Reference in New Issue
Block a user