chore: v2.8.23 — workspace groups, inner repos in Changes (opt-in), sub-agent tokens counted
Workspace groups: named, foldable, ordered sidebar sections kept per account (agenthub.workspace.groups); a workspace joins one from its menu, a drop on the header, a drag under a row, or Alt+Up/Down past a group's edge; headers drag to reorder; a folded header shows its most urgent state; New workspace in this group; fold/unfold all. Inner repos: GET /api/lanes/:id/repos + ?repo= on git/fs routes; the Changes panel shows a parent folder's repos only when Settings -> Git -> Changes panel turns it on (agenthub.changes.innerRepos, off by default). Cost: the hook writer adds a session's sub-agent transcripts to its tokens and hours (server/lib/subagent-usage.js) — a sub-agent on the session's own model was never counted live (~8% of a week here); an imported session is re-raised on every pass, so a claude -p run first seen with an empty transcript no longer stays at $0 (~10%). A one-time raise-only catch-up (user_version 5) re-reads ~/.claude/projects. Also: omp install script may redirect to its exact install.redirect URL (#85, repo-links test exempts it), native paste in the terminal.
This commit is contained in:
@@ -101,6 +101,8 @@ vi.mock("../../lib/api", async (importOriginal) => {
|
||||
: [],
|
||||
})
|
||||
),
|
||||
// The repos inside the workspace folder (`?repo=`): none by default.
|
||||
repos: vi.fn().mockResolvedValue({ root: true, items: [], truncated: false }),
|
||||
// Closing a folder's section removes it from the workspace.
|
||||
removeSource: r({
|
||||
sources: [{ id: 8, kind: "file", path: "/d/notes.md", name: "notes.md", missing: false }],
|
||||
@@ -330,6 +332,79 @@ describe("Workspace — the Changes panel's width", () => {
|
||||
expect(screen.queryByTestId("panel-git-source")).toBeNull();
|
||||
});
|
||||
|
||||
it("neither walks a parent folder nor shows its repos until the account turns it on", async () => {
|
||||
vi.mocked(api.lanes.repos).mockClear();
|
||||
vi.mocked(api.lanes.repos).mockResolvedValue({
|
||||
root: false,
|
||||
items: [{ path: "api", name: "api" }],
|
||||
truncated: false,
|
||||
});
|
||||
render(
|
||||
<MemoryRouter initialEntries={["/workspace?lane=2"]}>
|
||||
<Workspace />
|
||||
</MemoryRouter>
|
||||
);
|
||||
await settle();
|
||||
const rail = screen.queryByTestId("lane-changes-expand");
|
||||
if (rail) {
|
||||
fireEvent.click(rail);
|
||||
await settle();
|
||||
}
|
||||
expect(api.lanes.repos).not.toHaveBeenCalled();
|
||||
expect(screen.queryByTestId("panel-git-source")).toBeNull();
|
||||
vi.mocked(api.lanes.repos).mockResolvedValue({ root: true, items: [], truncated: false });
|
||||
});
|
||||
|
||||
it("gives each repo inside a parent folder its own section, without the folder's own", async () => {
|
||||
// Settings → Git → Changes panel: show the repositories inside.
|
||||
window.localStorage.setItem("agenthub.changes.innerRepos", "all");
|
||||
vi.mocked(api.lanes.repos).mockResolvedValue({
|
||||
root: false,
|
||||
items: [
|
||||
{ path: "api", name: "api" },
|
||||
{ path: "apps/web", name: "apps/web" },
|
||||
],
|
||||
truncated: false,
|
||||
});
|
||||
vi.mocked(api.lanes.git).mockClear();
|
||||
render(
|
||||
<MemoryRouter initialEntries={["/workspace?lane=2"]}>
|
||||
<Workspace />
|
||||
</MemoryRouter>
|
||||
);
|
||||
await settle();
|
||||
const rail = screen.queryByTestId("lane-changes-expand");
|
||||
if (rail) {
|
||||
fireEvent.click(rail);
|
||||
await settle();
|
||||
}
|
||||
const sections = screen.getAllByTestId("panel-git-source");
|
||||
expect(sections.map((s) => s.querySelector("button")!.textContent)).toEqual([
|
||||
"api",
|
||||
"apps/web",
|
||||
]);
|
||||
expect(api.lanes.git).toHaveBeenCalledWith({ lane: 2, repo: "apps/web" });
|
||||
// An inner repo is the folder's own: its menu rescans, never closes it.
|
||||
fireEvent.contextMenu(sections[1]!.querySelector("button")!);
|
||||
expect(screen.queryByTestId("panel-git-source-close")).toBeNull();
|
||||
fireEvent.click(screen.getByTestId("panel-git-source-rescan"));
|
||||
expect(api.lanes.repos).toHaveBeenLastCalledWith(2, true);
|
||||
// A chat's file inside an inner repo opens in that repo's section.
|
||||
fireEvent.click(sections[1]!.querySelector("button")!);
|
||||
expect(sections[1]!.querySelector("button")).toHaveAttribute("aria-expanded", "false");
|
||||
act(() => {
|
||||
openInChanges(2, "/workspace/beta", "/workspace/beta/apps/web/src/x.ts");
|
||||
});
|
||||
await settle();
|
||||
expect(sections[1]!.querySelector("button")).toHaveAttribute("aria-expanded", "true");
|
||||
expect(api.lanes.gitFile).toHaveBeenCalledWith(
|
||||
{ lane: 2, repo: "apps/web" },
|
||||
"src/x.ts",
|
||||
"work"
|
||||
);
|
||||
vi.mocked(api.lanes.repos).mockResolvedValue({ root: true, items: [], truncated: false });
|
||||
});
|
||||
|
||||
it("is one plain panel for a workspace with no extra folders", async () => {
|
||||
render(
|
||||
<MemoryRouter initialEntries={["/workspace?lane=2"]}>
|
||||
|
||||
Reference in New Issue
Block a user