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:
@@ -2092,6 +2092,95 @@ describe("an extra folder's own repo (?source=)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("repos inside a parent workspace folder (?repo=)", () => {
|
||||
const { execFileSync } = require("child_process");
|
||||
const gitEnv = Object.fromEntries(
|
||||
Object.entries(process.env).filter(([k]) => !k.startsWith("GIT_"))
|
||||
);
|
||||
const gitIn = (dir, ...args) =>
|
||||
execFileSync("git", ["-C", dir, "-c", "user.name=t", "-c", "user.email=t@t", ...args], {
|
||||
encoding: "utf8",
|
||||
env: gitEnv,
|
||||
});
|
||||
let parent, laneId, otherLaneId;
|
||||
|
||||
before(async () => {
|
||||
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "lane-nested-")));
|
||||
parent = path.join(root, "shop");
|
||||
for (const rel of ["api", "web", "group/admin"]) {
|
||||
const dir = path.join(parent, rel);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
gitIn(dir, "init", "-q", "-b", "main");
|
||||
fs.writeFileSync(path.join(dir, "a.txt"), "a\n");
|
||||
gitIn(dir, "add", ".");
|
||||
gitIn(dir, "commit", "-qm", "init");
|
||||
}
|
||||
// Not repos: a plain folder, a dot folder and node_modules holding one,
|
||||
// and a symlink to a repo (never followed).
|
||||
fs.mkdirSync(path.join(parent, "docs"));
|
||||
fs.mkdirSync(path.join(parent, ".cache/x/.git"), { recursive: true });
|
||||
fs.mkdirSync(path.join(parent, "node_modules/pkg/.git"), { recursive: true });
|
||||
fs.symlinkSync(path.join(parent, "api"), path.join(parent, "api-link"));
|
||||
// A repo found inside a repo is not walked into.
|
||||
fs.mkdirSync(path.join(parent, "web/inner/.git"), { recursive: true });
|
||||
laneId = (await request("POST", "/api/lanes", { cwd: parent })).body.lane.id;
|
||||
otherLaneId = (await request("POST", "/api/lanes", { cwd: path.join(root, "other") })).body.lane
|
||||
.id;
|
||||
});
|
||||
|
||||
it("lists the repos below the workspace folder", async () => {
|
||||
const r = await request("GET", `/api/lanes/${laneId}/repos`);
|
||||
assert.equal(r.status, 200);
|
||||
assert.equal(r.body.root, false);
|
||||
assert.deepEqual(
|
||||
r.body.items.map((i) => i.path),
|
||||
["api", "group/admin", "web"]
|
||||
);
|
||||
});
|
||||
|
||||
it("reads and writes git in the named repo only", async () => {
|
||||
fs.writeFileSync(path.join(parent, "group/admin/new.txt"), "x\n");
|
||||
const changes = await request("GET", `/api/lanes/${laneId}/git/changes?repo=group/admin`);
|
||||
assert.ok(changes.body.files.some((f) => f.path === "new.txt"));
|
||||
const facts = await request("GET", `/api/lanes/${laneId}/git?repo=api`);
|
||||
assert.equal(facts.body.available, true);
|
||||
assert.equal(facts.body.base, "main");
|
||||
const ok = await request("POST", `/api/lanes/${laneId}/git/stage?repo=group/admin`, {
|
||||
paths: ["new.txt"],
|
||||
});
|
||||
assert.equal(ok.status, 200);
|
||||
assert.match(
|
||||
gitIn(path.join(parent, "group/admin"), "diff", "--cached", "--name-only"),
|
||||
/new\.txt/
|
||||
);
|
||||
});
|
||||
|
||||
it("answers ENOREPO for any path the walk did not find", async () => {
|
||||
for (const rel of ["docs", "..", "../other", "/etc", "api-link", ".cache/x", "web/inner", ""]) {
|
||||
const r = await request(
|
||||
"GET",
|
||||
`/api/lanes/${laneId}/git/changes?repo=${encodeURIComponent(rel)}`
|
||||
);
|
||||
assert.equal(r.status, 404, rel);
|
||||
assert.equal(r.body.error.code, "ENOREPO", rel);
|
||||
}
|
||||
const r = await request("GET", `/api/lanes/${otherLaneId}/git/changes?repo=api`);
|
||||
assert.equal(r.status, 404);
|
||||
assert.equal(r.body.error.code, "ENOREPO");
|
||||
});
|
||||
|
||||
it("finds a repo cloned since the last read", async () => {
|
||||
require("../lib/nested-repos").clearNestedRepoCache();
|
||||
await request("GET", `/api/lanes/${laneId}/repos`);
|
||||
await new Promise((r) => setTimeout(r, 2100));
|
||||
const dir = path.join(parent, "late");
|
||||
fs.mkdirSync(dir);
|
||||
gitIn(dir, "init", "-q", "-b", "main");
|
||||
const r = await request("GET", `/api/lanes/${laneId}/git?repo=late`);
|
||||
assert.equal(r.status, 200);
|
||||
});
|
||||
});
|
||||
|
||||
// The server, not a stale client list, picks the tab and refuses a second pane
|
||||
// on a session one already holds.
|
||||
describe("start: slot auto and a session already live", () => {
|
||||
|
||||
Reference in New Issue
Block a user