feat(cli): historique de messages navigable haut/bas dans la CLI custom + contrôles de cellule visibles pendant busy — persistance localStorage du transcript custom (CustomAgentChatView), layering des contrôles sous l'overlay busy (LayoutGrid), bump pointeur IdeaSDK (docs plugin minimal + hot reload) (QA verte: 65 tests CustomAgentChatView, 25 tests LayoutGrid chat/layering, npm build frontend OK)
This commit is contained in:
@ -140,6 +140,7 @@ function setMeasuredTextOverflow(
|
|||||||
describe("CustomAgentChatView", () => {
|
describe("CustomAgentChatView", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
window.localStorage.clear();
|
||||||
__clearCustomAgentChatViewCacheForTests();
|
__clearCustomAgentChatViewCacheForTests();
|
||||||
__resetProjectCliInteractionActivityForTests();
|
__resetProjectCliInteractionActivityForTests();
|
||||||
});
|
});
|
||||||
@ -236,6 +237,144 @@ describe("CustomAgentChatView", () => {
|
|||||||
expect(screen.queryByRole("button", { name: "Ouvrir en TUI native" })).toBeNull();
|
expect(screen.queryByRole("button", { name: "Ouvrir en TUI native" })).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("recalls previously sent prompts with arrow up and down", async () => {
|
||||||
|
const agent = {
|
||||||
|
launchAgentChat: vi.fn(),
|
||||||
|
reattachAgentChat: vi.fn(async (sessionId: string) => ({
|
||||||
|
sessionId,
|
||||||
|
scrollback: [],
|
||||||
|
})),
|
||||||
|
sendAgentChat: vi.fn(
|
||||||
|
async (_sessionId: string, prompt: string, onChunk: (chunk: ReplyChunk) => void) => {
|
||||||
|
onChunk({ kind: "final", content: `answer ${prompt}` });
|
||||||
|
},
|
||||||
|
),
|
||||||
|
cancelAgentChat: vi.fn(async () => {}),
|
||||||
|
closeAgentChat: vi.fn(async () => {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
render(
|
||||||
|
<DIProvider
|
||||||
|
gateways={{
|
||||||
|
agent,
|
||||||
|
system: { pickFile: vi.fn(async () => null) },
|
||||||
|
} as unknown as Gateways}
|
||||||
|
>
|
||||||
|
<CustomAgentChatView
|
||||||
|
projectId="project-1"
|
||||||
|
agentId="agent-1"
|
||||||
|
agentName="Worker"
|
||||||
|
profile={profile}
|
||||||
|
cwd="/repo"
|
||||||
|
layoutId="layout-1"
|
||||||
|
nodeId="node-1"
|
||||||
|
sessionId="chat-session-1"
|
||||||
|
conversationId="conversation-1"
|
||||||
|
onSessionId={vi.fn()}
|
||||||
|
onConversationId={vi.fn()}
|
||||||
|
/>
|
||||||
|
</DIProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const composer = await screen.findByLabelText(/message CLI custom/) as HTMLTextAreaElement;
|
||||||
|
await waitFor(() => expect(agent.reattachAgentChat).toHaveBeenCalled());
|
||||||
|
|
||||||
|
fireEvent.change(composer, { target: { value: "first prompt" } });
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||||
|
await waitFor(() => expect(agent.sendAgentChat).toHaveBeenCalledWith(
|
||||||
|
"chat-session-1",
|
||||||
|
"first prompt",
|
||||||
|
expect.any(Function),
|
||||||
|
));
|
||||||
|
|
||||||
|
fireEvent.change(composer, { target: { value: "second prompt" } });
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||||
|
await waitFor(() => expect(agent.sendAgentChat).toHaveBeenCalledWith(
|
||||||
|
"chat-session-1",
|
||||||
|
"second prompt",
|
||||||
|
expect.any(Function),
|
||||||
|
));
|
||||||
|
|
||||||
|
fireEvent.keyDown(composer, { key: "ArrowUp" });
|
||||||
|
await waitFor(() => expect(composer.value).toBe("second prompt"));
|
||||||
|
fireEvent.keyDown(composer, { key: "ArrowUp" });
|
||||||
|
await waitFor(() => expect(composer.value).toBe("first prompt"));
|
||||||
|
fireEvent.keyDown(composer, { key: "ArrowDown" });
|
||||||
|
await waitFor(() => expect(composer.value).toBe("second prompt"));
|
||||||
|
|
||||||
|
fireEvent.change(composer, { target: { value: "draft in progress" } });
|
||||||
|
fireEvent.keyDown(composer, { key: "ArrowUp" });
|
||||||
|
await waitFor(() => expect(composer.value).toBe("second prompt"));
|
||||||
|
fireEvent.keyDown(composer, { key: "ArrowDown" });
|
||||||
|
await waitFor(() => expect(composer.value).toBe("draft in progress"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("persists prompt history across a custom chat remount", async () => {
|
||||||
|
const agent = {
|
||||||
|
launchAgentChat: vi.fn(),
|
||||||
|
reattachAgentChat: vi.fn(async (sessionId: string) => ({
|
||||||
|
sessionId,
|
||||||
|
scrollback: [],
|
||||||
|
})),
|
||||||
|
sendAgentChat: vi.fn(
|
||||||
|
async (_sessionId: string, prompt: string, onChunk: (chunk: ReplyChunk) => void) => {
|
||||||
|
onChunk({ kind: "final", content: `answer ${prompt}` });
|
||||||
|
},
|
||||||
|
),
|
||||||
|
cancelAgentChat: vi.fn(async () => {}),
|
||||||
|
closeAgentChat: vi.fn(async () => {}),
|
||||||
|
};
|
||||||
|
const props = {
|
||||||
|
projectId: "project-1",
|
||||||
|
agentId: "agent-1",
|
||||||
|
agentName: "Worker",
|
||||||
|
profile,
|
||||||
|
cwd: "/repo",
|
||||||
|
layoutId: "layout-1",
|
||||||
|
nodeId: "node-1",
|
||||||
|
sessionId: "chat-session-1",
|
||||||
|
conversationId: "conversation-1",
|
||||||
|
onSessionId: vi.fn(),
|
||||||
|
onConversationId: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const view = render(
|
||||||
|
<DIProvider
|
||||||
|
gateways={{
|
||||||
|
agent,
|
||||||
|
system: { pickFile: vi.fn(async () => null) },
|
||||||
|
} as unknown as Gateways}
|
||||||
|
>
|
||||||
|
<CustomAgentChatView {...props} />
|
||||||
|
</DIProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
let composer = await screen.findByLabelText(/message CLI custom/) as HTMLTextAreaElement;
|
||||||
|
fireEvent.change(composer, { target: { value: "remember me" } });
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||||
|
await waitFor(() => expect(agent.sendAgentChat).toHaveBeenCalledWith(
|
||||||
|
"chat-session-1",
|
||||||
|
"remember me",
|
||||||
|
expect.any(Function),
|
||||||
|
));
|
||||||
|
|
||||||
|
view.unmount();
|
||||||
|
render(
|
||||||
|
<DIProvider
|
||||||
|
gateways={{
|
||||||
|
agent,
|
||||||
|
system: { pickFile: vi.fn(async () => null) },
|
||||||
|
} as unknown as Gateways}
|
||||||
|
>
|
||||||
|
<CustomAgentChatView {...props} />
|
||||||
|
</DIProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
composer = await screen.findByLabelText(/message CLI custom/) as HTMLTextAreaElement;
|
||||||
|
fireEvent.keyDown(composer, { key: "ArrowUp" });
|
||||||
|
await waitFor(() => expect(composer.value).toBe("remember me"));
|
||||||
|
});
|
||||||
|
|
||||||
it("repaints attachment labels from user prompt scrollback", async () => {
|
it("repaints attachment labels from user prompt scrollback", async () => {
|
||||||
const agent = {
|
const agent = {
|
||||||
launchAgentChat: vi.fn(),
|
launchAgentChat: vi.fn(),
|
||||||
|
|||||||
@ -83,6 +83,7 @@ const chatConversationViewCache = new Map<string, CachedChatView>();
|
|||||||
const EMPTY_MODEL_RESPONSE_MESSAGE = "Le modèle n'a renvoyé aucune réponse.";
|
const EMPTY_MODEL_RESPONSE_MESSAGE = "Le modèle n'a renvoyé aucune réponse.";
|
||||||
const COMPOSER_RESIZE_STEP = 12;
|
const COMPOSER_RESIZE_STEP = 12;
|
||||||
const FALLBACK_COMPOSER_HEIGHT = 64;
|
const FALLBACK_COMPOSER_HEIGHT = 64;
|
||||||
|
const COMPOSER_HISTORY_LIMIT = 100;
|
||||||
const MIN_REASONABLE_CHAT_TIMESTAMP_MS = Date.UTC(2020, 0, 1);
|
const MIN_REASONABLE_CHAT_TIMESTAMP_MS = Date.UTC(2020, 0, 1);
|
||||||
const MAX_REASONABLE_CHAT_TIMESTAMP_MS = Date.UTC(2100, 0, 1);
|
const MAX_REASONABLE_CHAT_TIMESTAMP_MS = Date.UTC(2100, 0, 1);
|
||||||
const MIN_REASONABLE_CHAT_TIMESTAMP_SECONDS = Math.floor(
|
const MIN_REASONABLE_CHAT_TIMESTAMP_SECONDS = Math.floor(
|
||||||
@ -416,6 +417,55 @@ function conversationCacheKey(
|
|||||||
return conversationId ? `${projectId}::${agentId}::${conversationId}` : null;
|
return conversationId ? `${projectId}::${agentId}::${conversationId}` : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function composerHistoryStorageKey(
|
||||||
|
projectId: string,
|
||||||
|
agentId: string,
|
||||||
|
layoutId: string | undefined,
|
||||||
|
nodeId: string | undefined,
|
||||||
|
): string {
|
||||||
|
return [
|
||||||
|
"idea.custom-chat-history",
|
||||||
|
projectId,
|
||||||
|
layoutId ?? "default",
|
||||||
|
nodeId ?? "agent",
|
||||||
|
agentId,
|
||||||
|
].join(".");
|
||||||
|
}
|
||||||
|
|
||||||
|
function readComposerHistory(storageKey: string): string[] {
|
||||||
|
if (typeof window === "undefined") return [];
|
||||||
|
try {
|
||||||
|
const parsed: unknown = JSON.parse(window.localStorage.getItem(storageKey) ?? "[]");
|
||||||
|
if (!Array.isArray(parsed)) return [];
|
||||||
|
return parsed
|
||||||
|
.filter((entry): entry is string => typeof entry === "string")
|
||||||
|
.map((entry) => entry.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.slice(-COMPOSER_HISTORY_LIMIT);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeComposerHistory(storageKey: string, history: string[]): void {
|
||||||
|
if (typeof window === "undefined") return;
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(
|
||||||
|
storageKey,
|
||||||
|
JSON.stringify(history.slice(-COMPOSER_HISTORY_LIMIT)),
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
/* local history only */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendComposerHistoryEntry(history: string[], text: string): string[] {
|
||||||
|
const entry = text.trim();
|
||||||
|
if (!entry) return history;
|
||||||
|
const withoutDuplicateTail = history.at(-1) === entry ? history.slice(0, -1) : history;
|
||||||
|
return [...withoutDuplicateTail, entry].slice(-COMPOSER_HISTORY_LIMIT);
|
||||||
|
}
|
||||||
|
|
||||||
function appendProgress(state: ChatViewState, progress: ReplyProgress): ChatViewState {
|
function appendProgress(state: ChatViewState, progress: ReplyProgress): ChatViewState {
|
||||||
let key = progressUpsertKey(progress, state.progressScope);
|
let key = progressUpsertKey(progress, state.progressScope);
|
||||||
let existingIndex = state.turns.findIndex(
|
let existingIndex = state.turns.findIndex(
|
||||||
@ -953,6 +1003,7 @@ export function CustomAgentChatView({
|
|||||||
const lastPersistedDraftRef = useRef("");
|
const lastPersistedDraftRef = useRef("");
|
||||||
const draftRef = useRef("");
|
const draftRef = useRef("");
|
||||||
draftRef.current = draft;
|
draftRef.current = draft;
|
||||||
|
const draftBeforeHistoryNavigationRef = useRef("");
|
||||||
const attachmentsRef = useRef<AttachmentDraft[]>([]);
|
const attachmentsRef = useRef<AttachmentDraft[]>([]);
|
||||||
attachmentsRef.current = attachments;
|
attachmentsRef.current = attachments;
|
||||||
const imageTokenSeqRef = useRef(0);
|
const imageTokenSeqRef = useRef(0);
|
||||||
@ -1061,6 +1112,14 @@ export function CustomAgentChatView({
|
|||||||
() => conversationCacheKey(projectId, agentId, conversationId),
|
() => conversationCacheKey(projectId, agentId, conversationId),
|
||||||
[agentId, conversationId, projectId],
|
[agentId, conversationId, projectId],
|
||||||
);
|
);
|
||||||
|
const composerHistoryKey = useMemo(
|
||||||
|
() => composerHistoryStorageKey(projectId, agentId, layoutId, nodeId),
|
||||||
|
[agentId, layoutId, nodeId, projectId],
|
||||||
|
);
|
||||||
|
const [composerHistory, setComposerHistory] = useState<string[]>(() =>
|
||||||
|
readComposerHistory(composerHistoryKey),
|
||||||
|
);
|
||||||
|
const [, setComposerHistoryIndex] = useState<number | null>(null);
|
||||||
const displayTurns = turns;
|
const displayTurns = turns;
|
||||||
const sessionLabel = structuredSessionLabel(profile.structuredAdapter);
|
const sessionLabel = structuredSessionLabel(profile.structuredAdapter);
|
||||||
const [requesterDelegation, setRequesterDelegation] =
|
const [requesterDelegation, setRequesterDelegation] =
|
||||||
@ -1127,9 +1186,82 @@ export function CustomAgentChatView({
|
|||||||
|
|
||||||
const editDraft = useCallback((text: string) => {
|
const editDraft = useCallback((text: string) => {
|
||||||
if (!draftHydratedRef.current) draftEditedBeforeHydrationRef.current = true;
|
if (!draftHydratedRef.current) draftEditedBeforeHydrationRef.current = true;
|
||||||
|
setComposerHistoryIndex(null);
|
||||||
|
draftBeforeHistoryNavigationRef.current = "";
|
||||||
setDraft(text);
|
setDraft(text);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const recallComposerHistory = useCallback(
|
||||||
|
(direction: "previous" | "next") => {
|
||||||
|
if (composerHistory.length === 0) return;
|
||||||
|
setComposerHistoryIndex((current) => {
|
||||||
|
if (current === null && direction === "previous") {
|
||||||
|
draftBeforeHistoryNavigationRef.current = draftRef.current;
|
||||||
|
}
|
||||||
|
const nextIndex =
|
||||||
|
direction === "previous"
|
||||||
|
? current === null
|
||||||
|
? composerHistory.length - 1
|
||||||
|
: Math.max(0, current - 1)
|
||||||
|
: current === null
|
||||||
|
? null
|
||||||
|
: current >= composerHistory.length - 1
|
||||||
|
? null
|
||||||
|
: current + 1;
|
||||||
|
if (nextIndex === null) {
|
||||||
|
setDraft(draftBeforeHistoryNavigationRef.current);
|
||||||
|
draftBeforeHistoryNavigationRef.current = "";
|
||||||
|
} else {
|
||||||
|
setDraft(composerHistory[nextIndex] ?? "");
|
||||||
|
}
|
||||||
|
return nextIndex;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[composerHistory],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleComposerHistoryKeyDown = useCallback(
|
||||||
|
(event: KeyboardEvent<HTMLTextAreaElement>): boolean => {
|
||||||
|
if (
|
||||||
|
event.altKey ||
|
||||||
|
event.ctrlKey ||
|
||||||
|
event.metaKey ||
|
||||||
|
event.shiftKey ||
|
||||||
|
(event.key !== "ArrowUp" && event.key !== "ArrowDown")
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const target = event.currentTarget;
|
||||||
|
if (
|
||||||
|
event.key === "ArrowUp" &&
|
||||||
|
target.value.slice(0, target.selectionStart).includes("\n")
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
event.key === "ArrowDown" &&
|
||||||
|
target.value.slice(target.selectionEnd).includes("\n")
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
recallComposerHistory(event.key === "ArrowUp" ? "previous" : "next");
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
[recallComposerHistory],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const restored = readComposerHistory(composerHistoryKey);
|
||||||
|
setComposerHistory(restored);
|
||||||
|
setComposerHistoryIndex(null);
|
||||||
|
draftBeforeHistoryNavigationRef.current = "";
|
||||||
|
}, [composerHistoryKey]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
writeComposerHistory(composerHistoryKey, composerHistory);
|
||||||
|
}, [composerHistory, composerHistoryKey]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void refreshRequesterDelegation();
|
void refreshRequesterDelegation();
|
||||||
}, [refreshRequesterDelegation]);
|
}, [refreshRequesterDelegation]);
|
||||||
@ -1913,6 +2045,11 @@ export function CustomAgentChatView({
|
|||||||
markProjectCliInteractionFinished(projectId, interactionId);
|
markProjectCliInteractionFinished(projectId, interactionId);
|
||||||
activeInteractionIdRef.current = null;
|
activeInteractionIdRef.current = null;
|
||||||
};
|
};
|
||||||
|
if (text) {
|
||||||
|
setComposerHistory((prev) => appendComposerHistoryEntry(prev, text));
|
||||||
|
setComposerHistoryIndex(null);
|
||||||
|
draftBeforeHistoryNavigationRef.current = "";
|
||||||
|
}
|
||||||
setDraft("");
|
setDraft("");
|
||||||
setAttachments([]);
|
setAttachments([]);
|
||||||
imageTokenSeqRef.current = 0;
|
imageTokenSeqRef.current = 0;
|
||||||
@ -2373,6 +2510,7 @@ export function CustomAgentChatView({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (handleComposerHistoryKeyDown(e)) return;
|
||||||
if (e.key === "Enter" && !e.shiftKey) {
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
void send();
|
void send();
|
||||||
|
|||||||
@ -64,6 +64,7 @@ import { LayoutGrid } from "./LayoutGrid";
|
|||||||
|
|
||||||
interface GridSetup {
|
interface GridSetup {
|
||||||
agentId: string;
|
agentId: string;
|
||||||
|
otherAgentId: string;
|
||||||
bId: string;
|
bId: string;
|
||||||
gateways: Gateways;
|
gateways: Gateways;
|
||||||
layout: MockLayoutGateway;
|
layout: MockLayoutGateway;
|
||||||
@ -84,6 +85,10 @@ async function makeSplitAgentGrid(): Promise<GridSetup> {
|
|||||||
name: "Worker",
|
name: "Worker",
|
||||||
profileId: "mock-claude",
|
profileId: "mock-claude",
|
||||||
});
|
});
|
||||||
|
const otherAgent = await agentGateway.createAgent("p1", {
|
||||||
|
name: "Reviewer",
|
||||||
|
profileId: "mock-claude",
|
||||||
|
});
|
||||||
|
|
||||||
const initial = await layout.loadLayout("p1");
|
const initial = await layout.loadLayout("p1");
|
||||||
const aId = leaves(initial)[0].id;
|
const aId = leaves(initial)[0].id;
|
||||||
@ -102,6 +107,7 @@ async function makeSplitAgentGrid(): Promise<GridSetup> {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
agentId: agent.id,
|
agentId: agent.id,
|
||||||
|
otherAgentId: otherAgent.id,
|
||||||
bId: "b",
|
bId: "b",
|
||||||
gateways: {
|
gateways: {
|
||||||
layout,
|
layout,
|
||||||
@ -206,7 +212,7 @@ describe("LayoutGrid — ticket #48 cell control layering", () => {
|
|||||||
expect(controls.style.overflow).toBe("hidden");
|
expect(controls.style.overflow).toBe("hidden");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not render persistent cell chrome over the custom CLI surface (#249)", async () => {
|
it("keeps cell controls available over a busy custom CLI surface (#249)", async () => {
|
||||||
const setup = await makeSplitAgentGrid();
|
const setup = await makeSplitAgentGrid();
|
||||||
window.localStorage.setItem(`idea.agent-cell-mode.p1.${setup.bId}`, "custom");
|
window.localStorage.setItem(`idea.agent-cell-mode.p1.${setup.bId}`, "custom");
|
||||||
setup.workState._setProjectWorkState("p1", {
|
setup.workState._setProjectWorkState("p1", {
|
||||||
@ -227,19 +233,47 @@ describe("LayoutGrid — ticket #48 cell control layering", () => {
|
|||||||
const customToolbar = screen.getByRole("toolbar", {
|
const customToolbar = screen.getByRole("toolbar", {
|
||||||
name: "custom agent chat actions",
|
name: "custom agent chat actions",
|
||||||
});
|
});
|
||||||
|
const mutate = vi.spyOn(setup.layout, "mutateLayout");
|
||||||
|
|
||||||
expect(customToolbar.className).toContain("z-20");
|
expect(customToolbar.className).toContain("z-20");
|
||||||
expect(screen.queryByLabelText(`agent selector ${setup.bId}`)).toBeNull();
|
expect(screen.getByLabelText(`agent selector ${setup.bId}`)).toBeTruthy();
|
||||||
expect(screen.queryByRole("group", {
|
expect(screen.getByRole("group", {
|
||||||
name: `mode CLI agent ${setup.bId}`,
|
name: `mode CLI agent ${setup.bId}`,
|
||||||
})).toBeNull();
|
})).toBeTruthy();
|
||||||
expect(screen.queryByLabelText(`split ${setup.bId} columns`)).toBeNull();
|
expect(screen.getByLabelText(`split ${setup.bId} columns`)).toBeTruthy();
|
||||||
expect(screen.queryByLabelText(`split ${setup.bId} rows`)).toBeNull();
|
expect(screen.getByLabelText(`split ${setup.bId} rows`)).toBeTruthy();
|
||||||
expect(screen.queryByLabelText(`close ${setup.bId}`)).toBeNull();
|
expect(screen.getByLabelText(`close ${setup.bId}`)).toBeTruthy();
|
||||||
expect(screen.queryByRole("button", {
|
expect(screen.getByRole("button", {
|
||||||
name: `cancel current turn ${setup.bId}`,
|
name: `cancel current turn ${setup.bId}`,
|
||||||
})).toBeNull();
|
})).toBeTruthy();
|
||||||
expect(screen.queryByText("Busy")).toBeNull();
|
expect(screen.getByText("Busy")).toBeTruthy();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByLabelText(`split ${setup.bId} columns`));
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(mutate).toHaveBeenCalledWith(
|
||||||
|
"p1",
|
||||||
|
expect.objectContaining({
|
||||||
|
type: "split",
|
||||||
|
target: setup.bId,
|
||||||
|
direction: "row",
|
||||||
|
}),
|
||||||
|
undefined,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const selector = screen.getByLabelText(`agent selector ${setup.bId}`);
|
||||||
|
fireEvent.change(selector, { target: { value: setup.otherAgentId } });
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(mutate).toHaveBeenCalledWith(
|
||||||
|
"p1",
|
||||||
|
expect.objectContaining({
|
||||||
|
type: "setCellAgent",
|
||||||
|
target: setup.bId,
|
||||||
|
agent: setup.otherAgentId,
|
||||||
|
}),
|
||||||
|
undefined,
|
||||||
|
),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps busy Cancel above persistent cell controls and removes History (#151, #160)", async () => {
|
it("keeps busy Cancel above persistent cell controls and removes History (#151, #160)", async () => {
|
||||||
|
|||||||
@ -219,11 +219,11 @@ describe("LayoutGrid custom agent CLI (#147)", () => {
|
|||||||
|
|
||||||
expect(await screen.findByTestId("custom-agent-chat-view")).toBeTruthy();
|
expect(await screen.findByTestId("custom-agent-chat-view")).toBeTruthy();
|
||||||
expect(screen.queryByTestId("target-announcements-overlay")).toBeNull();
|
expect(screen.queryByTestId("target-announcements-overlay")).toBeNull();
|
||||||
expect(screen.queryByText("Busy · Main")).toBeNull();
|
expect(screen.getByText("Busy · Main")).toBeTruthy();
|
||||||
expect(screen.queryByLabelText(`agent selector ${leaf.id}`)).toBeNull();
|
expect(screen.getByLabelText(`agent selector ${leaf.id}`)).toBeTruthy();
|
||||||
expect(screen.queryByRole("button", {
|
expect(screen.getByRole("button", {
|
||||||
name: `cancel current turn ${leaf.id}`,
|
name: `cancel current turn ${leaf.id}`,
|
||||||
})).toBeNull();
|
})).toBeTruthy();
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
expect(screen.getByLabelText(/message CLI custom/)).toHaveProperty(
|
expect(screen.getByLabelText(/message CLI custom/)).toHaveProperty(
|
||||||
"disabled",
|
"disabled",
|
||||||
|
|||||||
@ -655,13 +655,6 @@ function LeafView({
|
|||||||
customCliAvailable && pinnedAgent ? pinnedAgent : trustedCustomCli?.agent;
|
customCliAvailable && pinnedAgent ? pinnedAgent : trustedCustomCli?.agent;
|
||||||
const customCliProfile =
|
const customCliProfile =
|
||||||
customCliAvailable && pinnedProfile ? pinnedProfile : trustedCustomCli?.profile;
|
customCliAvailable && pinnedProfile ? pinnedProfile : trustedCustomCli?.profile;
|
||||||
const customCliActive = Boolean(
|
|
||||||
agentId &&
|
|
||||||
cellMode === "custom" &&
|
|
||||||
effectiveCustomCliAvailable &&
|
|
||||||
customCliAgent &&
|
|
||||||
customCliProfile,
|
|
||||||
);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (cellMode !== "custom") {
|
if (cellMode !== "custom") {
|
||||||
setTrustedCustomCli(null);
|
setTrustedCustomCli(null);
|
||||||
@ -774,7 +767,6 @@ function LeafView({
|
|||||||
task.status === "completed" || task.status === "failed" || task.status === "cancelled",
|
task.status === "completed" || task.status === "failed" || task.status === "cancelled",
|
||||||
) ?? [];
|
) ?? [];
|
||||||
const busyByWorkState = agentWork?.busy.state === "busy" || Boolean(activeTicket);
|
const busyByWorkState = agentWork?.busy.state === "busy" || Boolean(activeTicket);
|
||||||
const suppressCellChrome = customCliActive && busyByWorkState;
|
|
||||||
const delegatedTicket = activeTicket?.source.kind === "agent" ? activeTicket : undefined;
|
const delegatedTicket = activeTicket?.source.kind === "agent" ? activeTicket : undefined;
|
||||||
const busyActive = announcementBusyActive || busyByWorkState;
|
const busyActive = announcementBusyActive || busyByWorkState;
|
||||||
const targetAnnouncementsFallback = delegatedTicket?.requesterLabel
|
const targetAnnouncementsFallback = delegatedTicket?.requesterLabel
|
||||||
@ -1138,27 +1130,27 @@ function LeafView({
|
|||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!suppressCellChrome && (
|
<div
|
||||||
<div
|
style={{
|
||||||
style={{
|
position: "absolute",
|
||||||
position: "absolute",
|
top: 2,
|
||||||
top: 2,
|
left: 4,
|
||||||
left: 4,
|
// Clear the xterm viewport scrollbar (rendered flush against the right
|
||||||
// Clear the xterm viewport scrollbar (rendered flush against the right
|
// edge, ~15px wide). Without this offset the right-most control — the
|
||||||
// edge, ~15px wide). Without this offset the right-most control — the
|
// close button — sits *behind* the scrollbar and is hard to click.
|
||||||
// close button — sits *behind* the scrollbar and is hard to click.
|
right: 16,
|
||||||
right: 16,
|
// Above every banner/veil so a cell in an error state stays manageable
|
||||||
// Above every banner/veil so a cell in an error state stays manageable
|
// (ticket #48). Custom CLI cells also keep these controls available
|
||||||
// (ticket #48). Hidden while a custom CLI turn is busy: that surface
|
// while busy so the cell remains a view that can be split or retargeted
|
||||||
// owns its own header, and cell chrome must not overlap the chat viewport.
|
// without stopping the running agent session.
|
||||||
zIndex: CELL_Z.controls,
|
zIndex: CELL_Z.controls,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
gap: 2,
|
gap: 2,
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "flex-end",
|
justifyContent: "flex-end",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Agent selector */}
|
{/* Agent selector */}
|
||||||
<select
|
<select
|
||||||
aria-label={`agent selector ${id}`}
|
aria-label={`agent selector ${id}`}
|
||||||
@ -1330,9 +1322,8 @@ function LeafView({
|
|||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
{agentId && busyByWorkState && (
|
||||||
{agentId && busyByWorkState && !suppressCellChrome && (
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={`cancel current turn ${id}`}
|
aria-label={`cancel current turn ${id}`}
|
||||||
@ -1358,7 +1349,7 @@ function LeafView({
|
|||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{agentId && !suppressCellChrome && (busyByWorkState || inboxDepth > 0 || completedBackgroundTasks.length > 0) && (
|
{agentId && (busyByWorkState || inboxDepth > 0 || completedBackgroundTasks.length > 0) && (
|
||||||
<div
|
<div
|
||||||
role="status"
|
role="status"
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
|
|||||||
Submodule sdk/IdeaSDK updated: d1c3c00b4d...9829ccdf2a
Reference in New Issue
Block a user