4802684d2f
- A session picks its own AI tool: `tool` on POST /api/lanes/:id/start (a catalog id, known + enabled, else 400 EBADTOOL/EDISABLED); a resume keeps the tool its session ran on (`sessions.tool`). The workspace's `lanes.tool` is now only the default. - The workspace home's prompt box is the one place a session starts. Its fields run in order: AI → AI Router (Native or a profile) → Model (the tool's own list, or the router's models) → Permission (Claude mode / Codex sandbox+approval / catalog bypass) → Effort. - The pane's run form (RunSetup) is gone: New run, a single pane's exit and an empty split pane's New session all go to the home. RunSetup.tsx keeps PromptEditor, CwdAutocomplete and ModelPicker (new `choices` prop). - The tool icon moved from the sidebar's workspace row to each session row (right edge, `session.tool`); the split-pane header shows the run's tool. - An ended session opens in its workspace (`&session=`, SessionReplay.tsx): its conversation read-only plus a prompt box; only a SENT message resumes it (resumeSessionId + prompt, first free slot). The console is only hidden behind it. - Analyst button beside Files/Changes in the right panel opens /sessions/:id for the opened session. - Gemini CLI ships its own icon (client/public/ai-tools/gemini.svg, from zonalogo.com). - RouterPicker always shows (Native disabled for tools without router support); useAiRouters() exported. - Tests: server start-with-tool cases; SessionReplay, home field order/tool/ router models, empty pane, Analyst; dropped the cwd-field suites (Run.defaultCwd, Workspace.laneCwd). Docs: CLAUDE.md rules, API.md, LANES.md, README, ARCHITECTURE, INSTALL.
76 lines
2.8 KiB
TypeScript
76 lines
2.8 KiB
TypeScript
/**
|
|
* @file SessionReplay.test.tsx
|
|
* @description An ended session opened in its workspace: its conversation and
|
|
* a prompt box; opening it starts nothing, and only a SENT message resumes it
|
|
* (`resumeSessionId` + the prompt, in the first free pane slot), after which
|
|
* the pane opens.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { render, screen, waitFor } from "@testing-library/react";
|
|
import userEvent from "@testing-library/user-event";
|
|
import type { Lane } from "../../../lib/types";
|
|
import type { RunHandle } from "../../../lib/api";
|
|
|
|
vi.mock("../../conversation/ConversationView", () => ({
|
|
ConversationView: ({ sessionId }: { sessionId: string }) => (
|
|
<div data-testid="conversation">{sessionId}</div>
|
|
),
|
|
}));
|
|
vi.mock("../../run/RunSetup", () => ({
|
|
PromptEditor: ({
|
|
value,
|
|
onChange,
|
|
placeholder,
|
|
}: {
|
|
value: string;
|
|
onChange: (s: string) => void;
|
|
placeholder: string;
|
|
}) => (
|
|
<textarea aria-label={placeholder} value={value} onChange={(e) => onChange(e.target.value)} />
|
|
),
|
|
}));
|
|
vi.mock("../WorkspaceHome", () => ({
|
|
freeSlot: (_id: number, runs: RunHandle[]) => (runs.length ? 1 : 0),
|
|
paneRunId: (id: number, slot: number) =>
|
|
slot ? `agenthub-lane-${id}-${slot}` : `agenthub-lane-${id}`,
|
|
}));
|
|
vi.mock("../../../lib/api", () => ({
|
|
api: {
|
|
sessions: {
|
|
get: vi.fn(async () => ({ session: { id: "s-old", tool: "claude", cwd: "/r" } })),
|
|
},
|
|
lanes: { action: vi.fn(async () => ({ run_id: "agenthub-lane-1-1" })) },
|
|
},
|
|
}));
|
|
|
|
import { api } from "../../../lib/api";
|
|
import { SessionReplay } from "../SessionReplay";
|
|
|
|
const lane = { id: 1, cwd: "/r", tool: "claude" } as unknown as Lane;
|
|
const busy = [{ id: "agenthub-lane-1", laneId: 1, slot: 0, status: "running" }] as RunHandle[];
|
|
|
|
beforeEach(() => vi.clearAllMocks());
|
|
|
|
describe("SessionReplay", () => {
|
|
it("shows the conversation and resumes nothing until a message is sent", async () => {
|
|
const onResumed = vi.fn();
|
|
const user = userEvent.setup();
|
|
render(<SessionReplay lane={lane} sessionId="s-old" liveRuns={busy} onResumed={onResumed} />);
|
|
expect(screen.getByTestId("conversation")).toHaveTextContent("s-old");
|
|
await waitFor(() => expect(api.sessions.get).toHaveBeenCalledWith("s-old"));
|
|
expect(screen.getByTestId("session-replay-send")).toBeDisabled();
|
|
expect(api.lanes.action).not.toHaveBeenCalled();
|
|
|
|
await user.type(screen.getByLabelText("Message to continue this session…"), "carry on");
|
|
await user.click(screen.getByTestId("session-replay-send"));
|
|
expect(api.lanes.action).toHaveBeenCalledWith(1, "start", {
|
|
slot: 1,
|
|
resumeSessionId: "s-old",
|
|
prompt: "carry on",
|
|
});
|
|
await waitFor(() => expect(onResumed).toHaveBeenCalledWith("agenthub-lane-1-1"));
|
|
});
|
|
});
|