46e3899945
- Changes (#72): a file git reports `MM` whose working copy equals HEAD again (a staged change since undone on disk) is no longer listed — JetBrains' GitChangesCollector drops it the same way. Mode-only changes stay listed. statusCounts (the dirty-tree safety checks) is unchanged. - Chat (#73): Claude Code runs no hook before a folder is trusted, so a new pane's session has no row; the chat now says the session has not started, offers Open terminal, and asks before typing a message or /model //effort into the pane (an Enter would pick the trust prompt's "No, exit"). - Terminal: Safari + macOS's own Vietnamese Telex replaces the whole word on each key (insertReplacementText), which xterm.js ignores — only each word's first letter reached the pane. The edit is now sent as DELs + the new tail. - Also: Ctrl+V pastes a screenshot into the terminal as a file path; the folder browser fallback lists files for a file field (`files=1`); the macOS native picker opens in front of the browser; a hidden tab is marked while a chat waits or once a turn ended; chat live line shows running calls, turn time and tokens, and "Done in …".
124 lines
4.6 KiB
TypeScript
124 lines
4.6 KiB
TypeScript
/**
|
|
* @file ChatOutline.test.tsx
|
|
* @description The Chat pane's activity panel renders a session's outline the
|
|
* way Claude Desktop does: progress with done/current items, outputs by name
|
|
* with an EXT tag (a click opens that file), and "Used in this session" rows —
|
|
* uploads, files read, memory and skills open their list.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
import { fireEvent, render, screen } from "@testing-library/react";
|
|
import { MemoryRouter } from "react-router-dom";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import { ChatOutline } from "../ChatOutline";
|
|
|
|
const outline = {
|
|
progress: [
|
|
{ id: "1", subject: "Parse plan", status: "completed" },
|
|
{ id: "2", subject: "Build report", status: "in_progress" },
|
|
],
|
|
outputs: ["/w/out/Progress report.xlsx"],
|
|
files: ["/w/src/a.ts", "/w/src/b.ts", "/w/src/c.ts"],
|
|
uploads: ["/tmp/agenthub-drops-1000/0123456789abcdef-minutes.xlsx"],
|
|
memory: ["/h/.claude/projects/-w/memory/release.md"],
|
|
skills: ["xlsx"],
|
|
mcp: ["vercel_vercel"],
|
|
};
|
|
|
|
describe("ChatOutline", () => {
|
|
it("shows progress, outputs and what the session used", () => {
|
|
const onClose = vi.fn();
|
|
const onOpenFile = vi.fn();
|
|
const onOpenList = vi.fn();
|
|
render(
|
|
<MemoryRouter>
|
|
<ChatOutline
|
|
outline={outline}
|
|
onClose={onClose}
|
|
onOpenFile={onOpenFile}
|
|
onOpenList={onOpenList}
|
|
/>
|
|
</MemoryRouter>
|
|
);
|
|
expect(screen.getByText("Parse plan").className).toContain("line-through");
|
|
expect(screen.getByText("Build report").className).not.toContain("line-through");
|
|
// How far the plan is, and which step is being worked on.
|
|
expect(screen.getByText("1/2")).toBeTruthy();
|
|
expect(screen.getByRole("progressbar")).toHaveAttribute("aria-valuenow", "1");
|
|
expect(screen.getByTitle("Build report")).toHaveAttribute("aria-current", "step");
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "XLSX: Progress report" }));
|
|
expect(onOpenFile).toHaveBeenCalledWith("/w/out/Progress report.xlsx");
|
|
|
|
// An upload is named without the drop dir's random prefix.
|
|
fireEvent.click(screen.getByTitle("Uploads: minutes.xlsx"));
|
|
expect(onOpenList).toHaveBeenCalledWith("uploads");
|
|
expect(screen.getByText("+1")).toBeTruthy(); // three files read, two named
|
|
fireEvent.click(screen.getByTitle("Memory: release"));
|
|
expect(onOpenList).toHaveBeenCalledWith("memory");
|
|
fireEvent.click(screen.getByTitle("Skills: xlsx"));
|
|
expect(onOpenList).toHaveBeenCalledWith("skills");
|
|
expect(screen.getByText("vercel_vercel")).toBeTruthy();
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "Hide side panel" }));
|
|
expect(onClose).toHaveBeenCalled();
|
|
});
|
|
|
|
it("drags its rows into the prompt box", () => {
|
|
render(
|
|
<MemoryRouter>
|
|
<ChatOutline
|
|
outline={outline}
|
|
onClose={() => {}}
|
|
onOpenFile={() => {}}
|
|
onOpenList={() => {}}
|
|
/>
|
|
</MemoryRouter>
|
|
);
|
|
const dragged = (el: HTMLElement) => {
|
|
const data: Record<string, string> = {};
|
|
fireEvent.dragStart(el, {
|
|
dataTransfer: { setData: (k: string, v: string) => (data[k] = v) },
|
|
});
|
|
return data;
|
|
};
|
|
const attached = (el: HTMLElement) =>
|
|
JSON.parse(dragged(el)["application/x-agenthub-attach"] ?? "[]") as { path: string }[];
|
|
expect(attached(screen.getByRole("button", { name: "XLSX: Progress report" }))).toEqual([
|
|
{ path: "@/w/out/Progress report.xlsx", name: "Progress report.xlsx" },
|
|
]);
|
|
expect(attached(screen.getByTitle(/a\.ts, /)).map((a) => a.path)).toEqual([
|
|
"@/w/src/a.ts",
|
|
"@/w/src/b.ts",
|
|
"@/w/src/c.ts",
|
|
]);
|
|
// An upload goes as the composer attaches one: its bare path.
|
|
expect(attached(screen.getByTitle("Uploads: minutes.xlsx"))[0]!.path).toBe(outline.uploads[0]);
|
|
expect(dragged(screen.getByTitle("Skills: xlsx"))["application/x-agenthub-skill"]).toBe("xlsx");
|
|
});
|
|
|
|
it("hides progress and the used section when there is none", () => {
|
|
render(
|
|
<MemoryRouter>
|
|
<ChatOutline
|
|
outline={{
|
|
...outline,
|
|
progress: [],
|
|
files: [],
|
|
uploads: [],
|
|
memory: [],
|
|
skills: [],
|
|
mcp: [],
|
|
}}
|
|
onClose={() => {}}
|
|
onOpenFile={() => {}}
|
|
onOpenList={() => {}}
|
|
/>
|
|
</MemoryRouter>
|
|
);
|
|
expect(screen.queryByText("Progress")).toBeNull();
|
|
expect(screen.queryByText("Used in this session")).toBeNull();
|
|
expect(screen.getByRole("button", { name: "Hide side panel" })).toBeTruthy();
|
|
});
|
|
});
|