feat(rpc): add validated desktop Pi RPC command palette
This commit is contained in:
@@ -4,12 +4,14 @@ import { describe, expect, it, vi } from "vitest";
|
||||
import { DirectorySidebar } from "./DirectorySidebar";
|
||||
import { SessionTabs } from "./SessionTabs";
|
||||
import { SessionPicker } from "./SessionPicker";
|
||||
import { ConversationWorkspace } from "./ConversationWorkspace";
|
||||
import { ExtensionDialog } from "./ExtensionDialog";
|
||||
import {
|
||||
initialWorkspaceState,
|
||||
reconcileWorkspace,
|
||||
runtimeView,
|
||||
} from "../workspace/reducer";
|
||||
import type { Workspace } from "../workspace/types";
|
||||
import type { DirectorySession, Workspace } from "../workspace/types";
|
||||
const summary = (id: string, state = "idle") => ({
|
||||
runtimeId: id,
|
||||
worktreePath: "/repo",
|
||||
@@ -96,41 +98,172 @@ describe("multi-session navigation", () => {
|
||||
expect(screen.getByRole("tab", { name: /b/ })).toHaveFocus();
|
||||
expect(onFocusRestored).toHaveBeenCalled();
|
||||
});
|
||||
it("traps and restores focus for the saved-session picker", async () => {
|
||||
it("collapses a tool result until its summary is expanded", async () => {
|
||||
const user = userEvent.setup();
|
||||
const runtime = runtimeView(summary("tool-result"));
|
||||
runtime.messages = [
|
||||
{ role: "toolResult", toolName: "read", content: "Detailed tool output" },
|
||||
];
|
||||
const { container } = render(
|
||||
<ConversationWorkspace
|
||||
runtime={runtime}
|
||||
onDraft={vi.fn()}
|
||||
onSubmit={vi.fn()}
|
||||
onAbort={vi.fn()}
|
||||
onScroll={vi.fn()}
|
||||
onModel={vi.fn()}
|
||||
onThinking={vi.fn()}
|
||||
onCommand={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
const details = container.querySelector("details");
|
||||
expect(details).not.toHaveAttribute("open");
|
||||
await user.click(screen.getByText("Tool result · read"));
|
||||
expect(details).toHaveAttribute("open");
|
||||
});
|
||||
it("shows an aborting control and labels an accepted follow-up", () => {
|
||||
const runtime = runtimeView(summary("working", "streaming"));
|
||||
runtime.progress = {
|
||||
phase: "working",
|
||||
detail: "Thinking and preparing a response",
|
||||
toolCount: 0,
|
||||
};
|
||||
runtime.pendingSubmissions = [
|
||||
{
|
||||
id: 1,
|
||||
text: "Continue with the plan",
|
||||
userMessageCount: 0,
|
||||
phase: "sent",
|
||||
delivery: "follow_up",
|
||||
},
|
||||
];
|
||||
render(
|
||||
<ConversationWorkspace
|
||||
runtime={runtime}
|
||||
onDraft={vi.fn()}
|
||||
onSubmit={vi.fn()}
|
||||
onAbort={vi.fn()}
|
||||
onScroll={vi.fn()}
|
||||
onModel={vi.fn()}
|
||||
onThinking={vi.fn()}
|
||||
onCommand={vi.fn()}
|
||||
isAborting
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByRole("button", { name: "Aborting…" })).toBeDisabled();
|
||||
expect(
|
||||
screen.getByText("Follow-up queued · waiting for Pi"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
it("searches saved sessions and displays useful metadata", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onOpen = vi.fn();
|
||||
const sessions: DirectorySession[] = [
|
||||
{
|
||||
path: "/planning",
|
||||
id: "plan-id",
|
||||
cwd: "/repo",
|
||||
name: "Planning",
|
||||
firstMessage: "Sketch the release plan",
|
||||
modified: "2026-08-15T10:00:00.000Z",
|
||||
messageCount: 1,
|
||||
runtimeId: "already-open",
|
||||
},
|
||||
{
|
||||
path: "/incident",
|
||||
id: "incident-42",
|
||||
cwd: "/repo",
|
||||
firstMessage: "Investigate search crash",
|
||||
modified: "2026-08-14T10:00:00.000Z",
|
||||
messageCount: 2,
|
||||
},
|
||||
];
|
||||
render(
|
||||
<SessionPicker sessions={sessions} onOpen={onOpen} onClose={vi.fn()} />,
|
||||
);
|
||||
const search = screen.getByRole("searchbox", {
|
||||
name: "Search saved sessions",
|
||||
});
|
||||
expect(screen.getByRole("status")).toHaveTextContent("2 saved sessions");
|
||||
expect(screen.getByText("Sketch the release plan")).toBeInTheDocument();
|
||||
expect(screen.getByText("1 message")).toBeInTheDocument();
|
||||
expect(screen.getByText("Open")).toBeInTheDocument();
|
||||
|
||||
await user.type(search, "release");
|
||||
expect(screen.getByRole("status")).toHaveTextContent("1 saved session");
|
||||
expect(
|
||||
screen.getByRole("button", { name: /Planning/ }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: /Investigate/ })).toBeNull();
|
||||
await user.clear(search);
|
||||
await user.type(search, "INCIDENT-42");
|
||||
await user.click(screen.getByRole("button", { name: /Investigate/ }));
|
||||
expect(onOpen).toHaveBeenCalledWith(sessions[1]);
|
||||
await user.clear(search);
|
||||
await user.type(search, "missing");
|
||||
expect(screen.getByText(/No saved sessions match/)).toBeInTheDocument();
|
||||
});
|
||||
it("focuses search, traps focus, and restores the opener", async () => {
|
||||
const user = userEvent.setup();
|
||||
const opener = document.createElement("button");
|
||||
document.body.append(opener);
|
||||
opener.focus();
|
||||
const onClose = vi.fn();
|
||||
const { unmount } = render(
|
||||
<SessionPicker
|
||||
sessions={[
|
||||
{
|
||||
path: "/s",
|
||||
id: "s",
|
||||
cwd: "/repo",
|
||||
modified: new Date().toISOString(),
|
||||
messageCount: 0,
|
||||
},
|
||||
]}
|
||||
onOpen={vi.fn()}
|
||||
onClose={onClose}
|
||||
/>,
|
||||
<SessionPicker sessions={[]} onOpen={vi.fn()} onClose={onClose} />,
|
||||
);
|
||||
const search = screen.getByRole("searchbox", {
|
||||
name: "Search saved sessions",
|
||||
});
|
||||
const close = screen.getByRole("button", { name: "Close" });
|
||||
expect(search).toHaveFocus();
|
||||
await user.keyboard("{Shift>}{Tab}{/Shift}");
|
||||
expect(close).toHaveFocus();
|
||||
await user.keyboard("{Tab}");
|
||||
expect(
|
||||
screen.getByRole("button", { name: /Untitled session/ }),
|
||||
).toHaveFocus();
|
||||
await user.keyboard("{Tab}");
|
||||
expect(close).toHaveFocus();
|
||||
await user.keyboard("{Shift>}{Tab}{/Shift}");
|
||||
expect(search).toHaveFocus();
|
||||
await user.keyboard("{Escape}");
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
unmount();
|
||||
expect(opener).toHaveFocus();
|
||||
document.body.removeChild(opener);
|
||||
});
|
||||
it("labels permission requests with the action and exact details", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onRespond = vi.fn();
|
||||
render(
|
||||
<ExtensionDialog
|
||||
extension={{
|
||||
method: "confirm",
|
||||
title: "Run a shell command",
|
||||
message:
|
||||
"rm -rf ./build\nThis removes the generated build directory.",
|
||||
}}
|
||||
onRespond={onRespond}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText("Pi agent request")).toBeInTheDocument();
|
||||
expect(screen.getByText("Requested action")).toBeInTheDocument();
|
||||
expect(screen.getByText("Run a shell command")).toBeInTheDocument();
|
||||
expect(screen.getByText("Request details")).toBeInTheDocument();
|
||||
expect(screen.getByText(/rm -rf .\/build/)).toBeInTheDocument();
|
||||
await user.click(screen.getByRole("button", { name: "Allow request" }));
|
||||
expect(onRespond).toHaveBeenCalledWith({ confirmed: true });
|
||||
});
|
||||
it("uses Pi editor prefill content", () => {
|
||||
render(
|
||||
<ExtensionDialog
|
||||
extension={{
|
||||
method: "editor",
|
||||
title: "Edit request",
|
||||
prefill: "Existing request",
|
||||
}}
|
||||
onRespond={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByLabelText("Extension value")).toHaveValue(
|
||||
"Existing request",
|
||||
);
|
||||
});
|
||||
it("routes new, saved-open, and close controls through callbacks", async () => {
|
||||
const onNew = vi.fn(),
|
||||
onResume = vi.fn(),
|
||||
|
||||
Reference in New Issue
Block a user